first commit
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
backend/target/
|
||||
spark-processor/target/
|
||||
frontend/node_modules/
|
||||
docs/node_modules/
|
||||
.idea/
|
||||
.vscode/
|
||||
.env
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
138
agricultural_stock.sql
Normal file
138
agricultural_stock.sql
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
|
||||
Source Server : mysql本地
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 80404 (8.4.4)
|
||||
Source Host : localhost:3306
|
||||
Source Schema : agricultural_stock
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 80404 (8.4.4)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 04/06/2025 20:19:54
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for industry_analysis
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `industry_analysis`;
|
||||
CREATE TABLE `industry_analysis` (
|
||||
`industry` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
|
||||
`stock_count` bigint NOT NULL,
|
||||
`avg_change_percent` double NULL DEFAULT NULL,
|
||||
`total_market_cap` double NULL DEFAULT NULL,
|
||||
`total_volume` double NULL DEFAULT NULL,
|
||||
`analysis_date` date NOT NULL
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for market_analysis
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `market_analysis`;
|
||||
CREATE TABLE `market_analysis` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`analysis_date` date NOT NULL COMMENT '分析日期',
|
||||
`up_count` int NULL DEFAULT NULL COMMENT '上涨股票数',
|
||||
`down_count` int NULL DEFAULT NULL COMMENT '下跌股票数',
|
||||
`flat_count` int NULL DEFAULT NULL COMMENT '平盘股票数',
|
||||
`total_count` int NULL DEFAULT NULL COMMENT '总股票数',
|
||||
`total_market_cap` decimal(15, 2) NULL DEFAULT NULL COMMENT '总市值',
|
||||
`total_volume` bigint NULL DEFAULT NULL COMMENT '总成交量',
|
||||
`total_turnover` decimal(15, 2) NULL DEFAULT NULL COMMENT '总成交额',
|
||||
`avg_change_percent` decimal(5, 2) NULL DEFAULT NULL COMMENT '平均涨跌幅',
|
||||
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_analysis_date`(`analysis_date` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '市场分析表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for market_trends
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `market_trends`;
|
||||
CREATE TABLE `market_trends` (
|
||||
`trade_date` timestamp NULL DEFAULT NULL,
|
||||
`avg_price` double NULL DEFAULT NULL,
|
||||
`avg_change_percent` double NULL DEFAULT NULL,
|
||||
`total_volume` double NULL DEFAULT NULL,
|
||||
`total_turnover` double NULL DEFAULT NULL,
|
||||
`stock_count` bigint NOT NULL
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for stock_data
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `stock_data`;
|
||||
CREATE TABLE `stock_data` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`stock_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '股票代码',
|
||||
`stock_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '股票名称',
|
||||
`open_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '开盘价',
|
||||
`close_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '收盘价',
|
||||
`high_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '最高价',
|
||||
`low_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '最低价',
|
||||
`volume` bigint NULL DEFAULT NULL COMMENT '成交量',
|
||||
`turnover` decimal(15, 2) NULL DEFAULT NULL COMMENT '成交额',
|
||||
`change_percent` decimal(5, 2) NULL DEFAULT NULL COMMENT '涨跌幅(%)',
|
||||
`change_amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '涨跌额',
|
||||
`total_shares` bigint NULL DEFAULT NULL COMMENT '总股本',
|
||||
`float_shares` bigint NULL DEFAULT NULL COMMENT '流通股本',
|
||||
`market_cap` decimal(15, 2) NULL DEFAULT NULL COMMENT '总市值',
|
||||
`float_market_cap` decimal(15, 2) NULL DEFAULT NULL COMMENT '流通市值',
|
||||
`pe_ratio` decimal(8, 2) NULL DEFAULT NULL COMMENT '市盈率',
|
||||
`pb_ratio` decimal(8, 2) NULL DEFAULT NULL COMMENT '市净率',
|
||||
`trade_date` datetime NOT NULL COMMENT '交易日期',
|
||||
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`deleted` tinyint NULL DEFAULT 0 COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_stock_code`(`stock_code` ASC) USING BTREE,
|
||||
INDEX `idx_trade_date`(`trade_date` ASC) USING BTREE,
|
||||
INDEX `idx_stock_trade`(`stock_code` ASC, `trade_date` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 26 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '股票数据表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for stock_prediction
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `stock_prediction`;
|
||||
CREATE TABLE `stock_prediction` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`stock_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '股票代码',
|
||||
`stock_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '股票名称',
|
||||
`predict_date` date NOT NULL COMMENT '预测日期',
|
||||
`predict_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '预测价格',
|
||||
`confidence` decimal(5, 2) NULL DEFAULT NULL COMMENT '置信度',
|
||||
`model_version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型版本',
|
||||
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_prediction_stock`(`stock_code` ASC) USING BTREE,
|
||||
INDEX `idx_prediction_date`(`predict_date` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '股票预测数据表' ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for stock_technical_indicators
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `stock_technical_indicators`;
|
||||
CREATE TABLE `stock_technical_indicators` (
|
||||
`stock_code` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
|
||||
`stock_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`trade_date` timestamp NULL DEFAULT NULL,
|
||||
`close_price` double NULL DEFAULT NULL,
|
||||
`ma5` double NULL DEFAULT NULL,
|
||||
`ma10` double NULL DEFAULT NULL,
|
||||
`ma20` double NULL DEFAULT NULL,
|
||||
`ma30` double NULL DEFAULT NULL,
|
||||
`rsi` double NULL DEFAULT NULL,
|
||||
`macd_dif` double NULL DEFAULT NULL,
|
||||
`macd_dea` double NULL DEFAULT NULL,
|
||||
`bb_upper` double NULL DEFAULT NULL,
|
||||
`bb_middle` double NULL DEFAULT NULL,
|
||||
`bb_lower` double NULL DEFAULT NULL
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
127
backend/pom.xml
Normal file
127
backend/pom.xml
Normal file
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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.agricultural</groupId>
|
||||
<artifactId>stock-platform-backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Agricultural Stock Platform Backend</name>
|
||||
<description>农业领域上市公司行情可视化监控平台后端</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.0</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>8</java.version>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starter Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Starter Data JPA -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Connector -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Kafka依赖已移除 -->
|
||||
|
||||
<!-- WebSocket -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI 3 -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-ui</artifactId>
|
||||
<version>1.6.9</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Lang -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.agricultural.stock;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
// Kafka导入已移除
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* 农业领域上市公司行情可视化监控平台启动类
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.agricultural.stock.mapper")
|
||||
@EnableCaching
|
||||
// @EnableKafka 已移除
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@EnableTransactionManagement
|
||||
public class AgriculturalStockPlatformApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AgriculturalStockPlatformApplication.class, args);
|
||||
System.out.println("==========================================");
|
||||
System.out.println("农业上市公司行情监控平台启动成功!");
|
||||
System.out.println("Swagger UI: http://localhost:8080/swagger-ui/index.html ");
|
||||
System.out.println("==========================================");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.agricultural.stock.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OpenAPI 配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("农业股票数据分析系统 API")
|
||||
.description("基于Spark大数据处理的农业股票市场监控与分析平台API文档\n\n" +
|
||||
"## 系统功能\n" +
|
||||
"- 🚀 **实时股票数据获取** - 获取最新的农业股票行情数据\n" +
|
||||
"- 📊 **市场分析** - 涨跌统计、市场总览、趋势分析\n" +
|
||||
"- 🔍 **股票搜索** - 根据代码或名称搜索股票\n" +
|
||||
"- 📈 **技术指标** - MA、RSI、MACD、布林带等技术分析\n" +
|
||||
"- 🏆 **排行榜** - 涨幅榜、跌幅榜、成交量榜\n" +
|
||||
"- 🔮 **趋势预测** - 基于历史数据的股票走势分析\n\n" +
|
||||
"## 访问地址\n" +
|
||||
"- **Swagger UI**: http://localhost:8080/swagger-ui/index.html\n" +
|
||||
"- **API Docs**: http://localhost:8080/v3/api-docs")
|
||||
.version("v1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("农业股票分析团队")
|
||||
.email("support@agricultural-stock.com")
|
||||
.url("https://github.com/agricultural-stock-platform"))
|
||||
.license(new License()
|
||||
.name("MIT License")
|
||||
.url("https://opensource.org/licenses/MIT")))
|
||||
.servers(List.of(
|
||||
new Server()
|
||||
.url("http://localhost:8080")
|
||||
.description("本地开发环境"),
|
||||
new Server()
|
||||
.url("https://api.agricultural-stock.com")
|
||||
.description("生产环境")
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.agricultural.stock.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 配置Swagger UI资源映射
|
||||
registry.addResourceHandler("/swagger-ui/**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/")
|
||||
.resourceChain(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
// 重定向根路径到Swagger UI
|
||||
registry.addViewController("/").setViewName("redirect:/swagger-ui/index.html");
|
||||
registry.addViewController("/swagger-ui").setViewName("redirect:/swagger-ui/index.html");
|
||||
registry.addViewController("/api").setViewName("redirect:/swagger-ui/index.html");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.agricultural.stock.controller;
|
||||
|
||||
import com.agricultural.stock.entity.MarketAnalysis;
|
||||
import com.agricultural.stock.service.MarketAnalysisService;
|
||||
import com.agricultural.stock.vo.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 市场分析数据API控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/market")
|
||||
@Tag(name = "市场分析数据API")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class MarketAnalysisController {
|
||||
|
||||
@Autowired
|
||||
private MarketAnalysisService marketAnalysisService;
|
||||
|
||||
/**
|
||||
* 获取最新的市场分析数据
|
||||
*/
|
||||
@GetMapping("/latest")
|
||||
@Operation(summary = "获取最新的市场分析数据")
|
||||
public Result<MarketAnalysis> getLatestMarketAnalysis() {
|
||||
try {
|
||||
MarketAnalysis marketAnalysis = marketAnalysisService.getLatestMarketAnalysis();
|
||||
if (marketAnalysis != null) {
|
||||
return Result.success(marketAnalysis);
|
||||
} else {
|
||||
return Result.error("暂无市场分析数据");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取最新市场分析数据失败", e);
|
||||
return Result.error("获取数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定日期范围的市场分析数据
|
||||
*/
|
||||
@GetMapping("/range")
|
||||
@Operation(summary = "获取指定日期范围的市场分析数据")
|
||||
public Result<List<MarketAnalysis>> getMarketAnalysisByDateRange(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
||||
try {
|
||||
List<MarketAnalysis> dataList = marketAnalysisService.getMarketAnalysisByDateRange(startDate, endDate);
|
||||
return Result.success(dataList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取指定日期范围的市场分析数据失败", e);
|
||||
return Result.error("获取数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近N天的市场分析数据
|
||||
*/
|
||||
@GetMapping("/recent/{days}")
|
||||
@Operation(summary = "获取最近N天的市场分析数据")
|
||||
public Result<List<MarketAnalysis>> getRecentMarketAnalysis(@PathVariable int days) {
|
||||
try {
|
||||
if (days <= 0 || days > 365) {
|
||||
return Result.error("天数参数无效,应在1-365之间");
|
||||
}
|
||||
List<MarketAnalysis> dataList = marketAnalysisService.getRecentMarketAnalysis(days);
|
||||
return Result.success(dataList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取最近{}天的市场分析数据失败", days, e);
|
||||
return Result.error("获取数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package com.agricultural.stock.controller;
|
||||
|
||||
import com.agricultural.stock.entity.StockData;
|
||||
import com.agricultural.stock.service.StockService;
|
||||
import com.agricultural.stock.vo.Result;
|
||||
import com.agricultural.stock.vo.StockAnalysisVO;
|
||||
import com.agricultural.stock.vo.StockTrendVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 股票数据API控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/stock")
|
||||
@Tag(name = "股票数据API")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class StockController {
|
||||
|
||||
@Autowired
|
||||
private StockService stockService;
|
||||
|
||||
/**
|
||||
* 获取实时股票数据
|
||||
*/
|
||||
@GetMapping("/realtime")
|
||||
@Operation(summary = "获取实时股票数据")
|
||||
public Result<List<StockData>> getRealtimeStockData() {
|
||||
try {
|
||||
List<StockData> stockDataList = stockService.getRealtimeStockData();
|
||||
return Result.success(stockDataList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取实时股票数据失败", e);
|
||||
return Result.error("获取实时股票数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据股票代码获取历史数据
|
||||
*/
|
||||
@GetMapping("/history/{stockCode}")
|
||||
@Operation(summary = "获取股票历史数据")
|
||||
public Result<List<StockData>> getHistoryData(
|
||||
@Parameter(description = "股票代码") @PathVariable String stockCode,
|
||||
@Parameter(description = "开始日期") @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
|
||||
@Parameter(description = "结束日期") @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) {
|
||||
try {
|
||||
List<StockData> historyData = stockService.getHistoryData(stockCode, startDate, endDate);
|
||||
return Result.success(historyData);
|
||||
} catch (Exception e) {
|
||||
log.error("获取股票{}历史数据失败", stockCode, e);
|
||||
return Result.error("获取历史数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取涨幅排行榜
|
||||
*/
|
||||
@GetMapping("/ranking/growth")
|
||||
@Operation(summary = "获取涨幅排行榜")
|
||||
public Result<List<StockData>> getGrowthRanking(
|
||||
@Parameter(description = "排行数量") @RequestParam(defaultValue = "10") Integer limit) {
|
||||
try {
|
||||
List<StockData> rankingList = stockService.getGrowthRanking(limit);
|
||||
return Result.success(rankingList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取涨幅排行榜失败", e);
|
||||
return Result.error("获取涨幅排行榜失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取市值排行榜
|
||||
*/
|
||||
@GetMapping("/ranking/market-cap")
|
||||
@Operation(summary = "获取市值排行榜")
|
||||
public Result<List<StockData>> getMarketCapRanking(
|
||||
@Parameter(description = "排行数量") @RequestParam(defaultValue = "10") Integer limit) {
|
||||
try {
|
||||
List<StockData> rankingList = stockService.getMarketCapRanking(limit);
|
||||
return Result.success(rankingList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取市值排行榜失败", e);
|
||||
return Result.error("获取市值排行榜失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取成交量排行榜
|
||||
*/
|
||||
@GetMapping("/ranking/volume")
|
||||
@Operation(summary = "获取成交量排行榜")
|
||||
public Result<List<StockData>> getVolumeRanking(
|
||||
@Parameter(description = "排行数量") @RequestParam(defaultValue = "10") Integer limit) {
|
||||
try {
|
||||
List<StockData> rankingList = stockService.getVolumeRanking(limit);
|
||||
return Result.success(rankingList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取成交量排行榜失败", e);
|
||||
return Result.error("获取成交量排行榜失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取股票趋势分析
|
||||
*/
|
||||
@GetMapping("/trend/{stockCode}")
|
||||
@Operation(summary = "获取股票趋势分析")
|
||||
public Result<StockTrendVO> getStockTrend(
|
||||
@Parameter(description = "股票代码") @PathVariable String stockCode,
|
||||
@Parameter(description = "分析天数") @RequestParam(defaultValue = "30") Integer days) {
|
||||
try {
|
||||
StockTrendVO trendVO = stockService.getStockTrend(stockCode, days);
|
||||
if (trendVO != null) {
|
||||
return Result.success(trendVO);
|
||||
} else {
|
||||
return Result.error("未找到该股票的趋势数据");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取股票{}趋势分析失败", stockCode, e);
|
||||
return Result.error("获取趋势分析失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取市场综合分析
|
||||
*/
|
||||
@GetMapping("/market-analysis")
|
||||
@Operation(summary = "获取市场综合分析")
|
||||
public Result<StockAnalysisVO> getMarketAnalysis() {
|
||||
try {
|
||||
StockAnalysisVO analysisVO = stockService.getMarketAnalysis();
|
||||
if (analysisVO != null) {
|
||||
return Result.success(analysisVO);
|
||||
} else {
|
||||
return Result.error("暂无市场分析数据");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取市场分析失败", e);
|
||||
return Result.error("获取市场分析失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取股票预测数据
|
||||
*/
|
||||
@GetMapping("/prediction/{stockCode}")
|
||||
@Operation(summary = "获取股票预测数据")
|
||||
public Result<List<StockData>> getStockPrediction(
|
||||
@Parameter(description = "股票代码") @PathVariable String stockCode,
|
||||
@Parameter(description = "预测天数") @RequestParam(defaultValue = "7") Integer days) {
|
||||
try {
|
||||
List<StockData> predictionList = stockService.getStockPrediction(stockCode, days);
|
||||
return Result.success(predictionList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取股票{}预测数据失败", stockCode, e);
|
||||
return Result.error("获取预测数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索股票
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
@Operation(summary = "搜索股票")
|
||||
public Result<List<StockData>> searchStocks(
|
||||
@Parameter(description = "搜索关键词") @RequestParam String keyword) {
|
||||
try {
|
||||
if (keyword == null || keyword.trim().isEmpty()) {
|
||||
return Result.error("搜索关键词不能为空");
|
||||
}
|
||||
List<StockData> searchResults = stockService.searchStocks(keyword.trim());
|
||||
return Result.success(searchResults);
|
||||
} catch (Exception e) {
|
||||
log.error("搜索股票失败,关键词: {}", keyword, e);
|
||||
return Result.error("搜索失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存股票数据
|
||||
*/
|
||||
@PostMapping("/save")
|
||||
@Operation(summary = "保存股票数据")
|
||||
public Result<StockData> saveStockData(@RequestBody StockData stockData) {
|
||||
try {
|
||||
StockData savedData = stockService.saveStockData(stockData);
|
||||
if (savedData != null) {
|
||||
return Result.success(savedData);
|
||||
} else {
|
||||
return Result.error("保存股票数据失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("保存股票数据失败", e);
|
||||
return Result.error("保存股票数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存股票数据
|
||||
*/
|
||||
@PostMapping("/batch-save")
|
||||
@Operation(summary = "批量保存股票数据")
|
||||
public Result<Integer> batchSaveStockData(@RequestBody List<StockData> stockDataList) {
|
||||
try {
|
||||
if (stockDataList == null || stockDataList.isEmpty()) {
|
||||
return Result.error("股票数据列表不能为空");
|
||||
}
|
||||
Integer count = stockService.batchSaveStockData(stockDataList);
|
||||
return Result.success(count);
|
||||
} catch (Exception e) {
|
||||
log.error("批量保存股票数据失败", e);
|
||||
return Result.error("批量保存失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.agricultural.stock.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 行业分析实体类
|
||||
*/
|
||||
@Data
|
||||
@TableName("industry_analysis")
|
||||
public class IndustryAnalysis {
|
||||
|
||||
/**
|
||||
* 行业名称
|
||||
*/
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 股票数量
|
||||
*/
|
||||
private Long stockCount;
|
||||
|
||||
/**
|
||||
* 平均涨跌幅
|
||||
*/
|
||||
private Double avgChangePercent;
|
||||
|
||||
/**
|
||||
* 总市值
|
||||
*/
|
||||
private Double totalMarketCap;
|
||||
|
||||
/**
|
||||
* 总成交量
|
||||
*/
|
||||
private Double totalVolume;
|
||||
|
||||
/**
|
||||
* 分析日期
|
||||
*/
|
||||
private LocalDate analysisDate;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.agricultural.stock.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 市场分析实体类
|
||||
*/
|
||||
@Data
|
||||
@TableName("market_analysis")
|
||||
public class MarketAnalysis {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 分析日期
|
||||
*/
|
||||
private LocalDate analysisDate;
|
||||
|
||||
/**
|
||||
* 上涨股票数
|
||||
*/
|
||||
private Integer upCount;
|
||||
|
||||
/**
|
||||
* 下跌股票数
|
||||
*/
|
||||
private Integer downCount;
|
||||
|
||||
/**
|
||||
* 平盘股票数
|
||||
*/
|
||||
private Integer flatCount;
|
||||
|
||||
/**
|
||||
* 总股票数
|
||||
*/
|
||||
private Integer totalCount;
|
||||
|
||||
/**
|
||||
* 总市值
|
||||
*/
|
||||
private BigDecimal totalMarketCap;
|
||||
|
||||
/**
|
||||
* 总成交量
|
||||
*/
|
||||
private Long totalVolume;
|
||||
|
||||
/**
|
||||
* 总成交额
|
||||
*/
|
||||
private BigDecimal totalTurnover;
|
||||
|
||||
/**
|
||||
* 平均涨跌幅
|
||||
*/
|
||||
private BigDecimal avgChangePercent;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.agricultural.stock.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 市场趋势实体类
|
||||
*/
|
||||
@Data
|
||||
@TableName("market_trends")
|
||||
public class MarketTrends {
|
||||
|
||||
/**
|
||||
* 交易日期
|
||||
*/
|
||||
private LocalDateTime tradeDate;
|
||||
|
||||
/**
|
||||
* 平均价格
|
||||
*/
|
||||
private Double avgPrice;
|
||||
|
||||
/**
|
||||
* 平均涨跌幅
|
||||
*/
|
||||
private Double avgChangePercent;
|
||||
|
||||
/**
|
||||
* 总成交量
|
||||
*/
|
||||
private Double totalVolume;
|
||||
|
||||
/**
|
||||
* 总成交额
|
||||
*/
|
||||
private Double totalTurnover;
|
||||
|
||||
/**
|
||||
* 股票数量
|
||||
*/
|
||||
private Long stockCount;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.agricultural.stock.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 股票数据实体类
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Entity
|
||||
@Table(name = "stock_data")
|
||||
@TableName("stock_data")
|
||||
public class StockData {
|
||||
|
||||
@Id
|
||||
@TableId(type = IdType.AUTO)
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 股票代码
|
||||
*/
|
||||
@Column(name = "stock_code", nullable = false, length = 10)
|
||||
private String stockCode;
|
||||
|
||||
/**
|
||||
* 股票名称
|
||||
*/
|
||||
@Column(name = "stock_name", nullable = false, length = 50)
|
||||
private String stockName;
|
||||
|
||||
/**
|
||||
* 开盘价
|
||||
*/
|
||||
@Column(name = "open_price", precision = 10, scale = 2)
|
||||
private BigDecimal openPrice;
|
||||
|
||||
/**
|
||||
* 收盘价
|
||||
*/
|
||||
@Column(name = "close_price", precision = 10, scale = 2)
|
||||
private BigDecimal closePrice;
|
||||
|
||||
/**
|
||||
* 最高价
|
||||
*/
|
||||
@Column(name = "high_price", precision = 10, scale = 2)
|
||||
private BigDecimal highPrice;
|
||||
|
||||
/**
|
||||
* 最低价
|
||||
*/
|
||||
@Column(name = "low_price", precision = 10, scale = 2)
|
||||
private BigDecimal lowPrice;
|
||||
|
||||
/**
|
||||
* 成交量
|
||||
*/
|
||||
@Column(name = "volume", nullable = false)
|
||||
private Long volume;
|
||||
|
||||
/**
|
||||
* 成交额
|
||||
*/
|
||||
@Column(name = "turnover", precision = 15, scale = 2)
|
||||
private BigDecimal turnover;
|
||||
|
||||
/**
|
||||
* 涨跌幅
|
||||
*/
|
||||
@Column(name = "change_percent", precision = 5, scale = 2)
|
||||
private BigDecimal changePercent;
|
||||
|
||||
/**
|
||||
* 涨跌额
|
||||
*/
|
||||
@Column(name = "change_amount", precision = 10, scale = 2)
|
||||
private BigDecimal changeAmount;
|
||||
|
||||
/**
|
||||
* 总股本
|
||||
*/
|
||||
@Column(name = "total_shares")
|
||||
private Long totalShares;
|
||||
|
||||
/**
|
||||
* 流通股本
|
||||
*/
|
||||
@Column(name = "float_shares")
|
||||
private Long floatShares;
|
||||
|
||||
/**
|
||||
* 总市值
|
||||
*/
|
||||
@Column(name = "market_cap", precision = 15, scale = 2)
|
||||
private BigDecimal marketCap;
|
||||
|
||||
/**
|
||||
* 流通市值
|
||||
*/
|
||||
@Column(name = "float_market_cap", precision = 15, scale = 2)
|
||||
private BigDecimal floatMarketCap;
|
||||
|
||||
/**
|
||||
* 市盈率
|
||||
*/
|
||||
@Column(name = "pe_ratio", precision = 8, scale = 2)
|
||||
private BigDecimal peRatio;
|
||||
|
||||
/**
|
||||
* 市净率
|
||||
*/
|
||||
@Column(name = "pb_ratio", precision = 8, scale = 2)
|
||||
private BigDecimal pbRatio;
|
||||
|
||||
/**
|
||||
* 交易日期
|
||||
*/
|
||||
@Column(name = "trade_date", nullable = false)
|
||||
private LocalDateTime tradeDate;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@Column(name = "create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 是否删除
|
||||
*/
|
||||
@Column(name = "deleted")
|
||||
private Integer deleted;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createTime = LocalDateTime.now();
|
||||
updateTime = LocalDateTime.now();
|
||||
deleted = 0;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updateTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.agricultural.stock.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 技术指标实体类
|
||||
*/
|
||||
@Data
|
||||
@TableName("stock_technical_indicators")
|
||||
public class TechnicalIndicator {
|
||||
|
||||
/**
|
||||
* 股票代码
|
||||
*/
|
||||
private String stockCode;
|
||||
|
||||
/**
|
||||
* 股票名称
|
||||
*/
|
||||
private String stockName;
|
||||
|
||||
/**
|
||||
* 交易日期
|
||||
*/
|
||||
private LocalDateTime tradeDate;
|
||||
|
||||
/**
|
||||
* 收盘价
|
||||
*/
|
||||
private Double closePrice;
|
||||
|
||||
/**
|
||||
* 5日移动平均线
|
||||
*/
|
||||
private Double ma5;
|
||||
|
||||
/**
|
||||
* 10日移动平均线
|
||||
*/
|
||||
private Double ma10;
|
||||
|
||||
/**
|
||||
* 20日移动平均线
|
||||
*/
|
||||
private Double ma20;
|
||||
|
||||
/**
|
||||
* 30日移动平均线
|
||||
*/
|
||||
private Double ma30;
|
||||
|
||||
/**
|
||||
* RSI相对强弱指标
|
||||
*/
|
||||
private Double rsi;
|
||||
|
||||
/**
|
||||
* MACD DIF值
|
||||
*/
|
||||
private Double macdDif;
|
||||
|
||||
/**
|
||||
* MACD DEA值
|
||||
*/
|
||||
private Double macdDea;
|
||||
|
||||
/**
|
||||
* 布林带上轨
|
||||
*/
|
||||
private Double bbUpper;
|
||||
|
||||
/**
|
||||
* 布林带中轨
|
||||
*/
|
||||
private Double bbMiddle;
|
||||
|
||||
/**
|
||||
* 布林带下轨
|
||||
*/
|
||||
private Double bbLower;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.agricultural.stock.mapper;
|
||||
|
||||
import com.agricultural.stock.entity.MarketAnalysis;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 市场分析数据Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface MarketAnalysisMapper extends BaseMapper<MarketAnalysis> {
|
||||
|
||||
/**
|
||||
* 获取最新的市场分析数据
|
||||
*/
|
||||
@Select("SELECT * FROM market_analysis ORDER BY analysis_date DESC LIMIT 1")
|
||||
MarketAnalysis getLatestMarketAnalysis();
|
||||
|
||||
/**
|
||||
* 获取指定日期范围的市场分析数据
|
||||
*/
|
||||
@Select("SELECT * FROM market_analysis WHERE analysis_date >= #{startDate} AND analysis_date <= #{endDate} ORDER BY analysis_date DESC")
|
||||
List<MarketAnalysis> getMarketAnalysisByDateRange(LocalDate startDate, LocalDate endDate);
|
||||
|
||||
/**
|
||||
* 获取最近N天的市场分析数据
|
||||
*/
|
||||
@Select("SELECT * FROM market_analysis ORDER BY analysis_date DESC LIMIT #{days}")
|
||||
List<MarketAnalysis> getRecentMarketAnalysis(int days);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.agricultural.stock.mapper;
|
||||
|
||||
import com.agricultural.stock.entity.StockData;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 股票数据Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface StockDataMapper extends BaseMapper<StockData> {
|
||||
|
||||
/**
|
||||
* 获取最新交易日的所有股票数据
|
||||
*/
|
||||
@Select("SELECT * FROM stock_data WHERE DATE(trade_date) = (SELECT MAX(DATE(trade_date)) FROM stock_data)")
|
||||
List<StockData> getLatestStockData();
|
||||
|
||||
/**
|
||||
* 获取指定股票代码的历史数据
|
||||
*/
|
||||
@Select("SELECT * FROM stock_data WHERE stock_code = #{stockCode} ORDER BY trade_date DESC LIMIT #{limit}")
|
||||
List<StockData> getStockHistoryData(String stockCode, int limit);
|
||||
|
||||
/**
|
||||
* 获取涨幅榜前N名
|
||||
*/
|
||||
@Select("SELECT * FROM stock_data WHERE DATE(trade_date) = (SELECT MAX(DATE(trade_date)) FROM stock_data) ORDER BY change_percent DESC LIMIT #{limit}")
|
||||
List<StockData> getTopGainers(int limit);
|
||||
|
||||
/**
|
||||
* 获取跌幅榜前N名
|
||||
*/
|
||||
@Select("SELECT * FROM stock_data WHERE DATE(trade_date) = (SELECT MAX(DATE(trade_date)) FROM stock_data) ORDER BY change_percent ASC LIMIT #{limit}")
|
||||
List<StockData> getTopLosers(int limit);
|
||||
|
||||
/**
|
||||
* 获取成交量榜前N名
|
||||
*/
|
||||
@Select("SELECT * FROM stock_data WHERE DATE(trade_date) = (SELECT MAX(DATE(trade_date)) FROM stock_data) ORDER BY volume DESC LIMIT #{limit}")
|
||||
List<StockData> getTopVolume(int limit);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.agricultural.stock.mapper;
|
||||
|
||||
import com.agricultural.stock.entity.TechnicalIndicator;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 技术指标数据Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface TechnicalIndicatorMapper extends BaseMapper<TechnicalIndicator> {
|
||||
|
||||
/**
|
||||
* 获取指定股票的最新技术指标
|
||||
*/
|
||||
@Select("SELECT * FROM stock_technical_indicators WHERE stock_code = #{stockCode} ORDER BY trade_date DESC LIMIT 1")
|
||||
TechnicalIndicator getLatestTechnicalIndicator(String stockCode);
|
||||
|
||||
/**
|
||||
* 获取指定股票的历史技术指标
|
||||
*/
|
||||
@Select("SELECT * FROM stock_technical_indicators WHERE stock_code = #{stockCode} ORDER BY trade_date DESC LIMIT #{limit}")
|
||||
List<TechnicalIndicator> getStockTechnicalHistory(String stockCode, int limit);
|
||||
|
||||
/**
|
||||
* 获取最新交易日所有股票的技术指标
|
||||
*/
|
||||
@Select("SELECT * FROM stock_technical_indicators WHERE trade_date = (SELECT MAX(trade_date) FROM stock_technical_indicators)")
|
||||
List<TechnicalIndicator> getLatestAllTechnicalIndicators();
|
||||
|
||||
/**
|
||||
* 获取指定日期范围的技术指标数据
|
||||
*/
|
||||
@Select("SELECT * FROM stock_technical_indicators WHERE stock_code = #{stockCode} AND trade_date >= #{startDate} AND trade_date <= #{endDate} ORDER BY trade_date DESC")
|
||||
List<TechnicalIndicator> getTechnicalIndicatorsByDateRange(String stockCode, LocalDate startDate, LocalDate endDate);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.agricultural.stock.service;
|
||||
|
||||
import com.agricultural.stock.entity.MarketAnalysis;
|
||||
import com.agricultural.stock.mapper.MarketAnalysisMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 市场分析服务类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MarketAnalysisService {
|
||||
|
||||
@Autowired
|
||||
private MarketAnalysisMapper marketAnalysisMapper;
|
||||
|
||||
/**
|
||||
* 获取最新的市场分析数据
|
||||
*/
|
||||
public MarketAnalysis getLatestMarketAnalysis() {
|
||||
try {
|
||||
return marketAnalysisMapper.getLatestMarketAnalysis();
|
||||
} catch (Exception e) {
|
||||
log.error("获取最新市场分析数据失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定日期范围的市场分析数据
|
||||
*/
|
||||
public List<MarketAnalysis> getMarketAnalysisByDateRange(LocalDate startDate, LocalDate endDate) {
|
||||
try {
|
||||
return marketAnalysisMapper.getMarketAnalysisByDateRange(startDate, endDate);
|
||||
} catch (Exception e) {
|
||||
log.error("获取指定日期范围的市场分析数据失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近N天的市场分析数据
|
||||
*/
|
||||
public List<MarketAnalysis> getRecentMarketAnalysis(int days) {
|
||||
try {
|
||||
return marketAnalysisMapper.getRecentMarketAnalysis(days);
|
||||
} catch (Exception e) {
|
||||
log.error("获取最近{}天的市场分析数据失败", days, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.agricultural.stock.service;
|
||||
|
||||
import com.agricultural.stock.entity.StockData;
|
||||
import com.agricultural.stock.vo.StockAnalysisVO;
|
||||
import com.agricultural.stock.vo.StockTrendVO;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 股票数据服务接口
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
public interface StockService {
|
||||
|
||||
/**
|
||||
* 获取实时股票数据
|
||||
*
|
||||
* @return 股票数据列表
|
||||
*/
|
||||
List<StockData> getRealtimeStockData();
|
||||
|
||||
/**
|
||||
* 根据股票代码获取历史数据
|
||||
*
|
||||
* @param stockCode 股票代码
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return 历史股票数据列表
|
||||
*/
|
||||
List<StockData> getHistoryData(String stockCode, LocalDateTime startDate, LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* 获取涨幅排行榜
|
||||
*
|
||||
* @param limit 排行数量
|
||||
* @return 涨幅排行榜
|
||||
*/
|
||||
List<StockData> getGrowthRanking(Integer limit);
|
||||
|
||||
/**
|
||||
* 获取市值排行榜
|
||||
*
|
||||
* @param limit 排行数量
|
||||
* @return 市值排行榜
|
||||
*/
|
||||
List<StockData> getMarketCapRanking(Integer limit);
|
||||
|
||||
/**
|
||||
* 获取成交量排行榜
|
||||
*
|
||||
* @param limit 排行数量
|
||||
* @return 成交量排行榜
|
||||
*/
|
||||
List<StockData> getVolumeRanking(Integer limit);
|
||||
|
||||
/**
|
||||
* 获取股票趋势分析
|
||||
*
|
||||
* @param stockCode 股票代码
|
||||
* @param days 分析天数
|
||||
* @return 股票趋势分析结果
|
||||
*/
|
||||
StockTrendVO getStockTrend(String stockCode, Integer days);
|
||||
|
||||
/**
|
||||
* 获取市场综合分析
|
||||
*
|
||||
* @return 市场分析结果
|
||||
*/
|
||||
StockAnalysisVO getMarketAnalysis();
|
||||
|
||||
/**
|
||||
* 获取股票预测数据
|
||||
*
|
||||
* @param stockCode 股票代码
|
||||
* @param days 预测天数
|
||||
* @return 预测数据列表
|
||||
*/
|
||||
List<StockData> getStockPrediction(String stockCode, Integer days);
|
||||
|
||||
/**
|
||||
* 搜索股票
|
||||
*
|
||||
* @param keyword 搜索关键词
|
||||
* @return 搜索结果
|
||||
*/
|
||||
List<StockData> searchStocks(String keyword);
|
||||
|
||||
/**
|
||||
* 保存股票数据
|
||||
*
|
||||
* @param stockData 股票数据
|
||||
* @return 保存结果
|
||||
*/
|
||||
StockData saveStockData(StockData stockData);
|
||||
|
||||
/**
|
||||
* 批量保存股票数据
|
||||
*
|
||||
* @param stockDataList 股票数据列表
|
||||
* @return 保存数量
|
||||
*/
|
||||
Integer batchSaveStockData(List<StockData> stockDataList);
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package com.agricultural.stock.service.impl;
|
||||
|
||||
import com.agricultural.stock.entity.StockData;
|
||||
import com.agricultural.stock.mapper.StockDataMapper;
|
||||
import com.agricultural.stock.service.StockService;
|
||||
import com.agricultural.stock.vo.StockAnalysisVO;
|
||||
import com.agricultural.stock.vo.StockTrendVO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 股票数据服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class StockServiceImpl implements StockService {
|
||||
|
||||
@Autowired
|
||||
private StockDataMapper stockDataMapper;
|
||||
|
||||
@Override
|
||||
public List<StockData> getRealtimeStockData() {
|
||||
try {
|
||||
return stockDataMapper.getLatestStockData();
|
||||
} catch (Exception e) {
|
||||
log.error("获取实时股票数据失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StockData> getHistoryData(String stockCode, LocalDateTime startDate, LocalDateTime endDate) {
|
||||
try {
|
||||
// 这里需要在Mapper中添加按日期范围查询的方法
|
||||
return stockDataMapper.getStockHistoryData(stockCode, 30); // 临时返回最近30条
|
||||
} catch (Exception e) {
|
||||
log.error("获取股票{}历史数据失败", stockCode, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StockData> getGrowthRanking(Integer limit) {
|
||||
try {
|
||||
return stockDataMapper.getTopGainers(limit != null ? limit : 10);
|
||||
} catch (Exception e) {
|
||||
log.error("获取涨幅排行榜失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StockData> getMarketCapRanking(Integer limit) {
|
||||
try {
|
||||
// 这里需要在Mapper中添加按市值排序的方法,暂时返回最新数据
|
||||
List<StockData> latestData = stockDataMapper.getLatestStockData();
|
||||
return latestData.stream()
|
||||
.sorted((a, b) -> b.getMarketCap().compareTo(a.getMarketCap()))
|
||||
.limit(limit != null ? limit : 10)
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
log.error("获取市值排行榜失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StockData> getVolumeRanking(Integer limit) {
|
||||
try {
|
||||
return stockDataMapper.getTopVolume(limit != null ? limit : 10);
|
||||
} catch (Exception e) {
|
||||
log.error("获取成交量排行榜失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StockTrendVO getStockTrend(String stockCode, Integer days) {
|
||||
try {
|
||||
List<StockData> historyData = stockDataMapper.getStockHistoryData(stockCode, days != null ? days : 30);
|
||||
if (historyData.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StockTrendVO trendVO = new StockTrendVO();
|
||||
StockData latestData = historyData.get(0);
|
||||
|
||||
// 基本信息
|
||||
trendVO.setStockCode(stockCode);
|
||||
trendVO.setStockName(latestData.getStockName());
|
||||
trendVO.setDays(days);
|
||||
trendVO.setCurrentPrice(latestData.getClosePrice());
|
||||
|
||||
// 计算统计数据
|
||||
BigDecimal maxPrice = historyData.stream()
|
||||
.map(StockData::getHighPrice)
|
||||
.max(BigDecimal::compareTo)
|
||||
.orElse(BigDecimal.ZERO);
|
||||
BigDecimal minPrice = historyData.stream()
|
||||
.map(StockData::getLowPrice)
|
||||
.min(BigDecimal::compareTo)
|
||||
.orElse(BigDecimal.ZERO);
|
||||
BigDecimal avgPrice = historyData.stream()
|
||||
.map(StockData::getClosePrice)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.divide(BigDecimal.valueOf(historyData.size()), 2, RoundingMode.HALF_UP);
|
||||
|
||||
trendVO.setHighestPrice(maxPrice);
|
||||
trendVO.setLowestPrice(minPrice);
|
||||
trendVO.setAveragePrice(avgPrice);
|
||||
|
||||
// 计算总涨跌幅
|
||||
if (historyData.size() > 1) {
|
||||
StockData oldestData = historyData.get(historyData.size() - 1);
|
||||
BigDecimal totalChange = latestData.getClosePrice()
|
||||
.subtract(oldestData.getClosePrice())
|
||||
.divide(oldestData.getClosePrice(), 4, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
trendVO.setTotalChangePercent(totalChange);
|
||||
}
|
||||
|
||||
// 计算平均涨跌幅
|
||||
BigDecimal avgChange = historyData.stream()
|
||||
.map(StockData::getChangePercent)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.divide(BigDecimal.valueOf(historyData.size()), 2, RoundingMode.HALF_UP);
|
||||
trendVO.setAvgChangePercent(avgChange);
|
||||
|
||||
// 计算成交量统计
|
||||
Long totalVolume = historyData.stream()
|
||||
.map(StockData::getVolume)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(0L, Long::sum);
|
||||
trendVO.setTotalVolume(totalVolume);
|
||||
trendVO.setAvgVolume(totalVolume / historyData.size());
|
||||
|
||||
// 构建价格历史数据
|
||||
List<StockTrendVO.PricePoint> priceHistory = historyData.stream()
|
||||
.map(data -> {
|
||||
StockTrendVO.PricePoint point = new StockTrendVO.PricePoint();
|
||||
point.setTradeDate(data.getTradeDate().toLocalDate());
|
||||
point.setOpenPrice(data.getOpenPrice());
|
||||
point.setClosePrice(data.getClosePrice());
|
||||
point.setHighPrice(data.getHighPrice());
|
||||
point.setLowPrice(data.getLowPrice());
|
||||
point.setVolume(data.getVolume());
|
||||
point.setChangePercent(data.getChangePercent());
|
||||
return point;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
trendVO.setPriceHistory(priceHistory);
|
||||
|
||||
// 判断趋势方向
|
||||
if (avgChange.compareTo(BigDecimal.valueOf(1)) > 0) {
|
||||
trendVO.setTrendDirection("UP");
|
||||
trendVO.setTrendStrength(avgChange.min(BigDecimal.valueOf(100)));
|
||||
} else if (avgChange.compareTo(BigDecimal.valueOf(-1)) < 0) {
|
||||
trendVO.setTrendDirection("DOWN");
|
||||
trendVO.setTrendStrength(avgChange.abs().min(BigDecimal.valueOf(100)));
|
||||
} else {
|
||||
trendVO.setTrendDirection("FLAT");
|
||||
trendVO.setTrendStrength(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
return trendVO;
|
||||
} catch (Exception e) {
|
||||
log.error("获取股票{}趋势分析失败", stockCode, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StockAnalysisVO getMarketAnalysis() {
|
||||
try {
|
||||
List<StockData> latestData = stockDataMapper.getLatestStockData();
|
||||
if (latestData.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StockAnalysisVO analysisVO = new StockAnalysisVO();
|
||||
|
||||
// 市场总览
|
||||
StockAnalysisVO.MarketOverview overview = new StockAnalysisVO.MarketOverview();
|
||||
overview.setTotalStocks(latestData.size());
|
||||
|
||||
long upCount = latestData.stream().filter(data ->
|
||||
data.getChangePercent() != null && data.getChangePercent().compareTo(BigDecimal.ZERO) > 0).count();
|
||||
long downCount = latestData.stream().filter(data ->
|
||||
data.getChangePercent() != null && data.getChangePercent().compareTo(BigDecimal.ZERO) < 0).count();
|
||||
long flatCount = latestData.size() - upCount - downCount;
|
||||
|
||||
overview.setUpCount((int) upCount);
|
||||
overview.setDownCount((int) downCount);
|
||||
overview.setFlatCount((int) flatCount);
|
||||
|
||||
// 计算总成交量、总成交额、总市值
|
||||
Long totalVolume = latestData.stream()
|
||||
.map(StockData::getVolume)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(0L, Long::sum);
|
||||
BigDecimal totalTurnover = latestData.stream()
|
||||
.map(StockData::getTurnover)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal totalMarketCap = latestData.stream()
|
||||
.map(StockData::getMarketCap)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
overview.setTotalVolume(totalVolume);
|
||||
overview.setTotalTurnover(totalTurnover);
|
||||
overview.setTotalMarketCap(totalMarketCap);
|
||||
|
||||
// 计算平均涨跌幅
|
||||
BigDecimal avgChange = latestData.stream()
|
||||
.map(StockData::getChangePercent)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.divide(BigDecimal.valueOf(latestData.size()), 2, RoundingMode.HALF_UP);
|
||||
overview.setAvgChangePercent(avgChange);
|
||||
|
||||
analysisVO.setMarketOverview(overview);
|
||||
|
||||
// 涨幅榜
|
||||
List<StockAnalysisVO.StockRankingItem> topGainers = getTopGainers(latestData, 10);
|
||||
analysisVO.setTopGainers(topGainers);
|
||||
|
||||
// 跌幅榜
|
||||
List<StockAnalysisVO.StockRankingItem> topLosers = getTopLosers(latestData, 10);
|
||||
analysisVO.setTopLosers(topLosers);
|
||||
|
||||
// 成交量榜
|
||||
List<StockAnalysisVO.StockRankingItem> topVolume = getTopVolumeStocks(latestData, 10);
|
||||
analysisVO.setTopVolume(topVolume);
|
||||
|
||||
return analysisVO;
|
||||
} catch (Exception e) {
|
||||
log.error("获取市场分析失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StockData> getStockPrediction(String stockCode, Integer days) {
|
||||
// 这里应该调用预测模型,暂时返回空列表
|
||||
log.info("股票预测功能暂未实现,股票代码: {}, 预测天数: {}", stockCode, days);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StockData> searchStocks(String keyword) {
|
||||
try {
|
||||
List<StockData> allStocks = stockDataMapper.getLatestStockData();
|
||||
return allStocks.stream()
|
||||
.filter(stock -> stock.getStockCode().contains(keyword) ||
|
||||
stock.getStockName().contains(keyword))
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
log.error("搜索股票失败,关键词: {}", keyword, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StockData saveStockData(StockData stockData) {
|
||||
try {
|
||||
stockDataMapper.insert(stockData);
|
||||
return stockData;
|
||||
} catch (Exception e) {
|
||||
log.error("保存股票数据失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer batchSaveStockData(List<StockData> stockDataList) {
|
||||
try {
|
||||
int count = 0;
|
||||
for (StockData stockData : stockDataList) {
|
||||
stockDataMapper.insert(stockData);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
log.error("批量保存股票数据失败", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private List<StockAnalysisVO.StockRankingItem> getTopGainers(List<StockData> stockData, int limit) {
|
||||
return stockData.stream()
|
||||
.filter(data -> data.getChangePercent() != null)
|
||||
.sorted((a, b) -> b.getChangePercent().compareTo(a.getChangePercent()))
|
||||
.limit(limit)
|
||||
.map(this::convertToRankingItem)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<StockAnalysisVO.StockRankingItem> getTopLosers(List<StockData> stockData, int limit) {
|
||||
return stockData.stream()
|
||||
.filter(data -> data.getChangePercent() != null)
|
||||
.sorted((a, b) -> a.getChangePercent().compareTo(b.getChangePercent()))
|
||||
.limit(limit)
|
||||
.map(this::convertToRankingItem)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<StockAnalysisVO.StockRankingItem> getTopVolumeStocks(List<StockData> stockData, int limit) {
|
||||
return stockData.stream()
|
||||
.filter(data -> data.getVolume() != null)
|
||||
.sorted((a, b) -> b.getVolume().compareTo(a.getVolume()))
|
||||
.limit(limit)
|
||||
.map(this::convertToRankingItem)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private StockAnalysisVO.StockRankingItem convertToRankingItem(StockData stockData) {
|
||||
StockAnalysisVO.StockRankingItem item = new StockAnalysisVO.StockRankingItem();
|
||||
item.setStockCode(stockData.getStockCode());
|
||||
item.setStockName(stockData.getStockName());
|
||||
item.setCurrentPrice(stockData.getClosePrice());
|
||||
item.setChangePercent(stockData.getChangePercent());
|
||||
item.setChangeAmount(stockData.getChangeAmount());
|
||||
item.setVolume(stockData.getVolume());
|
||||
item.setTurnover(stockData.getTurnover());
|
||||
item.setMarketCap(stockData.getMarketCap());
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.agricultural.stock.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* API统一响应结果封装
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
@Data
|
||||
public class ApiResponse<T> {
|
||||
|
||||
private static final int SUCCESS_CODE = 200;
|
||||
private static final int ERROR_CODE = 500;
|
||||
private static final String SUCCESS_MESSAGE = "success";
|
||||
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
private long timestamp;
|
||||
|
||||
public ApiResponse() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public ApiResponse(int code, String message, T data) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
*/
|
||||
public static <T> ApiResponse<T> success() {
|
||||
return new ApiResponse<>(SUCCESS_CODE, SUCCESS_MESSAGE, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应带数据
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return new ApiResponse<>(SUCCESS_CODE, SUCCESS_MESSAGE, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应带消息和数据
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(String message, T data) {
|
||||
return new ApiResponse<>(SUCCESS_CODE, message, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应
|
||||
*/
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
return new ApiResponse<>(ERROR_CODE, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误响应带错误码
|
||||
*/
|
||||
public static <T> ApiResponse<T> error(int code, String message) {
|
||||
return new ApiResponse<>(code, message, null);
|
||||
}
|
||||
}
|
||||
76
backend/src/main/java/com/agricultural/stock/vo/Result.java
Normal file
76
backend/src/main/java/com/agricultural/stock/vo/Result.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package com.agricultural.stock.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 统一API响应结果类
|
||||
*/
|
||||
@Data
|
||||
public class Result<T> {
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
private int code;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
private long timestamp;
|
||||
|
||||
public Result() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public Result(int code, String message, T data) {
|
||||
this();
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
*/
|
||||
public static <T> Result<T> success(T data) {
|
||||
return new Result<>(200, "成功", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(无数据)
|
||||
*/
|
||||
public static <T> Result<T> success() {
|
||||
return new Result<>(200, "成功", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应
|
||||
*/
|
||||
public static <T> Result<T> error(String message) {
|
||||
return new Result<>(500, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应(自定义状态码)
|
||||
*/
|
||||
public static <T> Result<T> error(int code, String message) {
|
||||
return new Result<>(code, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否成功
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return this.code == 200;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package com.agricultural.stock.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 股票市场分析结果VO
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
@Data
|
||||
public class StockAnalysisVO {
|
||||
|
||||
/**
|
||||
* 市场总览
|
||||
*/
|
||||
private MarketOverview marketOverview;
|
||||
|
||||
/**
|
||||
* 涨幅榜前10
|
||||
*/
|
||||
private List<StockRankingItem> topGainers;
|
||||
|
||||
/**
|
||||
* 跌幅榜前10
|
||||
*/
|
||||
private List<StockRankingItem> topLosers;
|
||||
|
||||
/**
|
||||
* 成交量榜前10
|
||||
*/
|
||||
private List<StockRankingItem> topVolume;
|
||||
|
||||
/**
|
||||
* 行业分析
|
||||
*/
|
||||
private List<IndustryAnalysisItem> industryAnalysis;
|
||||
|
||||
/**
|
||||
* 行业分析
|
||||
*/
|
||||
private List<IndustryAnalysis> industryAnalysisList;
|
||||
|
||||
/**
|
||||
* 热门股票
|
||||
*/
|
||||
private List<HotStock> hotStocks;
|
||||
|
||||
/**
|
||||
* 市场情绪指标
|
||||
*/
|
||||
private MarketSentiment marketSentiment;
|
||||
|
||||
@Data
|
||||
public static class MarketOverview {
|
||||
/**
|
||||
* 总股票数
|
||||
*/
|
||||
private Integer totalStocks;
|
||||
|
||||
/**
|
||||
* 上涨股票数
|
||||
*/
|
||||
private Integer upCount;
|
||||
|
||||
/**
|
||||
* 下跌股票数
|
||||
*/
|
||||
private Integer downCount;
|
||||
|
||||
/**
|
||||
* 平盘股票数
|
||||
*/
|
||||
private Integer flatCount;
|
||||
|
||||
/**
|
||||
* 平均涨跌幅
|
||||
*/
|
||||
private BigDecimal avgChangePercent;
|
||||
|
||||
/**
|
||||
* 总成交量
|
||||
*/
|
||||
private Long totalVolume;
|
||||
|
||||
/**
|
||||
* 总成交额
|
||||
*/
|
||||
private BigDecimal totalTurnover;
|
||||
|
||||
/**
|
||||
* 总市值
|
||||
*/
|
||||
private BigDecimal totalMarketCap;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class StockRankingItem {
|
||||
/**
|
||||
* 股票代码
|
||||
*/
|
||||
private String stockCode;
|
||||
|
||||
/**
|
||||
* 股票名称
|
||||
*/
|
||||
private String stockName;
|
||||
|
||||
/**
|
||||
* 当前价格
|
||||
*/
|
||||
private BigDecimal currentPrice;
|
||||
|
||||
/**
|
||||
* 涨跌幅
|
||||
*/
|
||||
private BigDecimal changePercent;
|
||||
|
||||
/**
|
||||
* 涨跌额
|
||||
*/
|
||||
private BigDecimal changeAmount;
|
||||
|
||||
/**
|
||||
* 成交量
|
||||
*/
|
||||
private Long volume;
|
||||
|
||||
/**
|
||||
* 成交额
|
||||
*/
|
||||
private BigDecimal turnover;
|
||||
|
||||
/**
|
||||
* 市值
|
||||
*/
|
||||
private BigDecimal marketCap;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IndustryAnalysisItem {
|
||||
/**
|
||||
* 行业名称
|
||||
*/
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 股票数量
|
||||
*/
|
||||
private Integer stockCount;
|
||||
|
||||
/**
|
||||
* 平均涨跌幅
|
||||
*/
|
||||
private BigDecimal avgChangePercent;
|
||||
|
||||
/**
|
||||
* 总市值
|
||||
*/
|
||||
private BigDecimal totalMarketCap;
|
||||
|
||||
/**
|
||||
* 总成交量
|
||||
*/
|
||||
private Long totalVolume;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class IndustryAnalysis {
|
||||
/**
|
||||
* 行业名称
|
||||
*/
|
||||
private String industryName;
|
||||
|
||||
/**
|
||||
* 股票数量
|
||||
*/
|
||||
private Integer stockCount;
|
||||
|
||||
/**
|
||||
* 平均涨跌幅
|
||||
*/
|
||||
private BigDecimal avgChangePercent;
|
||||
|
||||
/**
|
||||
* 总市值
|
||||
*/
|
||||
private BigDecimal totalMarketCap;
|
||||
|
||||
/**
|
||||
* 领涨股票
|
||||
*/
|
||||
private String leadingStock;
|
||||
|
||||
/**
|
||||
* 行业排名
|
||||
*/
|
||||
private Integer ranking;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class HotStock {
|
||||
/**
|
||||
* 股票代码
|
||||
*/
|
||||
private String stockCode;
|
||||
|
||||
/**
|
||||
* 股票名称
|
||||
*/
|
||||
private String stockName;
|
||||
|
||||
/**
|
||||
* 涨跌幅
|
||||
*/
|
||||
private BigDecimal changePercent;
|
||||
|
||||
/**
|
||||
* 成交量
|
||||
*/
|
||||
private Long volume;
|
||||
|
||||
/**
|
||||
* 热度评分
|
||||
*/
|
||||
private Integer hotScore;
|
||||
|
||||
/**
|
||||
* 热度原因
|
||||
*/
|
||||
private String hotReason;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MarketSentiment {
|
||||
/**
|
||||
* 市场情绪指数 (0-100)
|
||||
*/
|
||||
private Integer sentimentIndex;
|
||||
|
||||
/**
|
||||
* 恐慌贪婪指数 (0-100)
|
||||
*/
|
||||
private Integer fearGreedIndex;
|
||||
|
||||
/**
|
||||
* 波动率指数
|
||||
*/
|
||||
private BigDecimal volatilityIndex;
|
||||
|
||||
/**
|
||||
* 资金流向
|
||||
*/
|
||||
private String moneyFlow;
|
||||
|
||||
/**
|
||||
* 市场预期
|
||||
*/
|
||||
private String marketExpectation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.agricultural.stock.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 股票趋势分析VO
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
@Data
|
||||
public class StockTrendVO {
|
||||
|
||||
/**
|
||||
* 股票代码
|
||||
*/
|
||||
private String stockCode;
|
||||
|
||||
/**
|
||||
* 股票名称
|
||||
*/
|
||||
private String stockName;
|
||||
|
||||
/**
|
||||
* 分析天数
|
||||
*/
|
||||
private Integer days;
|
||||
|
||||
/**
|
||||
* 当前价格
|
||||
*/
|
||||
private BigDecimal currentPrice;
|
||||
|
||||
/**
|
||||
* 最高价格
|
||||
*/
|
||||
private BigDecimal highestPrice;
|
||||
|
||||
/**
|
||||
* 最低价格
|
||||
*/
|
||||
private BigDecimal lowestPrice;
|
||||
|
||||
/**
|
||||
* 平均价格
|
||||
*/
|
||||
private BigDecimal averagePrice;
|
||||
|
||||
/**
|
||||
* 总涨跌幅
|
||||
*/
|
||||
private BigDecimal totalChangePercent;
|
||||
|
||||
/**
|
||||
* 平均涨跌幅
|
||||
*/
|
||||
private BigDecimal avgChangePercent;
|
||||
|
||||
/**
|
||||
* 波动率
|
||||
*/
|
||||
private BigDecimal volatility;
|
||||
|
||||
/**
|
||||
* 总成交量
|
||||
*/
|
||||
private Long totalVolume;
|
||||
|
||||
/**
|
||||
* 平均成交量
|
||||
*/
|
||||
private Long avgVolume;
|
||||
|
||||
/**
|
||||
* 趋势方向 (UP/DOWN/FLAT)
|
||||
*/
|
||||
private String trendDirection;
|
||||
|
||||
/**
|
||||
* 趋势强度 (0-100)
|
||||
*/
|
||||
private BigDecimal trendStrength;
|
||||
|
||||
/**
|
||||
* 历史价格数据
|
||||
*/
|
||||
private List<PricePoint> priceHistory;
|
||||
|
||||
/**
|
||||
* 技术指标
|
||||
*/
|
||||
private TechnicalIndicators technicalIndicators;
|
||||
|
||||
@Data
|
||||
public static class PricePoint {
|
||||
/**
|
||||
* 交易日期
|
||||
*/
|
||||
private LocalDate tradeDate;
|
||||
|
||||
/**
|
||||
* 开盘价
|
||||
*/
|
||||
private BigDecimal openPrice;
|
||||
|
||||
/**
|
||||
* 收盘价
|
||||
*/
|
||||
private BigDecimal closePrice;
|
||||
|
||||
/**
|
||||
* 最高价
|
||||
*/
|
||||
private BigDecimal highPrice;
|
||||
|
||||
/**
|
||||
* 最低价
|
||||
*/
|
||||
private BigDecimal lowPrice;
|
||||
|
||||
/**
|
||||
* 成交量
|
||||
*/
|
||||
private Long volume;
|
||||
|
||||
/**
|
||||
* 涨跌幅
|
||||
*/
|
||||
private BigDecimal changePercent;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class TechnicalIndicators {
|
||||
/**
|
||||
* 5日移动平均线
|
||||
*/
|
||||
private BigDecimal ma5;
|
||||
|
||||
/**
|
||||
* 10日移动平均线
|
||||
*/
|
||||
private BigDecimal ma10;
|
||||
|
||||
/**
|
||||
* 20日移动平均线
|
||||
*/
|
||||
private BigDecimal ma20;
|
||||
|
||||
/**
|
||||
* 30日移动平均线
|
||||
*/
|
||||
private BigDecimal ma30;
|
||||
|
||||
/**
|
||||
* RSI相对强弱指标
|
||||
*/
|
||||
private BigDecimal rsi;
|
||||
|
||||
/**
|
||||
* MACD DIF值
|
||||
*/
|
||||
private BigDecimal macdDif;
|
||||
|
||||
/**
|
||||
* MACD DEA值
|
||||
*/
|
||||
private BigDecimal macdDea;
|
||||
|
||||
/**
|
||||
* 布林带上轨
|
||||
*/
|
||||
private BigDecimal bbUpper;
|
||||
|
||||
/**
|
||||
* 布林带中轨
|
||||
*/
|
||||
private BigDecimal bbMiddle;
|
||||
|
||||
/**
|
||||
* 布林带下轨
|
||||
*/
|
||||
private BigDecimal bbLower;
|
||||
}
|
||||
}
|
||||
100
backend/src/main/resources/application.yml
Normal file
100
backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,100 @@
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: agricultural-stock-platform-backend
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/agricultural_stock?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
show-sql: false
|
||||
database-platform: org.hibernate.dialect.MySQL8Dialect
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
format_sql: true
|
||||
|
||||
# Redis配置
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
timeout: 6000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 10
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# Kafka配置已移除
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
type-aliases-package: com.agricultural.stock.entity
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
call-setters-on-nulls: true
|
||||
jdbc-type-for-null: 'null'
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: auto
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# SpringDoc OpenAPI 配置
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
enabled: true
|
||||
swagger-ui:
|
||||
path: /swagger-ui
|
||||
enabled: true
|
||||
config-url: /v3/api-docs/swagger-config
|
||||
urls-primary-name: default
|
||||
packages-to-scan: com.agricultural.stock.controller
|
||||
paths-to-match: /api/**
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.agricultural.stock: INFO
|
||||
org.springframework: WARN
|
||||
com.baomidou.mybatisplus: WARN
|
||||
pattern:
|
||||
console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: logs/agricultural-stock-platform.log
|
||||
|
||||
# 自定义配置
|
||||
agricultural:
|
||||
stock:
|
||||
# 数据采集配置
|
||||
crawler:
|
||||
interval: 60000 # 采集间隔(毫秒)
|
||||
batch-size: 100 # 批处理大小
|
||||
# Kafka配置已移除
|
||||
# 缓存配置
|
||||
cache:
|
||||
expire-time: 3600 # 缓存过期时间(秒)
|
||||
20
data-collector/config.ini
Normal file
20
data-collector/config.ini
Normal file
@@ -0,0 +1,20 @@
|
||||
[database]
|
||||
host = localhost
|
||||
port = 3306
|
||||
user = root
|
||||
password = root
|
||||
database = agricultural_stock
|
||||
|
||||
[crawler]
|
||||
interval_minutes = 5
|
||||
batch_size = 100
|
||||
max_retries = 3
|
||||
request_timeout = 10
|
||||
|
||||
[logging]
|
||||
level = INFO
|
||||
format = %(asctime)s - %(levelname)s - %(message)s
|
||||
file = stock_crawler.log
|
||||
|
||||
# Kafka配置已移除
|
||||
# 所有数据直接保存到MySQL数据库
|
||||
9
data-collector/requirements.txt
Normal file
9
data-collector/requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# 核心数据采集所需的基础包
|
||||
requests>=2.31.0
|
||||
pandas>=2.1.0
|
||||
beautifulsoup4>=4.12.2
|
||||
lxml>=4.9.3
|
||||
mysql-connector-python>=8.1.0
|
||||
# kafka-python 已移除
|
||||
schedule>=1.2.0
|
||||
numpy>=1.26.0
|
||||
17
data-collector/stock_crawler.log
Normal file
17
data-collector/stock_crawler.log
Normal file
@@ -0,0 +1,17 @@
|
||||
2025-06-04 18:03:49,612 - INFO - 数据库连接成功
|
||||
2025-06-04 18:03:49,613 - INFO - 股票数据采集器启动
|
||||
2025-06-04 18:03:49,613 - INFO - 定时任务已启动,每 5 分钟采集一次
|
||||
2025-06-04 18:03:49,613 - INFO - 开始执行股票数据采集...image.png
|
||||
2025-06-04 18:03:49,706 - INFO - 成功获取股票 sz300630 数据: 普利退
|
||||
2025-06-04 18:03:50,248 - INFO - 成功获取股票 sh600998 数据: 九州通
|
||||
2025-06-04 18:03:50,791 - INFO - 成功获取股票 sh600371 数据: 万向德农
|
||||
2025-06-04 18:03:51,331 - INFO - 成功获取股票 sz000876 数据: 新 希 望
|
||||
2025-06-04 18:03:51,873 - INFO - 成功获取股票 sz002714 数据: 牧原股份
|
||||
2025-06-04 18:03:52,414 - INFO - 成功获取股票 sh600519 数据: 贵州茅台
|
||||
2025-06-04 18:03:52,956 - INFO - 成功获取股票 sz000858 数据: 五 粮 液
|
||||
2025-06-04 18:03:53,498 - INFO - 成功获取股票 sh600887 数据: 伊利股份
|
||||
2025-06-04 18:03:54,038 - INFO - 成功获取股票 sz002304 数据: 洋河股份
|
||||
2025-06-04 18:03:54,580 - INFO - 成功获取股票 sh600036 数据: 招商银行
|
||||
2025-06-04 18:03:55,081 - INFO - 本次采集完成,共获取 10 只股票数据
|
||||
2025-06-04 18:03:55,106 - INFO - 成功保存 10 条股票数据到数据库
|
||||
2025-06-04 18:03:55,106 - INFO - 数据采集完成,共处理 10 只股票
|
||||
301
data-collector/stock_crawler.py
Normal file
301
data-collector/stock_crawler.py
Normal file
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
农业股票数据采集器
|
||||
根据项目需求从腾讯行情接口采集农业类上市公司数据
|
||||
"""
|
||||
|
||||
import requests
|
||||
import pandas as pd
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
import schedule
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
import mysql.connector
|
||||
# Kafka导入已移除
|
||||
import configparser
|
||||
|
||||
# 日志配置
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('stock_crawler.log', encoding='utf-8'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class StockCrawler:
|
||||
"""股票数据爬虫类"""
|
||||
|
||||
def __init__(self, config_file='config.ini'):
|
||||
"""初始化爬虫"""
|
||||
self.config = self.load_config(config_file)
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
})
|
||||
|
||||
# 农业类股票代码列表(示例)
|
||||
self.agricultural_stocks = [
|
||||
'sz300630', # 普利制药
|
||||
'sh600998', # 九州通
|
||||
'sh600371', # 万向钱潮
|
||||
'sz000876', # 新希望
|
||||
'sz002714', # 牧原股份
|
||||
'sh600519', # 贵州茅台
|
||||
'sz000858', # 五粮液
|
||||
'sh600887', # 伊利股份
|
||||
'sz002304', # 洋河股份
|
||||
'sh600036', # 招商银行
|
||||
]
|
||||
|
||||
# 初始化数据库连接
|
||||
self.init_db_connection()
|
||||
|
||||
# Kafka生产者初始化已移除
|
||||
|
||||
def load_config(self, config_file: str) -> configparser.ConfigParser:
|
||||
"""加载配置文件"""
|
||||
config = configparser.ConfigParser()
|
||||
try:
|
||||
config.read(config_file, encoding='utf-8')
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"配置文件 {config_file} 不存在,使用默认配置")
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(f"配置文件编码错误,使用默认配置")
|
||||
return config
|
||||
|
||||
def init_db_connection(self):
|
||||
"""初始化数据库连接"""
|
||||
try:
|
||||
self.db_connection = mysql.connector.connect(
|
||||
host=self.config.get('database', 'host', fallback='localhost'),
|
||||
port=self.config.getint('database', 'port', fallback=3306),
|
||||
user=self.config.get('database', 'user', fallback='root'),
|
||||
password=self.config.get('database', 'password', fallback='123456'),
|
||||
database=self.config.get('database', 'database', fallback='agricultural_stock'),
|
||||
charset='utf8mb4'
|
||||
)
|
||||
logger.info("数据库连接成功")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库连接失败: {e}")
|
||||
self.db_connection = None
|
||||
|
||||
# Kafka生产者初始化方法已移除
|
||||
|
||||
def fetch_stock_data(self, stock_code: str) -> Optional[Dict]:
|
||||
"""
|
||||
从腾讯行情接口获取单个股票数据
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码,格式如 'sz300630' 或 'sh600998'
|
||||
|
||||
Returns:
|
||||
股票数据字典或None
|
||||
"""
|
||||
url = f"http://qt.gtimg.cn/q={stock_code}"
|
||||
|
||||
try:
|
||||
response = self.session.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
# 解析响应数据
|
||||
content = response.text.strip()
|
||||
if not content or 'v_' not in content:
|
||||
logger.warning(f"股票 {stock_code} 无数据返回")
|
||||
return None
|
||||
|
||||
# 提取数据部分
|
||||
data_part = content.split('="')[1].split('";')[0]
|
||||
fields = data_part.split('~')
|
||||
|
||||
if len(fields) < 50:
|
||||
logger.warning(f"股票 {stock_code} 数据字段不完整")
|
||||
return None
|
||||
|
||||
# 构造股票数据字典
|
||||
stock_data = {
|
||||
'stock_code': stock_code,
|
||||
'stock_name': fields[1],
|
||||
'current_price': float(fields[3]) if fields[3] else 0.0,
|
||||
'yesterday_close': float(fields[4]) if fields[4] else 0.0,
|
||||
'open_price': float(fields[5]) if fields[5] else 0.0,
|
||||
'volume': int(fields[6]) if fields[6] else 0,
|
||||
'outer_volume': int(fields[7]) if fields[7] else 0,
|
||||
'inner_volume': int(fields[8]) if fields[8] else 0,
|
||||
'buy1_price': float(fields[9]) if fields[9] else 0.0,
|
||||
'buy1_volume': int(fields[10]) if fields[10] else 0,
|
||||
'buy2_price': float(fields[11]) if fields[11] else 0.0,
|
||||
'buy2_volume': int(fields[12]) if fields[12] else 0,
|
||||
'buy3_price': float(fields[13]) if fields[13] else 0.0,
|
||||
'buy3_volume': int(fields[14]) if fields[14] else 0,
|
||||
'buy4_price': float(fields[15]) if fields[15] else 0.0,
|
||||
'buy4_volume': int(fields[16]) if fields[16] else 0,
|
||||
'buy5_price': float(fields[17]) if fields[17] else 0.0,
|
||||
'buy5_volume': int(fields[18]) if fields[18] else 0,
|
||||
'sell1_price': float(fields[19]) if fields[19] else 0.0,
|
||||
'sell1_volume': int(fields[20]) if fields[20] else 0,
|
||||
'sell2_price': float(fields[21]) if fields[21] else 0.0,
|
||||
'sell2_volume': int(fields[22]) if fields[22] else 0,
|
||||
'sell3_price': float(fields[23]) if fields[23] else 0.0,
|
||||
'sell3_volume': int(fields[24]) if fields[24] else 0,
|
||||
'sell4_price': float(fields[25]) if fields[25] else 0.0,
|
||||
'sell4_volume': int(fields[26]) if fields[26] else 0,
|
||||
'sell5_price': float(fields[27]) if fields[27] else 0.0,
|
||||
'sell5_volume': int(fields[28]) if fields[28] else 0,
|
||||
'latest_deals': fields[29],
|
||||
'trade_time': fields[30],
|
||||
'change_amount': float(fields[31]) if fields[31] else 0.0,
|
||||
'change_percent': float(fields[32]) if fields[32] else 0.0,
|
||||
'high_price': float(fields[33]) if fields[33] else 0.0,
|
||||
'low_price': float(fields[34]) if fields[34] else 0.0,
|
||||
'price_volume_ratio': fields[35],
|
||||
'volume_ratio': fields[36],
|
||||
'turnover_rate': float(fields[37]) if fields[37] else 0.0,
|
||||
'pe_ratio': float(fields[38]) if fields[38] else 0.0,
|
||||
'pb_ratio': float(fields[46]) if len(fields) > 46 and fields[46] else 0.0,
|
||||
'market_cap': float(fields[44]) if len(fields) > 44 and fields[44] else 0.0,
|
||||
'float_market_cap': float(fields[45]) if len(fields) > 45 and fields[45] else 0.0,
|
||||
'crawl_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
# 计算派生字段
|
||||
if stock_data['current_price'] > 0 and stock_data['yesterday_close'] > 0:
|
||||
stock_data['change_amount'] = stock_data['current_price'] - stock_data['yesterday_close']
|
||||
stock_data['change_percent'] = (stock_data['change_amount'] / stock_data['yesterday_close']) * 100
|
||||
|
||||
# 计算成交额
|
||||
stock_data['turnover'] = stock_data['volume'] * stock_data['current_price']
|
||||
|
||||
logger.info(f"成功获取股票 {stock_code} 数据: {stock_data['stock_name']}")
|
||||
return stock_data
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"请求股票 {stock_code} 数据失败: {e}")
|
||||
return None
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"解析股票 {stock_code} 数据失败: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取股票 {stock_code} 数据时出现未知错误: {e}")
|
||||
return None
|
||||
|
||||
def fetch_all_stocks(self) -> List[Dict]:
|
||||
"""获取所有农业股票数据"""
|
||||
all_data = []
|
||||
|
||||
for stock_code in self.agricultural_stocks:
|
||||
try:
|
||||
stock_data = self.fetch_stock_data(stock_code)
|
||||
if stock_data:
|
||||
all_data.append(stock_data)
|
||||
|
||||
# 避免请求过于频繁
|
||||
time.sleep(0.5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理股票 {stock_code} 时出错: {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"本次采集完成,共获取 {len(all_data)} 只股票数据")
|
||||
return all_data
|
||||
|
||||
def save_to_database(self, stock_data_list: List[Dict]):
|
||||
"""保存数据到MySQL数据库"""
|
||||
if not self.db_connection or not stock_data_list:
|
||||
return
|
||||
|
||||
try:
|
||||
cursor = self.db_connection.cursor()
|
||||
|
||||
# 构建插入SQL
|
||||
insert_sql = """
|
||||
INSERT INTO stock_data (
|
||||
stock_code, stock_name, open_price, close_price, high_price, low_price,
|
||||
volume, turnover, change_percent, change_amount, pe_ratio, pb_ratio,
|
||||
market_cap, float_market_cap, trade_date, create_time
|
||||
) VALUES (
|
||||
%(stock_code)s, %(stock_name)s, %(open_price)s, %(current_price)s,
|
||||
%(high_price)s, %(low_price)s, %(volume)s, %(turnover)s,
|
||||
%(change_percent)s, %(change_amount)s, %(pe_ratio)s, %(pb_ratio)s,
|
||||
%(market_cap)s, %(float_market_cap)s, NOW(), NOW()
|
||||
)
|
||||
"""
|
||||
|
||||
# 批量插入数据
|
||||
cursor.executemany(insert_sql, stock_data_list)
|
||||
self.db_connection.commit()
|
||||
|
||||
logger.info(f"成功保存 {len(stock_data_list)} 条股票数据到数据库")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存数据到数据库失败: {e}")
|
||||
if self.db_connection:
|
||||
self.db_connection.rollback()
|
||||
finally:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
|
||||
# Kafka发送方法已移除
|
||||
|
||||
def run_once(self):
|
||||
"""执行一次完整的数据采集流程"""
|
||||
logger.info("开始执行股票数据采集...")
|
||||
|
||||
# 获取股票数据
|
||||
stock_data_list = self.fetch_all_stocks()
|
||||
|
||||
if stock_data_list:
|
||||
# 保存到数据库
|
||||
self.save_to_database(stock_data_list)
|
||||
|
||||
# Kafka发送已移除
|
||||
|
||||
logger.info(f"数据采集完成,共处理 {len(stock_data_list)} 只股票")
|
||||
else:
|
||||
logger.warning("本次采集未获取到任何股票数据")
|
||||
|
||||
def start_scheduler(self):
|
||||
"""启动定时任务"""
|
||||
# 配置定时任务
|
||||
interval_minutes = self.config.getint('crawler', 'interval_minutes', fallback=5)
|
||||
|
||||
schedule.every(interval_minutes).minutes.do(self.run_once)
|
||||
|
||||
logger.info(f"定时任务已启动,每 {interval_minutes} 分钟采集一次")
|
||||
|
||||
# 立即执行一次
|
||||
self.run_once()
|
||||
|
||||
# 开始定时循环
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
|
||||
def close(self):
|
||||
"""关闭连接"""
|
||||
if self.db_connection:
|
||||
self.db_connection.close()
|
||||
# Kafka生产者关闭已移除
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
crawler = StockCrawler()
|
||||
|
||||
try:
|
||||
logger.info("股票数据采集器启动")
|
||||
crawler.start_scheduler()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("收到停止信号,正在关闭...")
|
||||
except Exception as e:
|
||||
logger.error(f"程序运行出错: {e}")
|
||||
finally:
|
||||
crawler.close()
|
||||
logger.info("股票数据采集器已停止")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
164
docker-compose.yml
Normal file
164
docker-compose.yml
Normal file
@@ -0,0 +1,164 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# MySQL数据库
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: agricultural-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: 123456
|
||||
MYSQL_DATABASE: agricultural_stock
|
||||
MYSQL_USER: agricultural
|
||||
MYSQL_PASSWORD: agricultural123
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
networks:
|
||||
- agricultural-network
|
||||
|
||||
# Redis缓存
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: agricultural-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- agricultural-network
|
||||
|
||||
# Kafka相关服务已移除
|
||||
|
||||
# Spring Boot后端服务
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: agricultural-backend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mysql
|
||||
- redis
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: docker
|
||||
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/agricultural_stock?useSSL=false&serverTimezone=Asia/Shanghai
|
||||
SPRING_DATASOURCE_USERNAME: root
|
||||
SPRING_DATASOURCE_PASSWORD: 123456
|
||||
SPRING_REDIS_HOST: redis
|
||||
SPRING_REDIS_PORT: 6379
|
||||
# Kafka配置已移除
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- backend_logs:/app/logs
|
||||
networks:
|
||||
- agricultural-network
|
||||
|
||||
# Vue.js前端服务
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
container_name: agricultural-frontend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "3000:80"
|
||||
networks:
|
||||
- agricultural-network
|
||||
|
||||
# Python数据采集服务
|
||||
data-collector:
|
||||
build:
|
||||
context: ./data-collector
|
||||
dockerfile: Dockerfile
|
||||
container_name: agricultural-collector
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mysql
|
||||
environment:
|
||||
DB_HOST: mysql
|
||||
DB_PORT: 3306
|
||||
DB_NAME: agricultural_stock
|
||||
DB_USER: root
|
||||
DB_PASSWORD: 123456
|
||||
# Kafka配置已移除
|
||||
volumes:
|
||||
- collector_logs:/app/logs
|
||||
networks:
|
||||
- agricultural-network
|
||||
|
||||
# Spark数据处理服务
|
||||
spark-master:
|
||||
image: bitnami/spark:3.4
|
||||
container_name: agricultural-spark-master
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SPARK_MODE=master
|
||||
- SPARK_RPC_AUTHENTICATION_ENABLED=no
|
||||
- SPARK_RPC_ENCRYPTION_ENABLED=no
|
||||
- SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no
|
||||
- SPARK_SSL_ENABLED=no
|
||||
ports:
|
||||
- "8081:8080"
|
||||
- "7077:7077"
|
||||
volumes:
|
||||
- spark_data:/opt/bitnami/spark/data
|
||||
- ./spark-processor:/app
|
||||
networks:
|
||||
- agricultural-network
|
||||
|
||||
spark-worker:
|
||||
image: bitnami/spark:3.4
|
||||
container_name: agricultural-spark-worker
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- spark-master
|
||||
environment:
|
||||
- SPARK_MODE=worker
|
||||
- SPARK_MASTER_URL=spark://spark-master:7077
|
||||
- SPARK_WORKER_MEMORY=2G
|
||||
- SPARK_WORKER_CORES=2
|
||||
- SPARK_RPC_AUTHENTICATION_ENABLED=no
|
||||
- SPARK_RPC_ENCRYPTION_ENABLED=no
|
||||
- SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no
|
||||
- SPARK_SSL_ENABLED=no
|
||||
volumes:
|
||||
- spark_data:/opt/bitnami/spark/data
|
||||
- ./spark-processor:/app
|
||||
networks:
|
||||
- agricultural-network
|
||||
|
||||
# Jupyter Notebook (用于LSTM模型开发)
|
||||
jupyter:
|
||||
image: jupyter/tensorflow-notebook:latest
|
||||
container_name: agricultural-jupyter
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8888:8888"
|
||||
environment:
|
||||
JUPYTER_ENABLE_LAB: "yes"
|
||||
volumes:
|
||||
- jupyter_data:/home/jovyan/work
|
||||
- ./ml-predictor:/home/jovyan/work/ml-predictor
|
||||
networks:
|
||||
- agricultural-network
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
redis_data:
|
||||
# Kafka volumes已移除
|
||||
backend_logs:
|
||||
collector_logs:
|
||||
spark_data:
|
||||
jupyter_data:
|
||||
|
||||
networks:
|
||||
agricultural-network:
|
||||
driver: bridge
|
||||
14
docs/index.html
Normal file
14
docs/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vue Docs UI</title>
|
||||
<meta name="description" content="Beautiful documentation website built with Vue Docs UI" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
1058
docs/package-lock.json
generated
Normal file
1058
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
docs/package.json
Normal file
21
docs/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "1.0.0",
|
||||
"description": "A beautiful documentation website built with Vue Docs UI",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-docs-ui": "^1.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"vite": "^4.4.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
59
docs/public/config/ai.json
Normal file
59
docs/public/config/ai.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"provider": "openai",
|
||||
"models": {
|
||||
"openai": {
|
||||
"modelId": "gpt-3.5-turbo",
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://api.openai.com/v1",
|
||||
"maxTokens": 4000,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"claude": {
|
||||
"modelId": "claude-3-sonnet-20240229",
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://api.anthropic.com",
|
||||
"maxTokens": 4000,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"gemini": {
|
||||
"modelId": "gemini-pro",
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://generativelanguage.googleapis.com/v1",
|
||||
"maxTokens": 4000,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"deepseek-v3": {
|
||||
"modelId": "deepseek-chat",
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://api.deepseek.com",
|
||||
"maxTokens": 4000,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"deepseek-r1": {
|
||||
"modelId": "deepseek-reasoner",
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://api.deepseek.com",
|
||||
"maxTokens": 4000,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"custom": {
|
||||
"modelId": "",
|
||||
"apiKey": "",
|
||||
"baseUrl": "",
|
||||
"maxTokens": 4000,
|
||||
"temperature": 0.7
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"chatAssistant": true,
|
||||
"documentSummary": true,
|
||||
"codeExplanation": true,
|
||||
"searchEnhancement": false
|
||||
},
|
||||
"ui": {
|
||||
"position": "bottom-right",
|
||||
"theme": "auto",
|
||||
"size": "medium"
|
||||
}
|
||||
}
|
||||
76
docs/public/config/site.en.yaml
Normal file
76
docs/public/config/site.en.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
# Website Configuration
|
||||
site:
|
||||
title: "Agricultural Stock Data Analysis System"
|
||||
description: "Big data-driven agricultural stock data analysis platform"
|
||||
# Logo formats supported:
|
||||
# 1. emoji: "🤖"
|
||||
# 2. image URL: "https://example.com/logo.png"
|
||||
# 3. local image: "/images/logo.png"
|
||||
# 4. relative path: "./assets/logo.svg"
|
||||
logo: "🌾"
|
||||
author: "Agricultural Stock Platform Team"
|
||||
|
||||
# Navigation Bar Configuration
|
||||
navbar:
|
||||
items:
|
||||
- title: "Home"
|
||||
link: "/"
|
||||
active: true
|
||||
- title: "Guide"
|
||||
link: "/guide"
|
||||
- title: "GitHub"
|
||||
link: "https://github.com/agricultural-stock-platform"
|
||||
external: true
|
||||
|
||||
# Sidebar Navigation Configuration
|
||||
sidebar:
|
||||
sections:
|
||||
- title: "System Guide"
|
||||
path: "/guide"
|
||||
children:
|
||||
- title: "Data Collector"
|
||||
path: "/guide/data-collector"
|
||||
- title: "Spark Processor"
|
||||
path: "/guide/spark-processor"
|
||||
- title: "Backend API"
|
||||
path: "/guide/backend"
|
||||
- title: "Frontend Interface"
|
||||
path: "/guide/frontend"
|
||||
|
||||
|
||||
# Theme Configuration
|
||||
theme:
|
||||
# Default theme mode: 'light' | 'dark' | 'auto'
|
||||
# light: Force light mode
|
||||
# dark: Force dark mode
|
||||
# auto: Follow system preference (default)
|
||||
defaultMode: "auto"
|
||||
|
||||
# Allow users to toggle theme (show theme toggle button)
|
||||
allowToggle: true
|
||||
|
||||
# Color Configuration
|
||||
colors:
|
||||
primary: "#3b82f6"
|
||||
secondary: "#64748b"
|
||||
accent: "#06b6d4"
|
||||
background: "#ffffff"
|
||||
surface: "#f8fafc"
|
||||
text: "#1e293b"
|
||||
textSecondary: "#64748b"
|
||||
border: "#e2e8f0"
|
||||
|
||||
fonts:
|
||||
primary: "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
|
||||
mono: "JetBrains Mono, Consolas, monospace"
|
||||
|
||||
# Table of Contents Configuration
|
||||
toc:
|
||||
# Maximum heading level to display in TOC (1-6)
|
||||
maxLevel: 2
|
||||
|
||||
# Enable table of contents
|
||||
enabled: true
|
||||
|
||||
# TOC title
|
||||
title: "On This Page"
|
||||
120
docs/public/config/site.yaml
Normal file
120
docs/public/config/site.yaml
Normal file
@@ -0,0 +1,120 @@
|
||||
# 网站基本配置
|
||||
site:
|
||||
title: "农业股票数据分析系统"
|
||||
description: "基于大数据技术的农业股票数据分析平台"
|
||||
# logo支持以下格式:
|
||||
# 1. emoji: "🤖"
|
||||
# 2. 图片URL: "https://example.com/logo.png"
|
||||
# 3. 本地图片: "/images/logo.png"
|
||||
# 4. 相对路径: "./assets/logo.svg"
|
||||
logo: "🌾"
|
||||
author: "Agricultural Stock Platform Team"
|
||||
|
||||
# 顶部导航配置
|
||||
navbar:
|
||||
items:
|
||||
- title: "首页"
|
||||
link: "/"
|
||||
active: true
|
||||
- title: "指南"
|
||||
link: "/guide"
|
||||
- title: "GitHub"
|
||||
link: "https://github.com/agricultural-stock-platform"
|
||||
external: true
|
||||
|
||||
# 侧边栏导航配置
|
||||
sidebar:
|
||||
sections:
|
||||
- title: "系统指南"
|
||||
path: "/guide"
|
||||
children:
|
||||
- title: "数据采集器"
|
||||
path: "/guide/data-collector"
|
||||
- title: "Spark数据处理器"
|
||||
path: "/guide/spark-processor"
|
||||
- title: "后端API服务"
|
||||
path: "/guide/backend"
|
||||
- title: "前端界面"
|
||||
path: "/guide/frontend"
|
||||
|
||||
|
||||
# Theme Configuration
|
||||
theme:
|
||||
# Default theme mode: 'light' | 'dark' | 'auto'
|
||||
# light: Force light mode
|
||||
# dark: Force dark mode
|
||||
# auto: Follow system preference (default)
|
||||
defaultMode: "auto"
|
||||
|
||||
# Allow users to toggle theme (show theme toggle button)
|
||||
allowToggle: true
|
||||
|
||||
# Color Configuration
|
||||
colors:
|
||||
primary: "#3b82f6"
|
||||
secondary: "#64748b"
|
||||
accent: "#06b6d4"
|
||||
background: "#ffffff"
|
||||
surface: "#f8fafc"
|
||||
text: "#1e293b"
|
||||
textSecondary: "#64748b"
|
||||
border: "#e2e8f0"
|
||||
success: "#10b981"
|
||||
warning: "#f59e0b"
|
||||
error: "#ef4444"
|
||||
|
||||
fonts:
|
||||
primary: "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
|
||||
mono: "JetBrains Mono, Consolas, Monaco, monospace"
|
||||
|
||||
# Layout Configuration
|
||||
layout:
|
||||
headerHeight: "60px"
|
||||
sidebarWidth: "280px"
|
||||
tocWidth: "240px"
|
||||
contentMaxWidth: "1200px"
|
||||
|
||||
# Table of Contents Configuration
|
||||
toc:
|
||||
# Maximum heading level to display in TOC (1-6)
|
||||
maxLevel: 3
|
||||
|
||||
# Enable table of contents
|
||||
enabled: true
|
||||
|
||||
# TOC title
|
||||
title: "本页目录"
|
||||
|
||||
# Show TOC on mobile devices
|
||||
showOnMobile: false
|
||||
|
||||
# Footer Configuration
|
||||
footer:
|
||||
enabled: true
|
||||
copyright: "© 2024 农业股票数据分析系统. All rights reserved."
|
||||
links:
|
||||
- title: "系统指南"
|
||||
link: "/guide"
|
||||
- title: "GitHub"
|
||||
link: "https://github.com/agricultural-stock-platform"
|
||||
external: true
|
||||
- title: "许可证"
|
||||
link: "/license"
|
||||
|
||||
# Analytics Configuration
|
||||
analytics:
|
||||
# Google Analytics
|
||||
google:
|
||||
enabled: false
|
||||
id: ""
|
||||
|
||||
# Other analytics providers can be added here
|
||||
|
||||
# PWA Configuration
|
||||
pwa:
|
||||
enabled: false
|
||||
name: "农业股票数据分析系统"
|
||||
shortName: "AgricStock"
|
||||
description: "基于大数据技术的农业股票数据分析平台"
|
||||
themeColor: "#3b82f6"
|
||||
backgroundColor: "#ffffff"
|
||||
83
docs/public/docs/en/guide.md
Normal file
83
docs/public/docs/en/guide.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# 🌾 Agricultural Stock Data Analysis System Development Guide
|
||||
|
||||
## 📋 Project Collaboration Overview
|
||||
|
||||
The Agricultural Stock Data Analysis System consists of four core sub-projects that implement a complete data pipeline from data collection to user presentation through a sophisticated collaboration mechanism. This guide provides detailed explanations of the collaboration relationships, development processes, and operational mechanisms of each sub-project.
|
||||
|
||||
## 🏗️ System Architecture & Data Flow
|
||||
|
||||
### Overall Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ External APIs │ │ Data Collector │ │ Spark Processor │ │ Backend API │
|
||||
│ │───▶│ data-collector │───▶│ spark-processor │───▶│ backend │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ • Tencent API │ │ • Python Crawler│ │ • Data Cleaning │ │ • RESTful API │
|
||||
│ • Sina Finance │ │ • Real-time Col │ │ • Tech Indicators│ │ • Data Caching │
|
||||
│ • East Money │ │ • Data Validation│ │ • Market Analysis│ │ • WebSocket │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ MySQL DB │ │ Frontend UI │
|
||||
│ Database │ │ frontend │
|
||||
│ │ │ │
|
||||
│ • Raw Data │ │ • Vue.js UI │
|
||||
│ • Analysis Data │ │ • Chart Display │
|
||||
│ • Index Opt │◀───│ • User Interact │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 2. Deployment Collaboration Checklist
|
||||
|
||||
#### Database Team Responsibilities
|
||||
- [ ] MySQL 8.0 environment setup completed
|
||||
- [ ] Database table structure creation completed
|
||||
- [ ] Index optimization configuration completed
|
||||
- [ ] Database connection pool configuration completed
|
||||
- [ ] Backup strategy configuration completed
|
||||
|
||||
#### Data Collector Team Responsibilities
|
||||
- [ ] Python 3.8+ environment configuration completed
|
||||
- [ ] Dependency package installation completed
|
||||
- [ ] Configuration file environment variables setup completed
|
||||
- [ ] Scheduled task configuration completed
|
||||
- [ ] Log monitoring configuration completed
|
||||
|
||||
#### Spark Processor Team Responsibilities
|
||||
- [ ] Java 8 environment configuration completed
|
||||
- [ ] Spark 3.4.0 cluster setup completed
|
||||
- [ ] Maven dependency management configuration completed
|
||||
- [ ] Job scheduling configuration completed
|
||||
- [ ] Performance monitoring configuration completed
|
||||
|
||||
#### Backend API Team Responsibilities
|
||||
- [ ] Spring Boot application packaging completed
|
||||
- [ ] Redis cache service configuration completed
|
||||
- [ ] API interface testing passed
|
||||
- [ ] WebSocket service configuration completed
|
||||
- [ ] Load balancing configuration completed
|
||||
|
||||
#### Frontend UI Team Responsibilities
|
||||
- [ ] Node.js environment configuration completed
|
||||
- [ ] Vue project build completed
|
||||
- [ ] Static resource deployment completed
|
||||
- [ ] CDN configuration optimization completed
|
||||
- [ ] Browser compatibility testing completed
|
||||
|
||||
## 📋 Key Success Factors for Collaboration
|
||||
|
||||
### 1. Unified Technology Stack
|
||||
- **Standardized Development Languages**: Python 3.8+, Java 8+, Node.js 16+
|
||||
- **Unified Database**: MySQL 8.0 + Redis 6.0
|
||||
- **Containerized Deployment**: Docker + Kubernetes
|
||||
- **Unified Monitoring Tools**: ELK Stack + Prometheus
|
||||
|
||||
### 2. Documentation Collaboration Standards
|
||||
- **API Documentation**: Using Swagger/OpenAPI 3.0
|
||||
- **Database Documentation**: Auto-generated using DbDoc
|
||||
- **Deployment Documentation**: Using Markdown + Flowcharts
|
||||
- **User Manual**: Using GitBook or VuePress
|
||||
|
||||
Through the above collaborative development process, the four sub-projects can organically combine to form an efficient, stable, and scalable agricultural stock data analysis system. While each team focuses on developing their own modules, they ensure overall system consistency and reliability through standardized interfaces and collaboration mechanisms.
|
||||
500
docs/public/docs/en/guide/backend.md
Normal file
500
docs/public/docs/en/guide/backend.md
Normal file
@@ -0,0 +1,500 @@
|
||||
# 🏠 Backend API Service Documentation
|
||||
|
||||
## 📋 Module Overview
|
||||
|
||||
**backend** is the API service module of the Agricultural Stock Data Analysis System, built on Spring Boot 2.7.0. It provides RESTful API interfaces, business logic processing, real-time data push, and data caching optimization services.
|
||||
|
||||
## 🏗️ Technical Architecture
|
||||
|
||||
### Core Technology Stack
|
||||
- **Spring Boot 2.7.0** - Main Application Framework
|
||||
- **Spring Data JPA** - Data Access Layer
|
||||
- **MyBatis Plus 3.5.2** - Enhanced MyBatis Framework
|
||||
- **MySQL 8.0** - Primary Database
|
||||
- **Redis 6.0** - Caching and Session Management
|
||||
- **WebSocket** - Real-time Data Push
|
||||
- **SpringDoc OpenAPI 3** - API Documentation Generation
|
||||
- **Lombok** - Code Simplification Tool
|
||||
|
||||
### Swagger UI Access
|
||||
|
||||
After starting the application, you can access the API documentation through:
|
||||
- **Swagger UI:** http://localhost:8080/swagger-ui/index.html
|
||||
- **OpenAPI JSON:** http://localhost:8080/v3/api-docs
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/main/java/com/agricultural/stock/
|
||||
│ ├── StockApplication.java # Main application class
|
||||
│ ├── controller/ # Controller layer
|
||||
│ │ ├── StockController.java # Stock data interfaces
|
||||
│ │ ├── MarketController.java # Market analysis interfaces
|
||||
│ │ └── HealthController.java # Health check interfaces
|
||||
│ ├── service/ # Service layer
|
||||
│ │ ├── StockService.java # Stock business logic
|
||||
│ │ ├── MarketAnalysisService.java # Market analysis service
|
||||
│ │ └── CacheService.java # Cache management service
|
||||
│ ├── entity/ # Entity classes
|
||||
│ │ ├── StockData.java # Stock data entity
|
||||
│ │ ├── MarketAnalysis.java # Market analysis entity
|
||||
│ │ └── TechnicalIndicator.java # Technical indicator entity
|
||||
│ ├── mapper/ # Data access layer
|
||||
│ │ ├── StockMapper.java # Stock data mapper
|
||||
│ │ └── MarketMapper.java # Market data mapper
|
||||
│ └── vo/ # View objects
|
||||
│ ├── ApiResponse.java # Unified response format
|
||||
│ ├── StockVO.java # Stock view object
|
||||
│ └── MarketAnalysisVO.java # Market analysis view object
|
||||
├── src/main/resources/
|
||||
│ ├── application.yml # Application configuration
|
||||
│ ├── mapper/ # MyBatis mapping files
|
||||
│ └── static/ # Static resources
|
||||
└── pom.xml # Maven configuration
|
||||
```
|
||||
|
||||
## 🔧 Core Functional Modules
|
||||
|
||||
### 1. Stock Data Controller (StockController)
|
||||
|
||||
**Description:** Provides stock data query and management interfaces.
|
||||
|
||||
**Main Interfaces:**
|
||||
- `GET /api/stock/realtime` - Get real-time stock data
|
||||
- `GET /api/stock/history/{stockCode}` - Get historical stock data
|
||||
- `GET /api/stock/search` - Search stocks
|
||||
- `GET /api/stock/ranking/growth` - Get growth ranking
|
||||
- `GET /api/stock/ranking/volume` - Get volume ranking
|
||||
- `GET /api/stock/detail/{stockCode}` - Get stock details
|
||||
|
||||
### 2. Market Analysis Controller (MarketController)
|
||||
|
||||
**Description:** Provides market analysis data interfaces.
|
||||
|
||||
**Main Interfaces:**
|
||||
- `GET /api/market/latest` - Get latest market analysis
|
||||
- `GET /api/market/recent/{days}` - Get recent market data
|
||||
- `GET /api/market/range` - Get market data by date range
|
||||
- `GET /api/market/industry` - Get industry analysis
|
||||
- `GET /api/market/trends` - Get market trends
|
||||
|
||||
### 3. Health Check Controller (HealthController)
|
||||
|
||||
**Description:** Provides system health monitoring interfaces.
|
||||
|
||||
**Main Interfaces:**
|
||||
- `GET /api/health/status` - Get system status
|
||||
- `GET /api/health/database` - Check database connection
|
||||
- `GET /api/health/cache` - Check cache status
|
||||
|
||||
## 🗄️ Database Integration
|
||||
|
||||
### Entity Design
|
||||
|
||||
**StockData Entity:**
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "stock_data")
|
||||
public class StockData {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "stock_code")
|
||||
private String stockCode;
|
||||
|
||||
@Column(name = "stock_name")
|
||||
private String stockName;
|
||||
|
||||
@Column(name = "open_price")
|
||||
private BigDecimal openPrice;
|
||||
|
||||
@Column(name = "close_price")
|
||||
private BigDecimal closePrice;
|
||||
|
||||
// Other fields...
|
||||
}
|
||||
```
|
||||
|
||||
### Data Access Layer
|
||||
|
||||
**Using MyBatis Plus for efficient data operations:**
|
||||
```java
|
||||
@Mapper
|
||||
public interface StockMapper extends BaseMapper<StockData> {
|
||||
|
||||
@Select("SELECT * FROM stock_data WHERE stock_code = #{stockCode} ORDER BY trade_date DESC LIMIT #{limit}")
|
||||
List<StockData> getHistoryData(@Param("stockCode") String stockCode, @Param("limit") int limit);
|
||||
|
||||
@Select("SELECT * FROM stock_data WHERE trade_date = #{date}")
|
||||
List<StockData> getStockDataByDate(@Param("date") LocalDate date);
|
||||
}
|
||||
```
|
||||
|
||||
## ⚙️ Configuration Management
|
||||
|
||||
### Application Configuration (application.yml)
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: agricultural-stock-backend
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/agricultural_stock?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: 123456
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
idle-timeout: 300000
|
||||
connection-timeout: 20000
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
show-sql: false
|
||||
database-platform: org.hibernate.dialect.MySQL8Dialect
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/*.xml
|
||||
type-aliases-package: com.agricultural.stock.entity
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: true
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
```
|
||||
|
||||
## 🚀 Deployment and Running
|
||||
|
||||
### 1. Environment Requirements
|
||||
|
||||
```bash
|
||||
# Java 8+
|
||||
java -version
|
||||
|
||||
# Maven 3.6+
|
||||
mvn -version
|
||||
|
||||
# MySQL 8.0
|
||||
mysql --version
|
||||
|
||||
# Redis 6.0
|
||||
redis-server --version
|
||||
```
|
||||
|
||||
### 2. Database Setup
|
||||
|
||||
```bash
|
||||
# Start MySQL service
|
||||
systemctl start mysql
|
||||
|
||||
# Create database
|
||||
mysql -u root -p -e "CREATE DATABASE agricultural_stock CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
|
||||
# Import data structure
|
||||
mysql -u root -p agricultural_stock < ../spark-processor/database_tables.sql
|
||||
```
|
||||
|
||||
### 3. Redis Setup
|
||||
|
||||
```bash
|
||||
# Start Redis service
|
||||
systemctl start redis
|
||||
|
||||
# Test Redis connection
|
||||
redis-cli ping
|
||||
```
|
||||
|
||||
### 4. Application Startup
|
||||
|
||||
```bash
|
||||
# Compile and package
|
||||
mvn clean package
|
||||
|
||||
# Run application
|
||||
java -jar target/agricultural-stock-backend-1.0.0.jar
|
||||
|
||||
# Or use Maven to run directly
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
## 📊 API Interface Documentation
|
||||
|
||||
### Unified Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
// Actual data content
|
||||
},
|
||||
"timestamp": 1705123456789
|
||||
}
|
||||
```
|
||||
|
||||
### Stock Data Interfaces
|
||||
|
||||
**Get Real-time Stock Data:**
|
||||
```http
|
||||
GET /api/stock/realtime
|
||||
Response:
|
||||
{
|
||||
"code": 200,
|
||||
"message": "Success",
|
||||
"data": [
|
||||
{
|
||||
"stockCode": "sz000876",
|
||||
"stockName": "New Hope",
|
||||
"currentPrice": 15.68,
|
||||
"changePercent": 2.34,
|
||||
"volume": 1234567,
|
||||
"marketCap": 6789012345
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Get Stock Details:**
|
||||
```http
|
||||
GET /api/stock/detail/sz000876
|
||||
Response:
|
||||
{
|
||||
"code": 200,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"stockCode": "sz000876",
|
||||
"stockName": "New Hope",
|
||||
"currentPrice": 15.68,
|
||||
"openPrice": 15.32,
|
||||
"highPrice": 15.89,
|
||||
"lowPrice": 15.21,
|
||||
"volume": 1234567,
|
||||
"turnover": 19456789,
|
||||
"changePercent": 2.34,
|
||||
"changeAmount": 0.36,
|
||||
"peRatio": 12.5,
|
||||
"marketCap": 6789012345
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Market Analysis Interfaces
|
||||
|
||||
**Get Latest Market Analysis:**
|
||||
```http
|
||||
GET /api/market/latest
|
||||
Response:
|
||||
{
|
||||
"code": 200,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"analysisDate": "2024-01-15",
|
||||
"upCount": 7,
|
||||
"downCount": 2,
|
||||
"flatCount": 1,
|
||||
"totalCount": 10,
|
||||
"totalMarketCap": 38147.05,
|
||||
"totalVolume": 1878844,
|
||||
"avgChangePercent": 0.33
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 Real-time Data Push
|
||||
|
||||
### WebSocket Configuration
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(new StockWebSocketHandler(), "/ws/stock/*")
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Real-time Data Handler
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class StockWebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
// Handle connection establishment
|
||||
String stockCode = extractStockCode(session.getUri());
|
||||
subscribeToStockUpdates(session, stockCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
|
||||
// Handle received messages
|
||||
}
|
||||
|
||||
public void broadcastStockUpdate(String stockCode, StockData data) {
|
||||
// Broadcast stock updates to subscribed clients
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 Caching Strategy
|
||||
|
||||
### Redis Cache Configuration
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager(RedisConnectionFactory factory) {
|
||||
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
||||
.entryTtl(Duration.ofMinutes(10))
|
||||
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
|
||||
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
|
||||
|
||||
return RedisCacheManager.builder(factory).cacheDefaults(config).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Usage
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class StockService {
|
||||
|
||||
@Cacheable(value = "stock:realtime", key = "#stockCode")
|
||||
public StockData getRealtimeData(String stockCode) {
|
||||
// Query from database
|
||||
return stockMapper.getByStockCode(stockCode);
|
||||
}
|
||||
|
||||
@CacheEvict(value = "stock:realtime", key = "#stockCode")
|
||||
public void updateStockData(String stockCode, StockData data) {
|
||||
// Update database and clear cache
|
||||
stockMapper.updateByStockCode(stockCode, data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📈 Performance Optimization
|
||||
|
||||
### Database Query Optimization
|
||||
|
||||
- **Index Creation:** Create appropriate indexes for frequently queried fields
|
||||
- **Query Optimization:** Use efficient SQL queries and avoid N+1 problems
|
||||
- **Connection Pool:** Use HikariCP for optimal connection management
|
||||
- **Pagination:** Implement proper pagination for large datasets
|
||||
|
||||
### Cache Optimization
|
||||
|
||||
- **Multi-level Caching:** Combine local cache and Redis
|
||||
- **Cache Warming:** Pre-load frequently accessed data
|
||||
- **Cache Invalidation:** Implement proper cache invalidation strategies
|
||||
- **Cache Monitoring:** Monitor cache hit rates and performance
|
||||
|
||||
## 🔒 Security Measures
|
||||
|
||||
### Data Validation
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@Validated
|
||||
public class StockController {
|
||||
|
||||
@GetMapping("/stock/detail/{stockCode}")
|
||||
public ApiResponse<StockVO> getStockDetail(
|
||||
@PathVariable @Pattern(regexp = "^(sz|sh)\\d{6}$", message = "Invalid stock code format")
|
||||
String stockCode) {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SQL Injection Prevention
|
||||
|
||||
- Use parameterized queries
|
||||
- Input validation and sanitization
|
||||
- ORM framework usage (JPA/MyBatis)
|
||||
|
||||
### CORS Configuration
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("http://localhost:3000")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 Development Guidelines
|
||||
|
||||
### Code Standards
|
||||
|
||||
- Follow Spring Boot best practices
|
||||
- Use proper layer separation (Controller-Service-Repository)
|
||||
- Implement comprehensive error handling
|
||||
- Add proper logging
|
||||
- Write unit and integration tests
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
```bash
|
||||
# Run unit tests
|
||||
mvn test
|
||||
|
||||
# Run integration tests
|
||||
mvn verify
|
||||
|
||||
# Generate test coverage report
|
||||
mvn jacoco:report
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
This module is licensed under the MIT License. See the LICENSE file in the project root directory for details.
|
||||
|
||||
---
|
||||
|
||||
**Contact Information:**
|
||||
- Project Repository: [Agricultural Stock Platform](https://github.com/agricultural-stock-platform)
|
||||
- Technical Support: support@agricultural-stock.com
|
||||
- Development Team: Agricultural Stock Platform Team
|
||||
179
docs/public/docs/en/guide/data-collector.md
Normal file
179
docs/public/docs/en/guide/data-collector.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# 📊 Python Data Collector Documentation
|
||||
|
||||
## 📋 Module Overview
|
||||
|
||||
**data-collector** is the data collection module of the Agricultural Stock Data Analysis System, built on Python 3.8+. It is responsible for real-time collection of agricultural listed companies' stock data from external data sources and storing the data in MySQL database for subsequent analysis.
|
||||
|
||||
## 🏗️ Technical Architecture
|
||||
|
||||
### Core Technology Stack
|
||||
- **Python 3.8+** - Programming Language
|
||||
- **Requests 2.31.0** - HTTP Request Library
|
||||
- **Pandas 2.1.0** - Data Processing Library
|
||||
- **BeautifulSoup4 4.12.2** - HTML Parsing Library
|
||||
- **MySQL Connector 8.1.0** - MySQL Database Connector
|
||||
- **Schedule 1.2.0** - Task Scheduling Library
|
||||
- **NumPy 1.26.0** - Numerical Computing Library
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
data-collector/
|
||||
├── stock_crawler.py # Main crawler program
|
||||
├── config.ini # Configuration file
|
||||
├── requirements.txt # Python dependencies
|
||||
└── stock_crawler.log # Runtime logs
|
||||
```
|
||||
|
||||
## 🔧 Core Features
|
||||
|
||||
### 1. Stock Data Collection
|
||||
|
||||
Supports collecting agricultural stock data from Tencent Finance API:
|
||||
- New Hope (sz000876)
|
||||
- Muyuan Foodstuff (sz002714)
|
||||
- Kweichow Moutai (sh600519)
|
||||
- Wuliangye Yibin (sz000858)
|
||||
- Inner Mongolia Yili (sh600887)
|
||||
- Jiangsu Yanghe (sz002304)
|
||||
|
||||
### 2. Data Processing Features
|
||||
|
||||
- Real-time price data acquisition
|
||||
- Automatic percentage change calculation
|
||||
- Trading volume calculation
|
||||
- Data format standardization
|
||||
- Anomalous data filtering
|
||||
|
||||
### 3. Database Storage
|
||||
|
||||
- MySQL batch writing
|
||||
- Duplicate data updates
|
||||
- Transaction management
|
||||
- Connection pool management
|
||||
|
||||
### 4. Task Scheduling
|
||||
|
||||
- Scheduled data collection
|
||||
- Error retry mechanism
|
||||
- Logging
|
||||
- Status monitoring
|
||||
|
||||
## ⚙️ Configuration Management
|
||||
|
||||
### config.ini Configuration File
|
||||
|
||||
```ini
|
||||
[database]
|
||||
host = localhost
|
||||
port = 3306
|
||||
user = root
|
||||
password = 123456
|
||||
database = agricultural_stock
|
||||
|
||||
[crawler]
|
||||
request_interval = 0.5
|
||||
request_timeout = 10
|
||||
max_retries = 3
|
||||
user_agent = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
|
||||
|
||||
[logging]
|
||||
level = INFO
|
||||
file = stock_crawler.log
|
||||
```
|
||||
|
||||
## 🚀 Deployment and Running
|
||||
|
||||
### 1. Environment Setup
|
||||
|
||||
```bash
|
||||
# Check Python version
|
||||
python --version # Requires 3.8+
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Configuration Setup
|
||||
|
||||
```bash
|
||||
# Edit configuration file
|
||||
vim config.ini
|
||||
|
||||
# Configure database connection
|
||||
[database]
|
||||
host = localhost
|
||||
port = 3306
|
||||
user = your_username
|
||||
password = your_password
|
||||
database = agricultural_stock
|
||||
```
|
||||
|
||||
### 3. Running Methods
|
||||
|
||||
```bash
|
||||
# Execute data collection once
|
||||
python stock_crawler.py
|
||||
|
||||
# Run in background
|
||||
nohup python stock_crawler.py > /dev/null 2>&1 &
|
||||
```
|
||||
|
||||
## 📊 Data Quality Assurance
|
||||
|
||||
### Data Validation Mechanism
|
||||
|
||||
- Required field completeness check
|
||||
- Price data rationality validation
|
||||
- Percentage change anomaly detection
|
||||
- Trading volume negative value filtering
|
||||
|
||||
### Exception Handling
|
||||
|
||||
- Network request retry mechanism
|
||||
- Database connection failure recovery
|
||||
- Detailed error logging
|
||||
- Graceful exception handling
|
||||
|
||||
## 📈 Monitoring and Maintenance
|
||||
|
||||
### Log Management
|
||||
|
||||
The system automatically records the following information:
|
||||
- Data collection start/end time
|
||||
- Number of successfully collected stocks
|
||||
- Failed stock codes and reasons
|
||||
- Database operation results
|
||||
- Exception error details
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
- Collection time statistics
|
||||
- Success rate monitoring
|
||||
- Error rate statistics
|
||||
- Database performance monitoring
|
||||
|
||||
## 📋 Dependency Management
|
||||
|
||||
### requirements.txt
|
||||
|
||||
```txt
|
||||
requests>=2.31.0
|
||||
pandas>=2.1.0
|
||||
beautifulsoup4>=4.12.2
|
||||
lxml>=4.9.3
|
||||
mysql-connector-python>=8.1.0
|
||||
schedule>=1.2.0
|
||||
numpy>=1.26.0
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
This module is licensed under the MIT License. See the LICENSE file in the project root directory for details.
|
||||
|
||||
---
|
||||
|
||||
**Contact Information:**
|
||||
- Project Repository: [Agricultural Stock Platform](https://github.com/agricultural-stock-platform)
|
||||
- Technical Support: support@agricultural-stock.com
|
||||
- Development Team: Agricultural Stock Platform Team
|
||||
205
docs/public/docs/en/guide/frontend.md
Normal file
205
docs/public/docs/en/guide/frontend.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# 🎨 Frontend Interface Documentation
|
||||
|
||||
## 📋 Module Overview
|
||||
|
||||
**frontend** is the user interface module of the Agricultural Stock Data Analysis System, built on Vue.js 3.3.0. It provides real-time stock data display, interactive charts, market analysis visualization, and responsive user experience.
|
||||
|
||||
## 🏗️ Technical Architecture
|
||||
|
||||
### Core Technology Stack
|
||||
- **Vue.js 3.3.0** - Progressive JavaScript Framework
|
||||
- **Vue Router 4.2.0** - Official Router for Vue.js
|
||||
- **Vuex 4.0.2** - State Management Pattern and Library
|
||||
- **Element Plus 2.3.0** - Vue 3 Component Library
|
||||
- **ECharts 5.4.0** - Data Visualization Library
|
||||
- **Axios 1.4.0** - HTTP Client Library
|
||||
- **Vite 4.3.0** - Build Tool and Development Server
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── main.ts # Application entry point
|
||||
│ ├── App.vue # Root component
|
||||
│ ├── views/ # Page components
|
||||
│ │ ├── Dashboard.vue # Dashboard overview
|
||||
│ │ ├── MarketAnalysis.vue # Market analysis
|
||||
│ │ ├── StockDetail.vue # Stock details
|
||||
│ │ ├── StockSearch.vue # Stock search
|
||||
│ │ └── Rankings.vue # Stock rankings
|
||||
│ ├── components/ # Reusable components
|
||||
│ ├── router/ # Routing configuration
|
||||
│ ├── store/ # State management
|
||||
│ ├── api/ # API interface encapsulation
|
||||
│ └── utils/ # Utility functions
|
||||
├── package.json # Dependencies and scripts
|
||||
└── vite.config.js # Vite configuration
|
||||
```
|
||||
|
||||
## 🔧 Core Functional Modules
|
||||
|
||||
### 1. Dashboard Overview (Dashboard.vue)
|
||||
|
||||
**Main Features:**
|
||||
- Real-time market summary cards
|
||||
- Market trend charts
|
||||
- Industry distribution visualization
|
||||
- Top gainers/losers lists
|
||||
- Market news display
|
||||
|
||||
### 2. Market Analysis Page (MarketAnalysis.vue)
|
||||
|
||||
**Main Features:**
|
||||
- Market trend analysis
|
||||
- Technical indicator charts
|
||||
- Industry comparison analysis
|
||||
- Historical data backtesting
|
||||
|
||||
### 3. Stock Detail Page (StockDetail.vue)
|
||||
|
||||
**Main Features:**
|
||||
- Real-time price information
|
||||
- Interactive price charts
|
||||
- Trading volume analysis
|
||||
- Financial metrics
|
||||
- Related news
|
||||
|
||||
### 4. Stock Search Page (StockSearch.vue)
|
||||
|
||||
**Main Features:**
|
||||
- Keyword-based search
|
||||
- Advanced filtering options
|
||||
- Search result display
|
||||
- Search history tracking
|
||||
|
||||
### 5. Rankings Page (Rankings.vue)
|
||||
|
||||
**Main Features:**
|
||||
- Top gainers/losers rankings
|
||||
- Volume rankings
|
||||
- Market cap rankings
|
||||
- Custom sorting options
|
||||
|
||||
## 🚀 Deployment and Running
|
||||
|
||||
### 1. Environment Requirements
|
||||
|
||||
```bash
|
||||
# Node.js 16+
|
||||
node --version
|
||||
|
||||
# npm or yarn
|
||||
npm --version
|
||||
```
|
||||
|
||||
### 2. Installation and Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development server
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 3. Environment Configuration
|
||||
|
||||
```env
|
||||
# Development
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
VITE_WS_BASE_URL=ws://localhost:8080
|
||||
|
||||
# Production
|
||||
VITE_API_BASE_URL=https://api.agricultural-stock.com
|
||||
VITE_WS_BASE_URL=wss://api.agricultural-stock.com
|
||||
```
|
||||
|
||||
## 📊 Data Visualization
|
||||
|
||||
### ECharts Integration
|
||||
|
||||
The system uses ECharts for creating interactive charts:
|
||||
- Stock price trend charts
|
||||
- Market distribution pie charts
|
||||
- Technical indicator visualizations
|
||||
- Volume analysis charts
|
||||
|
||||
## 🎯 State Management
|
||||
|
||||
### Vuex Store
|
||||
|
||||
Global state management for:
|
||||
- Stock data caching
|
||||
- User interface state
|
||||
- Real-time data updates
|
||||
- Application configuration
|
||||
|
||||
## 🔌 API Integration
|
||||
|
||||
### HTTP Client
|
||||
|
||||
Axios-based HTTP client with:
|
||||
- Request/response interceptors
|
||||
- Error handling
|
||||
- Loading state management
|
||||
- API response standardization
|
||||
|
||||
## ⚙️ Build Configuration
|
||||
|
||||
### Vite Configuration
|
||||
|
||||
- Hot module replacement
|
||||
- Proxy configuration for development
|
||||
- Production build optimization
|
||||
- Asset bundling and compression
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
### Mobile Adaptation
|
||||
|
||||
- Responsive grid layouts
|
||||
- Mobile-optimized navigation
|
||||
- Touch-friendly interactions
|
||||
- Adaptive chart sizing
|
||||
|
||||
## 🔒 Security Measures
|
||||
|
||||
### Client-side Security
|
||||
|
||||
- XSS protection
|
||||
- Input validation
|
||||
- CSRF token handling
|
||||
- Secure API communication
|
||||
|
||||
## 📈 Performance Optimization
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
- Component lazy loading
|
||||
- Virtual scrolling for large lists
|
||||
- Data caching strategies
|
||||
- Bundle size optimization
|
||||
|
||||
## 📋 Development Guidelines
|
||||
|
||||
### Code Standards
|
||||
|
||||
- ESLint + Prettier configuration
|
||||
- TypeScript support
|
||||
- Component naming conventions
|
||||
- Testing best practices
|
||||
|
||||
## 📄 License
|
||||
|
||||
This module is licensed under the MIT License. See the LICENSE file in the project root directory for details.
|
||||
|
||||
---
|
||||
|
||||
**Contact Information:**
|
||||
- Project Repository: [Agricultural Stock Platform](https://github.com/agricultural-stock-platform)
|
||||
- Technical Support: support@agricultural-stock.com
|
||||
- Development Team: Agricultural Stock Platform Team
|
||||
330
docs/public/docs/en/guide/spark-processor.md
Normal file
330
docs/public/docs/en/guide/spark-processor.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# 🌾 Spark Data Processing Module Documentation
|
||||
|
||||
## 📋 Module Overview
|
||||
|
||||
**spark-processor** is the core data processing engine of the Agricultural Stock Data Analysis System, built on Apache Spark 3.4.0. It is responsible for large-scale processing, technical analysis, and market analysis of agricultural listed companies' stock data.
|
||||
|
||||
## 🏗️ Technical Architecture
|
||||
|
||||
### Core Technology Stack
|
||||
- **Apache Spark 3.4.0** - Distributed Big Data Processing Framework
|
||||
- **Spark SQL** - Structured Data Query and Processing
|
||||
- **Spark Streaming** - Real-time Stream Data Processing
|
||||
- **MySQL 8.0** - Data Storage and Persistence
|
||||
- **Java 8** - Programming Language
|
||||
- **Maven 3.6+** - Project Build Management
|
||||
|
||||
### Architecture Design
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Spark Data Processing Module │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
|
||||
│ │ Data Cleaning │ │ Technical │ │ Market │ │
|
||||
│ │ Service │ │ Indicator │ │ Analysis │ │
|
||||
│ │ │ │ Service │ │ Service │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ Database Save │ │ Configuration │ │
|
||||
│ │ Service │ │ SparkConfig │ │
|
||||
│ │ │ │ │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ StockDataProcessor (Main Processing Class) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
spark-processor/
|
||||
├── src/main/java/com/agricultural/spark/
|
||||
│ ├── StockDataProcessor.java # Main processing class
|
||||
│ ├── config/
|
||||
│ │ └── SparkConfig.java # Configuration management
|
||||
│ └── service/
|
||||
│ ├── DataCleaningService.java # Data cleaning service
|
||||
│ ├── TechnicalIndicatorService.java # Technical indicator service
|
||||
│ ├── MarketAnalysisService.java # Market analysis service
|
||||
│ └── DatabaseSaveService.java # Database save service
|
||||
├── src/main/resources/
|
||||
│ ├── application.conf # Application configuration
|
||||
│ └── logback.xml # Logging configuration
|
||||
├── database_tables.sql # Database table structure
|
||||
├── pom.xml # Maven configuration
|
||||
└── README_DATABASE.md # Database documentation
|
||||
```
|
||||
|
||||
## 🔧 Core Functional Modules
|
||||
|
||||
### 1. Main Data Processor (StockDataProcessor)
|
||||
|
||||
**Description:** The core scheduler of the system, responsible for coordinating the workflow of various service modules.
|
||||
|
||||
**Main Responsibilities:**
|
||||
- Spark session management and initialization
|
||||
- Data loading and preprocessing coordination
|
||||
- Batch processing and stream processing mode switching
|
||||
- Exception handling and resource management
|
||||
|
||||
**Key Methods:**
|
||||
```java
|
||||
// Batch processing mode
|
||||
public void runBatchProcessing()
|
||||
|
||||
// Stream processing mode
|
||||
public void runStreamProcessing()
|
||||
|
||||
// Load data from MySQL
|
||||
public Dataset<Row> loadStockDataFromMySQL()
|
||||
```
|
||||
|
||||
### 2. Data Cleaning Service (DataCleaningService)
|
||||
|
||||
**Description:** Standardized cleaning and preprocessing of raw stock data.
|
||||
|
||||
**Processing Flow:**
|
||||
1. **Deduplication** - Remove duplicate records based on stock code and trading date
|
||||
2. **Missing Value Handling** - Fill numerical fields with 0, string fields with empty values
|
||||
3. **Data Type Conversion** - Ensure field type correctness
|
||||
4. **Outlier Detection** - Identify and handle anomalous data in prices, volumes, etc.
|
||||
5. **Derived Field Calculation** - Calculate percentage changes, market cap, and other derived metrics
|
||||
|
||||
**Key Functions:**
|
||||
```java
|
||||
// Main cleaning method
|
||||
public Dataset<Row> cleanData(Dataset<Row> rawData)
|
||||
|
||||
// Missing value handling
|
||||
private Dataset<Row> handleMissingValues(Dataset<Row> data)
|
||||
|
||||
// Outlier handling
|
||||
private Dataset<Row> handleOutliers(Dataset<Row> data)
|
||||
```
|
||||
|
||||
### 3. Technical Indicator Service (TechnicalIndicatorService)
|
||||
|
||||
**Description:** Calculate various technical analysis indicators to provide data support for quantitative analysis.
|
||||
|
||||
**Supported Technical Indicators:**
|
||||
- **Moving Averages (MA)** - MA5, MA10, MA20, MA30
|
||||
- **Relative Strength Index (RSI)** - 14-day RSI indicator
|
||||
- **MACD Indicator** - Fast line, slow line, histogram
|
||||
- **Bollinger Bands** - Upper band, middle band, lower band
|
||||
- **Volume Moving Average** - Volume technical analysis
|
||||
|
||||
**Calculation Methods:**
|
||||
```java
|
||||
// Calculate technical indicators
|
||||
public Dataset<Row> calculateTechnicalIndicators(Dataset<Row> data)
|
||||
|
||||
// Moving averages calculation
|
||||
private Dataset<Row> calculateMovingAverages(Dataset<Row> data)
|
||||
|
||||
// RSI indicator calculation
|
||||
private Dataset<Row> calculateRSI(Dataset<Row> data)
|
||||
```
|
||||
|
||||
### 4. Market Analysis Service (MarketAnalysisService)
|
||||
|
||||
**Description:** Analyze overall market performance and industry distribution from a macro perspective.
|
||||
|
||||
**Analysis Dimensions:**
|
||||
- **Market Overview** - Rise/fall statistics, average percentage change, total market cap
|
||||
- **Industry Performance** - Stock performance statistics by industry classification
|
||||
- **Market Trends** - Historical trend analysis and forecasting
|
||||
- **Sector Rotation** - Fund flow analysis between different sectors
|
||||
|
||||
**Core Analysis:**
|
||||
```java
|
||||
// Market overview analysis
|
||||
public Map<String, Object> analyzeMarketOverview(Dataset<Row> data)
|
||||
|
||||
// Industry performance analysis
|
||||
public Dataset<Row> analyzeIndustryPerformance(Dataset<Row> data)
|
||||
|
||||
// Market trend analysis
|
||||
public Dataset<Row> analyzeTrends(Dataset<Row> data)
|
||||
```
|
||||
|
||||
### 5. Database Save Service (DatabaseSaveService)
|
||||
|
||||
**Description:** Persist Spark-processed result data to MySQL database.
|
||||
|
||||
**Save Strategy:**
|
||||
- **Incremental Updates** - Save only new or changed data
|
||||
- **Batch Writing** - Improve data writing efficiency
|
||||
- **Transaction Management** - Ensure data consistency
|
||||
- **Connection Pool Management** - Optimize database connection performance
|
||||
|
||||
**Main Functions:**
|
||||
```java
|
||||
// Save market analysis results
|
||||
public void saveMarketAnalysis(Map<String, Object> analysisResult)
|
||||
|
||||
// Save technical indicator data
|
||||
public void saveTechnicalIndicators(Dataset<Row> indicators)
|
||||
|
||||
// Batch save processing results
|
||||
public void batchSaveResults(List<Dataset<Row>> datasets)
|
||||
```
|
||||
|
||||
## 🗄️ Database Design
|
||||
|
||||
### Main Data Tables
|
||||
|
||||
| Table Name | Description | Key Fields |
|
||||
|------------|-------------|------------|
|
||||
| `stock_data` | Stock basic data | stock_code, stock_name, open_price, close_price, volume, trade_date |
|
||||
| `market_analysis` | Market analysis results | analysis_date, up_count, down_count, total_market_cap, avg_change_percent |
|
||||
| `stock_technical_indicators` | Technical indicator data | stock_code, trade_date, ma5, ma10, ma20, rsi, macd |
|
||||
| `industry_analysis` | Industry analysis data | industry_name, stock_count, avg_change_percent, total_market_cap |
|
||||
| `market_trends` | Market trend data | trade_date, avg_price, total_volume, stock_count |
|
||||
|
||||
## ⚙️ Configuration Management
|
||||
|
||||
### Configuration File (application.conf)
|
||||
|
||||
```hocon
|
||||
# Spark configuration
|
||||
spark {
|
||||
master = "local[*]"
|
||||
app.name = "AgriculturalStockDataProcessor"
|
||||
}
|
||||
|
||||
# MySQL database configuration
|
||||
mysql {
|
||||
host = "localhost"
|
||||
port = 3306
|
||||
database = "agricultural_stock"
|
||||
username = "root"
|
||||
password = "123456"
|
||||
}
|
||||
|
||||
# Processing configuration
|
||||
processing {
|
||||
batch.size = 1000
|
||||
window.duration = "10 seconds"
|
||||
checkpoint.location = "/tmp/spark-checkpoint"
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Deployment and Running
|
||||
|
||||
### 1. Environment Requirements
|
||||
|
||||
```bash
|
||||
# Java 8+ (recommended OpenJDK 8)
|
||||
java -version
|
||||
|
||||
# Maven 3.6+
|
||||
mvn -version
|
||||
|
||||
# MySQL 8.0+
|
||||
mysql --version
|
||||
```
|
||||
|
||||
### 2. Database Setup
|
||||
|
||||
```bash
|
||||
# Create database
|
||||
mysql -u root -p -e "CREATE DATABASE agricultural_stock CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
|
||||
# Import table structure
|
||||
mysql -u root -p agricultural_stock < database_tables.sql
|
||||
```
|
||||
|
||||
### 3. Build and Run
|
||||
|
||||
```bash
|
||||
# Compile project
|
||||
mvn clean compile
|
||||
|
||||
# Run batch processing
|
||||
mvn exec:java -Dexec.mainClass="com.agricultural.spark.StockDataProcessor"
|
||||
|
||||
# Run with custom parameters
|
||||
mvn exec:java -Dexec.mainClass="com.agricultural.spark.StockDataProcessor" -Dexec.args="--mode batch --date 2024-01-01"
|
||||
```
|
||||
|
||||
## 📊 Performance Optimization
|
||||
|
||||
### Spark Optimization Strategies
|
||||
|
||||
- **Memory Management** - Optimize heap size and garbage collection
|
||||
- **Parallelism Tuning** - Adjust partition and executor numbers
|
||||
- **Cache Strategy** - Cache frequently accessed datasets
|
||||
- **Broadcast Variables** - Use broadcast for small lookup tables
|
||||
|
||||
### Database Optimization
|
||||
|
||||
- **Index Creation** - Create appropriate indexes for query optimization
|
||||
- **Connection Pooling** - Use HikariCP for connection management
|
||||
- **Batch Operations** - Use batch inserts for better performance
|
||||
- **Query Optimization** - Optimize SQL queries and use prepared statements
|
||||
|
||||
## 📈 Monitoring and Maintenance
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
- **Spark UI** - Monitor job execution and resource usage
|
||||
- **Application Metrics** - Track processing time and throughput
|
||||
- **Database Metrics** - Monitor connection pool and query performance
|
||||
- **System Resources** - Monitor CPU, memory, and disk usage
|
||||
|
||||
### Log Management
|
||||
|
||||
```xml
|
||||
<!-- logback.xml configuration -->
|
||||
<configuration>
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>logs/spark-processor.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>logs/spark-processor.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
## 📋 Development Guidelines
|
||||
|
||||
### Code Standards
|
||||
|
||||
- Follow Java coding conventions
|
||||
- Use meaningful variable and method names
|
||||
- Add comprehensive comments and documentation
|
||||
- Implement proper error handling
|
||||
- Write unit tests for critical components
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
```bash
|
||||
# Run unit tests
|
||||
mvn test
|
||||
|
||||
# Run integration tests
|
||||
mvn verify
|
||||
|
||||
# Generate coverage report
|
||||
mvn jacoco:report
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
This module is licensed under the MIT License. See the LICENSE file in the project root directory for details.
|
||||
|
||||
---
|
||||
|
||||
**Contact Information:**
|
||||
- Project Repository: [Agricultural Stock Platform](https://github.com/agricultural-stock-platform)
|
||||
- Technical Support: support@agricultural-stock.com
|
||||
- Development Team: Agricultural Stock Platform Team
|
||||
64
docs/public/docs/en/index.md
Normal file
64
docs/public/docs/en/index.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Welcome to Agricultural Stock Data Analysis System
|
||||
|
||||
The Agricultural Stock Data Analysis System is a comprehensive big data-driven platform focused on agricultural listed companies' stock data collection, processing, analysis, and visualization.
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
- **🚀 Real-time Data Collection** - Automated collection of agricultural stock data from multiple sources
|
||||
- **🎨 Interactive Visualization** - Beautiful charts and dashboards with real-time updates
|
||||
- **📱 Responsive Design** - Perfect experience across desktop and mobile devices
|
||||
- **🌐 Comprehensive Analysis** - Technical indicators, market trends, and industry analysis
|
||||
- **🤖 Big Data Processing** - Apache Spark-powered large-scale data processing
|
||||
- **⚡ High Performance** - Optimized for handling millions of data points
|
||||
- **🔍 Advanced Search** - Intelligent stock search and filtering capabilities
|
||||
- **📝 Rich Documentation** - Complete technical documentation and user guides
|
||||
|
||||
## 🏗️ System Architecture
|
||||
|
||||
- **Microservices Design** - Modular architecture with independent services
|
||||
- **Scalable Processing** - Distributed computing with Apache Spark
|
||||
- **Modern Tech Stack** - Vue.js 3, Spring Boot, Python, and MySQL
|
||||
- **Real-time Updates** - WebSocket-based live data streaming
|
||||
|
||||
## 📦 System Components
|
||||
|
||||
```bash
|
||||
# Data Collection Layer
|
||||
├── Python Data Collector # Real-time stock data collection
|
||||
├── External APIs Integration # Tencent Finance, Sina Finance
|
||||
|
||||
# Data Processing Layer
|
||||
├── Apache Spark Processor # Big data processing engine
|
||||
├── Technical Indicators # MA, RSI, MACD, Bollinger Bands
|
||||
├── Market Analysis # Industry trends, market overview
|
||||
|
||||
# Service Layer
|
||||
├── Spring Boot Backend # RESTful API services
|
||||
├── WebSocket Services # Real-time data streaming
|
||||
├── Redis Caching # Performance optimization
|
||||
|
||||
# Presentation Layer
|
||||
├── Vue.js Frontend # Interactive user interface
|
||||
├── ECharts Visualization # Charts and graphs
|
||||
└── Responsive Design # Mobile-friendly UI
|
||||
```
|
||||
|
||||
## 📊 Supported Agricultural Stocks
|
||||
|
||||
- **New Hope (sz000876)** - Leading livestock and feed company
|
||||
- **Muyuan Foodstuff (sz002714)** - Major pig farming enterprise
|
||||
- **Kweichow Moutai (sh600519)** - Premium liquor producer
|
||||
- **Wuliangye Yibin (sz000858)** - Famous Chinese spirits brand
|
||||
- **Inner Mongolia Yili (sh600887)** - Dairy industry leader
|
||||
- **Jiangsu Yanghe (sz002304)** - Renowned liquor manufacturer
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
Visit our [System Guide](/guide) to learn about each component and how to deploy the platform.
|
||||
|
||||
## 🔗 Related Links
|
||||
|
||||
- [GitHub Repository](https://github.com/agricultural-stock-platform)
|
||||
- [System Guide](/guide)
|
||||
- [Data Collector](/guide/data-collector)
|
||||
- [Spark Processor](/guide/spark-processor)
|
||||
87
docs/public/docs/zh-cn/guide.md
Normal file
87
docs/public/docs/zh-cn/guide.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 🌾 农业股票数据分析系统开发指南
|
||||
|
||||
## 📋 项目协作概述
|
||||
|
||||
农业股票数据分析系统由四个核心子项目组成,通过精密的协作机制实现从数据采集到用户展示的完整数据流水线。本指南详细说明各子项目的协作关系、开发流程和运作机制。
|
||||
|
||||
## 🏗️ 系统架构与数据流
|
||||
|
||||
### 整体数据流向
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ 外部数据源 │ │ 数据采集器 │ │ Spark处理器 │ │ 后端API服务 │
|
||||
│ External APIs │───▶│ data-collector │───▶│ spark-processor │───▶│ backend │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ • 腾讯财经API │ │ • Python爬虫 │ │ • 数据清洗 │ │ • RESTful API │
|
||||
│ • 新浪财经API │ │ • 实时采集 │ │ • 技术指标计算 │ │ • 数据缓存 │
|
||||
│ • 东方财富API │ │ • 数据验证 │ │ • 市场分析 │ │ • WebSocket │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ MySQL数据库 │ │ 前端界面 │
|
||||
│ Database │ │ frontend │
|
||||
│ │ │ │
|
||||
│ • 原始数据存储 │ │ • Vue.js界面 │
|
||||
│ • 分析结果存储 │ │ • 图表展示 │
|
||||
│ • 索引优化 │◀───│ • 用户交互 │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 2. 部署协作检查清单
|
||||
|
||||
#### 数据库团队责任
|
||||
- [ ] MySQL 8.0环境搭建完成
|
||||
- [ ] 数据库表结构创建完成
|
||||
- [ ] 索引优化配置完成
|
||||
- [ ] 数据库连接池配置完成
|
||||
- [ ] 备份策略配置完成
|
||||
|
||||
#### 数据采集器团队责任
|
||||
- [ ] Python 3.8+环境配置完成
|
||||
- [ ] 依赖包安装完成
|
||||
- [ ] 配置文件环境变量设置完成
|
||||
- [ ] 定时任务调度配置完成
|
||||
- [ ] 日志监控配置完成
|
||||
|
||||
#### Spark处理器团队责任
|
||||
- [ ] Java 8环境配置完成
|
||||
- [ ] Spark 3.4.0集群搭建完成
|
||||
- [ ] Maven依赖管理配置完成
|
||||
- [ ] 作业调度配置完成
|
||||
- [ ] 性能监控配置完成
|
||||
|
||||
#### 后端API团队责任
|
||||
- [ ] Spring Boot应用打包完成
|
||||
- [ ] Redis缓存服务配置完成
|
||||
- [ ] API接口测试通过
|
||||
- [ ] WebSocket服务配置完成
|
||||
- [ ] 负载均衡配置完成
|
||||
|
||||
#### 前端界面团队责任
|
||||
- [ ] Node.js环境配置完成
|
||||
- [ ] Vue项目构建完成
|
||||
- [ ] 静态资源部署完成
|
||||
- [ ] CDN配置优化完成
|
||||
- [ ] 浏览器兼容性测试完成
|
||||
|
||||
|
||||
## 📋 协作成功要素
|
||||
|
||||
### 1. 技术栈统一
|
||||
- **开发语言标准化**:Python 3.8+, Java 8+, Node.js 16+
|
||||
- **数据库统一**:MySQL 8.0 + Redis 6.0
|
||||
- **容器化部署**:Docker + Kubernetes
|
||||
- **监控工具统一**:ELK Stack + Prometheus
|
||||
|
||||
### 2. 文档协作规范
|
||||
- **API文档**:使用Swagger/OpenAPI 3.0
|
||||
- **数据库文档**:使用DbDoc自动生成
|
||||
- **部署文档**:使用Markdown + 流程图
|
||||
- **用户手册**:使用GitBook或VuePress
|
||||
|
||||
|
||||
通过以上协作开发流程,四个子项目能够有机结合,形成一个高效、稳定、可扩展的农业股票数据分析系统。每个团队在专注自身模块开发的同时,通过标准化的接口和协作机制,确保系统整体的一致性和可靠性。
|
||||
280
docs/public/docs/zh-cn/guide/backend.md
Normal file
280
docs/public/docs/zh-cn/guide/backend.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# 🏠 Spring Boot后端API文档
|
||||
|
||||
## 📋 模块概述
|
||||
|
||||
**backend** 是农业股票数据分析系统的后端API服务模块,基于Spring Boot 2.7.0构建,负责提供RESTful API接口、业务逻辑处理、数据库操作和系统集成服务。
|
||||
|
||||
## Swagger UI访问
|
||||
|
||||
启动应用后,可通过以下地址访问API文档:
|
||||
- **Swagger UI:** http://localhost:8080/swagger-ui/index.html
|
||||
- **OpenAPI JSON:** http://localhost:8080/v3/api-docs
|
||||
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 核心技术栈
|
||||
- **Spring Boot 2.7.0** - 核心框架
|
||||
- **Spring Web** - RESTful API支持
|
||||
- **Spring Data JPA** - 数据持久化
|
||||
- **MyBatis Plus 3.5.2** - 数据库操作增强
|
||||
- **MySQL 8.0** - 关系型数据库
|
||||
- **Redis** - 缓存与会话管理
|
||||
- **WebSocket** - 实时数据推送
|
||||
- **SpringDoc OpenAPI 3** - API文档生成
|
||||
- **Lombok** - 代码简化工具
|
||||
|
||||
|
||||
### 系统架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Spring Boot 后端服务 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
|
||||
│ │ 控制器层 │ │ 服务层 │ │ 数据层 │ │
|
||||
│ │ Controllers │ │ Services │ │ Mappers │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 股票数据API │ │ • 股票数据服务 │ │ • 数据访问 │ │
|
||||
│ │ • 市场分析API │ │ • 市场分析服务 │ │ • SQL映射 │ │
|
||||
│ │ • 健康检查API │ │ • 缓存服务 │ │ • 实体映射 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
|
||||
│ │ 实体层 │ │ 配置层 │ │ 工具层 │ │
|
||||
│ │ Entities │ │ Configurations │ │ Utils │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 数据模型 │ │ • 数据库配置 │ │ • 响应封装 │ │
|
||||
│ │ • VO对象 │ │ • Redis配置 │ │ • 异常处理 │ │
|
||||
│ │ • DTO对象 │ │ • WebSocket配置 │ │ • 工具类 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/main/java/com/agricultural/stock/
|
||||
│ ├── StockPlatformApplication.java # 应用启动类
|
||||
│ ├── controller/ # 控制器层
|
||||
│ │ ├── StockController.java # 股票数据API控制器
|
||||
│ │ ├── MarketAnalysisController.java # 市场分析API控制器
|
||||
│ │ └── HealthController.java # 健康检查控制器
|
||||
│ ├── service/ # 服务层
|
||||
│ │ ├── StockService.java # 股票数据服务接口
|
||||
│ │ ├── MarketAnalysisService.java # 市场分析服务
|
||||
│ │ └── impl/
|
||||
│ │ └── StockServiceImpl.java # 股票数据服务实现
|
||||
│ ├── entity/ # 实体层
|
||||
│ │ ├── StockData.java # 股票数据实体
|
||||
│ │ ├── MarketAnalysis.java # 市场分析实体
|
||||
│ │ └── MarketTrends.java # 市场趋势实体
|
||||
│ ├── mapper/ # 数据访问层
|
||||
│ │ ├── StockDataMapper.java # 股票数据Mapper
|
||||
│ │ └── MarketAnalysisMapper.java # 市场分析Mapper
|
||||
│ ├── vo/ # 视图对象
|
||||
│ │ ├── Result.java # 统一响应结果
|
||||
│ │ ├── StockAnalysisVO.java # 股票分析视图对象
|
||||
│ │ └── StockTrendVO.java # 股票趋势视图对象
|
||||
│ └── config/ # 配置类
|
||||
│ ├── WebConfig.java # Web配置
|
||||
│ ├── RedisConfig.java # Redis配置
|
||||
│ └── WebSocketConfig.java # WebSocket配置
|
||||
├── src/main/resources/
|
||||
│ ├── application.yml # 应用配置文件
|
||||
│ ├── mapper/ # MyBatis映射文件
|
||||
│ └── static/ # 静态资源
|
||||
└── pom.xml # Maven配置文件
|
||||
```
|
||||
|
||||
## 🔧 核心功能模块
|
||||
|
||||
### 1. 股票数据API (StockController)
|
||||
|
||||
**功能描述:** 提供股票数据的增删改查、排行榜、搜索等核心API接口。
|
||||
|
||||
**主要接口:**
|
||||
|
||||
| HTTP方法 | 路径 | 描述 | 响应格式 |
|
||||
|---------|------|------|----------|
|
||||
| GET | `/api/stock/realtime` | 获取实时股票数据 | `Result<List<StockData>>` |
|
||||
| GET | `/api/stock/history/{stockCode}` | 获取历史数据 | `Result<List<StockData>>` |
|
||||
| GET | `/api/stock/ranking/growth` | 涨幅排行榜 | `Result<List<StockData>>` |
|
||||
| GET | `/api/stock/ranking/market-cap` | 市值排行榜 | `Result<List<StockData>>` |
|
||||
| GET | `/api/stock/ranking/volume` | 成交量排行榜 | `Result<List<StockData>>` |
|
||||
| GET | `/api/stock/trend/{stockCode}` | 股票趋势分析 | `Result<StockTrendVO>` |
|
||||
| GET | `/api/stock/analysis` | 市场综合分析 | `Result<StockAnalysisVO>` |
|
||||
| GET | `/api/stock/search` | 股票搜索 | `Result<List<StockData>>` |
|
||||
| POST | `/api/stock/save` | 保存股票数据 | `Result<StockData>` |
|
||||
| POST | `/api/stock/batch-save` | 批量保存数据 | `Result<Integer>` |
|
||||
|
||||
|
||||
|
||||
|
||||
### 2. 市场分析API (MarketAnalysisController)
|
||||
|
||||
**功能描述:** 提供市场分析数据的查询和统计接口。
|
||||
|
||||
**主要接口:**
|
||||
|
||||
| HTTP方法 | 路径 | 描述 | 参数 |
|
||||
|---------|------|------|------|
|
||||
| GET | `/api/market/latest` | 获取最新市场分析 | 无 |
|
||||
| GET | `/api/market/recent/{days}` | 获取最近N天数据 | days: 天数 |
|
||||
| GET | `/api/market/range` | 获取日期范围数据 | startDate, endDate |
|
||||
|
||||
|
||||
## 🚀 部署与运行
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
```bash
|
||||
# 检查Java版本
|
||||
java -version
|
||||
|
||||
# 检查Maven版本
|
||||
mvn -version
|
||||
|
||||
# 启动MySQL服务
|
||||
sudo systemctl start mysql
|
||||
|
||||
# 启动Redis服务
|
||||
sudo systemctl start redis
|
||||
```
|
||||
|
||||
### 2. 数据库初始化
|
||||
|
||||
```sql
|
||||
# 创建数据库
|
||||
CREATE DATABASE agricultural_stock CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
# 导入表结构
|
||||
mysql -u root -p agricultural_stock < ../agricultural_stock.sql
|
||||
```
|
||||
|
||||
### 3. 编译运行
|
||||
|
||||
```bash
|
||||
# 进入后端目录
|
||||
cd backend
|
||||
|
||||
# 清理编译
|
||||
mvn clean compile
|
||||
|
||||
# 运行应用
|
||||
mvn spring-boot:run
|
||||
|
||||
# 或打包后运行
|
||||
mvn package
|
||||
java -jar target/stock-platform-backend-1.0.0.jar
|
||||
```
|
||||
|
||||
### 4. Docker部署
|
||||
|
||||
**Dockerfile:**
|
||||
```dockerfile
|
||||
FROM openjdk:8-jre-alpine
|
||||
VOLUME /tmp
|
||||
COPY target/stock-platform-backend-1.0.0.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
|
||||
```
|
||||
|
||||
**构建镜像:**
|
||||
```bash
|
||||
# 构建Docker镜像
|
||||
docker build -t agricultural-stock-backend .
|
||||
|
||||
# 运行容器
|
||||
docker run -d -p 8080:8080 \
|
||||
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/agricultural_stock \
|
||||
-e SPRING_REDIS_HOST=redis \
|
||||
--name backend agricultural-stock-backend
|
||||
```
|
||||
|
||||
## 📊 API文档
|
||||
|
||||
### Swagger UI访问
|
||||
|
||||
启动应用后,可通过以下地址访问API文档:
|
||||
- **Swagger UI:** http://localhost:8080/swagger-ui/index.html
|
||||
- **OpenAPI JSON:** http://localhost:8080/v3/api-docs
|
||||
|
||||
|
||||
## 📈 性能优化
|
||||
|
||||
### 1. 数据库优化
|
||||
|
||||
**连接池配置:**
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
hikari:
|
||||
minimum-idle: 5 # 最小空闲连接
|
||||
maximum-pool-size: 20 # 最大连接池大小
|
||||
max-lifetime: 1800000 # 连接最大生存时间
|
||||
connection-timeout: 30000 # 连接超时时间
|
||||
```
|
||||
|
||||
**索引优化:**
|
||||
```sql
|
||||
# 股票代码索引
|
||||
CREATE INDEX idx_stock_code ON stock_data(stock_code);
|
||||
|
||||
# 交易日期索引
|
||||
CREATE INDEX idx_trade_date ON stock_data(trade_date);
|
||||
|
||||
# 复合索引
|
||||
CREATE INDEX idx_stock_trade ON stock_data(stock_code, trade_date);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## 📋 监控与维护
|
||||
|
||||
### 1. 健康检查
|
||||
**健康检查接口:**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/health")
|
||||
public class HealthController {
|
||||
@GetMapping("/check")
|
||||
public Result<Map<String, Object>> healthCheck() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
status.put("status", "UP");
|
||||
status.put("timestamp", System.currentTimeMillis());
|
||||
status.put("database", checkDatabase());
|
||||
status.put("redis", checkRedis());
|
||||
return Result.success(status);
|
||||
}
|
||||
}
|
||||
```
|
||||
### 2. 日志管理
|
||||
|
||||
**日志配置示例:**
|
||||
```yaml
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.agricultural.stock: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: logs/backend.log
|
||||
max-size: 100MB
|
||||
max-history: 30
|
||||
```
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本模块采用 MIT 许可证,详见项目根目录 LICENSE 文件。
|
||||
|
||||
---
|
||||
|
||||
**联系信息:**
|
||||
- 项目仓库: [Agricultural Stock Platform](https://github.com/agricultural-stock-platform)
|
||||
- 技术支持: support@agricultural-stock.com
|
||||
- 开发团队: Agricultural Stock Platform Team
|
||||
179
docs/public/docs/zh-cn/guide/data-collector.md
Normal file
179
docs/public/docs/zh-cn/guide/data-collector.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# 📊 Python数据采集器文档
|
||||
|
||||
## 📋 模块概述
|
||||
|
||||
**data-collector** 是农业股票数据分析系统的数据采集模块,基于Python 3.8+构建,负责从外部数据源实时采集农业类上市公司的股票数据,并将数据存储到MySQL数据库中供后续分析使用。
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 核心技术栈
|
||||
- **Python 3.8+** - 编程语言
|
||||
- **Requests 2.31.0** - HTTP请求库
|
||||
- **Pandas 2.1.0** - 数据处理库
|
||||
- **BeautifulSoup4 4.12.2** - HTML解析库
|
||||
- **MySQL Connector 8.1.0** - MySQL数据库连接器
|
||||
- **Schedule 1.2.0** - 任务调度库
|
||||
- **NumPy 1.26.0** - 数值计算库
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
data-collector/
|
||||
├── stock_crawler.py # 主要爬虫程序
|
||||
├── config.ini # 配置文件
|
||||
├── requirements.txt # Python依赖
|
||||
└── stock_crawler.log # 运行日志
|
||||
```
|
||||
|
||||
## 🔧 核心功能
|
||||
|
||||
### 1. 股票数据采集
|
||||
|
||||
支持从腾讯财经API采集以下农业股票数据:
|
||||
- 新希望 (sz000876)
|
||||
- 牧原股份 (sz002714)
|
||||
- 贵州茅台 (sh600519)
|
||||
- 五粮液 (sz000858)
|
||||
- 伊利股份 (sh600887)
|
||||
- 洋河股份 (sz002304)
|
||||
|
||||
### 2. 数据处理功能
|
||||
|
||||
- 实时价格数据获取
|
||||
- 涨跌幅自动计算
|
||||
- 成交额计算
|
||||
- 数据格式标准化
|
||||
- 异常数据过滤
|
||||
|
||||
### 3. 数据库存储
|
||||
|
||||
- MySQL批量写入
|
||||
- 重复数据更新
|
||||
- 事务管理
|
||||
- 连接池管理
|
||||
|
||||
### 4. 任务调度
|
||||
|
||||
- 定时数据采集
|
||||
- 错误重试机制
|
||||
- 日志记录
|
||||
- 状态监控
|
||||
|
||||
## ⚙️ 配置管理
|
||||
|
||||
### config.ini 配置文件
|
||||
|
||||
```ini
|
||||
[database]
|
||||
host = localhost
|
||||
port = 3306
|
||||
user = root
|
||||
password = 123456
|
||||
database = agricultural_stock
|
||||
|
||||
[crawler]
|
||||
request_interval = 0.5
|
||||
request_timeout = 10
|
||||
max_retries = 3
|
||||
user_agent = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
|
||||
|
||||
[logging]
|
||||
level = INFO
|
||||
file = stock_crawler.log
|
||||
```
|
||||
|
||||
## 🚀 部署与运行
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
```bash
|
||||
# 检查Python版本
|
||||
python --version # 需要 3.8+
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 配置设置
|
||||
|
||||
```bash
|
||||
# 编辑配置文件
|
||||
vim config.ini
|
||||
|
||||
# 配置数据库连接信息
|
||||
[database]
|
||||
host = localhost
|
||||
port = 3306
|
||||
user = your_username
|
||||
password = your_password
|
||||
database = agricultural_stock
|
||||
```
|
||||
|
||||
### 3. 运行方式
|
||||
|
||||
```bash
|
||||
# 执行一次数据采集
|
||||
python stock_crawler.py
|
||||
|
||||
# 后台运行
|
||||
nohup python stock_crawler.py > /dev/null 2>&1 &
|
||||
```
|
||||
|
||||
## 📊 数据质量保证
|
||||
|
||||
### 数据验证机制
|
||||
|
||||
- 必填字段完整性检查
|
||||
- 价格数据合理性验证
|
||||
- 涨跌幅异常值检测
|
||||
- 成交量负值过滤
|
||||
|
||||
### 异常处理
|
||||
|
||||
- 网络请求重试机制
|
||||
- 数据库连接故障恢复
|
||||
- 错误日志详细记录
|
||||
- 优雅异常处理
|
||||
|
||||
## 📈 监控与维护
|
||||
|
||||
### 日志管理
|
||||
|
||||
系统自动记录以下信息:
|
||||
- 数据采集开始/结束时间
|
||||
- 成功采集的股票数量
|
||||
- 失败的股票代码和原因
|
||||
- 数据库操作结果
|
||||
- 异常错误详情
|
||||
|
||||
### 性能监控
|
||||
|
||||
- 采集耗时统计
|
||||
- 成功率监控
|
||||
- 错误率统计
|
||||
- 数据库性能监控
|
||||
|
||||
## 📋 依赖管理
|
||||
|
||||
### requirements.txt
|
||||
|
||||
```txt
|
||||
requests>=2.31.0
|
||||
pandas>=2.1.0
|
||||
beautifulsoup4>=4.12.2
|
||||
lxml>=4.9.3
|
||||
mysql-connector-python>=8.1.0
|
||||
schedule>=1.2.0
|
||||
numpy>=1.26.0
|
||||
```
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本模块采用 MIT 许可证,详见项目根目录 LICENSE 文件。
|
||||
|
||||
---
|
||||
|
||||
**联系信息:**
|
||||
- 项目仓库: [Agricultural Stock Platform](https://github.com/agricultural-stock-platform)
|
||||
- 技术支持: support@agricultural-stock.com
|
||||
- 开发团队: Agricultural Stock Platform Team
|
||||
248
docs/public/docs/zh-cn/guide/frontend.md
Normal file
248
docs/public/docs/zh-cn/guide/frontend.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 🎨 Vue.js前端界面文档
|
||||
|
||||
## 📋 模块概述
|
||||
|
||||
**frontend** 是农业股票数据分析系统的前端用户界面模块,基于Vue.js 3.3.0构建,提供现代化的响应式Web界面,支持实时数据展示、交互式图表、股票分析和市场监控功能。
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 核心技术栈
|
||||
- **Vue 3.3.0** - 渐进式JavaScript框架
|
||||
- **Vue Router 4.2.0** - 官方路由管理器
|
||||
- **Vuex 4.0.2** - 状态管理模式
|
||||
- **Element Plus 2.3.0** - Vue 3组件库
|
||||
- **ECharts 5.4.0** - 数据可视化图表库
|
||||
- **Axios 1.4.0** - HTTP客户端
|
||||
- **Vite 4.3.0** - 构建工具
|
||||
- **SASS 1.62.0** - CSS预处理器
|
||||
- **WebSocket** - 实时数据通信
|
||||
|
||||
### 系统架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Vue.js 前端应用 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
|
||||
│ │ 视图层 │ │ 状态管理 │ │ 路由管理 │ │
|
||||
│ │ Views │ │ Vuex │ │ Vue Router │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 仪表板页面 │ │ • 股票数据状态 │ │ • 路由配置 │ │
|
||||
│ │ • 市场分析页面 │ │ • 用户界面状态 │ │ • 导航守卫 │ │
|
||||
│ │ • 股票详情页面 │ │ • 异步操作 │ │ • 懒加载 │ │
|
||||
│ │ • 数据管理页面 │ │ • 缓存管理 │ │ • 嵌套路由 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
|
||||
│ │ 组件层 │ │ 服务层 │ │ 工具层 │ │
|
||||
│ │ Components │ │ Services │ │ Utils │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 图表组件 │ │ • API接口 │ │ • 工具函数 │ │
|
||||
│ │ • 表格组件 │ │ • WebSocket │ │ • 常量定义 │ │
|
||||
│ │ • 搜索组件 │ │ • 数据处理 │ │ • 格式化 │ │
|
||||
│ │ • 排行榜组件 │ │ • 错误处理 │ │ • 验证器 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── public/
|
||||
│ ├── index.html # 入口HTML文件
|
||||
│ └── favicon.ico # 网站图标
|
||||
├── src/
|
||||
│ ├── main.js # 应用入口文件
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── views/ # 页面视图
|
||||
│ │ ├── Dashboard.vue # 仪表板页面
|
||||
│ │ ├── MarketAnalysis.vue # 市场分析页面
|
||||
│ │ ├── StockDetail.vue # 股票详情页面
|
||||
│ │ ├── StockSearch.vue # 股票搜索页面
|
||||
│ │ ├── Rankings.vue # 排行榜页面
|
||||
│ │ ├── DataManagement.vue # 数据管理页面
|
||||
│ │ └── HealthCheck.vue # 健康检查页面
|
||||
│ ├── components/ # 可复用组件
|
||||
│ │ └── MarketOverview.vue # 市场总览组件
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ └── index.js # 路由定义
|
||||
│ ├── store/ # 状态管理
|
||||
│ │ ├── index.js # Vuex store
|
||||
│ │ └── modules/ # 模块化store
|
||||
│ ├── api/ # API接口
|
||||
│ │ ├── stock.js # 股票数据API
|
||||
│ │ └── market.js # 市场分析API
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── request.js # HTTP请求封装
|
||||
│ │ ├── format.js # 数据格式化
|
||||
│ │ └── constants.js # 常量定义
|
||||
│ └── styles/ # 样式文件
|
||||
│ ├── main.scss # 主样式文件
|
||||
│ └── variables.scss # 样式变量
|
||||
├── package.json # 项目依赖配置
|
||||
├── vite.config.js # Vite构建配置
|
||||
└── .eslintrc.js # ESLint配置
|
||||
```
|
||||
|
||||
## 🔧 核心功能模块
|
||||
|
||||
### 1. 仪表板页面 (Dashboard.vue)
|
||||
|
||||
**功能描述:** 系统主页面,提供股票市场的整体概览和关键指标展示。
|
||||
|
||||
**主要功能:**
|
||||
- 实时市场数据展示
|
||||
- 涨跌分布统计图表
|
||||
- 成交量趋势分析
|
||||
- 热门股票快速入口
|
||||
- 市场新闻资讯展示
|
||||
|
||||
|
||||
### 2. 市场分析页面 (MarketAnalysis.vue)
|
||||
|
||||
**功能描述:** 提供深度的市场分析功能,包括技术指标、趋势分析和预测模型。
|
||||
|
||||
**主要功能:**
|
||||
- K线图表展示
|
||||
- 技术指标分析(MA、RSI、MACD)
|
||||
- 市场热力图
|
||||
- 行业对比分析
|
||||
- 历史数据回测
|
||||
|
||||
### 3. 股票详情页面 (StockDetail.vue)
|
||||
|
||||
**功能描述:** 展示单个股票的详细信息,包括实时价格、历史走势和基本面分析。
|
||||
|
||||
**主要功能:**
|
||||
- 实时价格展示
|
||||
- 历史价格走势图
|
||||
- 成交量分析
|
||||
- 基本面数据(PE、PB、市值等)
|
||||
- 相关新闻和公告
|
||||
|
||||
|
||||
|
||||
### 4. 股票搜索页面 (StockSearch.vue)
|
||||
|
||||
**功能描述:** 提供智能的股票搜索功能,支持多种搜索条件和筛选器。
|
||||
|
||||
**主要功能:**
|
||||
- 关键词搜索(股票代码、名称)
|
||||
- 高级筛选(价格区间、市值、行业)
|
||||
- 搜索结果排序
|
||||
- 收藏夹功能
|
||||
- 搜索历史记录
|
||||
|
||||
|
||||
### 5. 排行榜页面 (Rankings.vue)
|
||||
|
||||
**功能描述:** 展示各类股票排行榜,帮助用户快速发现市场热点。
|
||||
|
||||
**主要功能:**
|
||||
- 涨幅排行榜
|
||||
- 跌幅排行榜
|
||||
- 成交量排行榜
|
||||
- 市值排行榜
|
||||
- 换手率排行榜
|
||||
|
||||
### 6. 数据管理页面 (DataManagement.vue)
|
||||
|
||||
**功能描述:** 为管理员提供数据管理功能,包括数据导入、导出和系统监控。
|
||||
|
||||
**主要功能:**
|
||||
- 股票数据批量导入
|
||||
- 历史数据导出
|
||||
- 数据同步状态监控
|
||||
- 系统健康检查
|
||||
- 错误日志查看
|
||||
|
||||
|
||||
## 🚀 部署与运行
|
||||
|
||||
### 1. 开发环境
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 代码格式化
|
||||
npm run format
|
||||
```
|
||||
|
||||
### 2. 生产构建
|
||||
|
||||
```bash
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 预览构建结果
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### 3. Docker部署
|
||||
|
||||
**Dockerfile:**
|
||||
```dockerfile
|
||||
# 构建阶段
|
||||
FROM node:16-alpine as builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# 生产阶段
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
**nginx.conf:**
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本模块采用 MIT 许可证,详见项目根目录 LICENSE 文件。
|
||||
|
||||
---
|
||||
|
||||
**联系信息:**
|
||||
- 项目仓库: [Agricultural Stock Platform](https://github.com/agricultural-stock-platform)
|
||||
- 技术支持: support@agricultural-stock.com
|
||||
- 开发团队: Agricultural Stock Platform Team
|
||||
422
docs/public/docs/zh-cn/guide/spark-processor.md
Normal file
422
docs/public/docs/zh-cn/guide/spark-processor.md
Normal file
@@ -0,0 +1,422 @@
|
||||
# 🌾 Spark数据处理模块文档
|
||||
|
||||
## 📋 模块概述
|
||||
|
||||
**spark-processor** 是农业股票数据分析系统的核心数据处理引擎,基于Apache Spark 3.4.0构建,负责对农业相关上市公司的股票数据进行大规模处理、技术分析和市场分析。
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 核心技术栈
|
||||
- **Apache Spark 3.4.0** - 分布式大数据处理框架
|
||||
- **Spark SQL** - 结构化数据查询与处理
|
||||
- **Spark Streaming** - 实时流数据处理
|
||||
- **MySQL 8.0** - 数据存储与持久化
|
||||
- **Java 8** - 编程语言
|
||||
- **Maven 3.6+** - 项目构建管理
|
||||
|
||||
### 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Spark数据处理模块 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
|
||||
│ │ 数据清洗服务 │ │ 技术指标服务 │ │ 市场分析服务 │ │
|
||||
│ │ DataCleaning │ │ TechnicalIndica │ │ MarketAnaly │ │
|
||||
│ │ Service │ │ torService │ │ sisService │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 数据库保存服务 │ │ 配置管理 │ │
|
||||
│ │ DatabaseSave │ │ SparkConfig │ │
|
||||
│ │ Service │ │ │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ StockDataProcessor (主处理类) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
spark-processor/
|
||||
├── src/main/java/com/agricultural/spark/
|
||||
│ ├── StockDataProcessor.java # 主处理类
|
||||
│ ├── config/
|
||||
│ │ └── SparkConfig.java # 配置管理类
|
||||
│ └── service/
|
||||
│ ├── DataCleaningService.java # 数据清洗服务
|
||||
│ ├── TechnicalIndicatorService.java # 技术指标计算服务
|
||||
│ ├── MarketAnalysisService.java # 市场分析服务
|
||||
│ └── DatabaseSaveService.java # 数据库保存服务
|
||||
├── src/main/resources/
|
||||
│ ├── application.conf # 应用配置文件
|
||||
│ └── logback.xml # 日志配置
|
||||
├── database_tables.sql # 数据库表结构
|
||||
├── pom.xml # Maven配置文件
|
||||
└── README_DATABASE.md # 数据库文档
|
||||
```
|
||||
|
||||
## 🔧 核心功能模块
|
||||
|
||||
### 1. 数据处理主类 (StockDataProcessor)
|
||||
|
||||
**功能描述:** 系统的核心调度器,负责协调各个服务模块的工作流程。
|
||||
|
||||
**主要职责:**
|
||||
- Spark会话管理与初始化
|
||||
- 数据加载与预处理协调
|
||||
- 批处理与流处理模式切换
|
||||
- 异常处理与资源管理
|
||||
|
||||
**关键方法:**
|
||||
```java
|
||||
// 批处理模式
|
||||
public void runBatchProcessing()
|
||||
|
||||
// 流处理模式
|
||||
public void runStreamProcessing()
|
||||
|
||||
// 从MySQL加载数据
|
||||
public Dataset<Row> loadStockDataFromMySQL()
|
||||
```
|
||||
|
||||
### 2. 数据清洗服务 (DataCleaningService)
|
||||
|
||||
**功能描述:** 对原始股票数据进行标准化清洗和预处理。
|
||||
|
||||
**处理流程:**
|
||||
1. **去重处理** - 基于股票代码和交易日期去除重复记录
|
||||
2. **缺失值处理** - 数值字段填充0,字符串字段填充空值
|
||||
3. **数据类型转换** - 确保字段类型正确性
|
||||
4. **异常值检测** - 识别并处理价格、成交量等异常数据
|
||||
5. **派生字段计算** - 计算涨跌幅、市值等衍生指标
|
||||
|
||||
**关键功能:**
|
||||
```java
|
||||
// 主要清洗方法
|
||||
public Dataset<Row> cleanData(Dataset<Row> rawData)
|
||||
|
||||
// 缺失值处理
|
||||
private Dataset<Row> handleMissingValues(Dataset<Row> data)
|
||||
|
||||
// 异常值处理
|
||||
private Dataset<Row> handleOutliers(Dataset<Row> data)
|
||||
```
|
||||
|
||||
### 3. 技术指标服务 (TechnicalIndicatorService)
|
||||
|
||||
**功能描述:** 计算各种技术分析指标,为量化分析提供数据支撑。
|
||||
|
||||
**支持的技术指标:**
|
||||
- **移动平均线 (MA)** - MA5, MA10, MA20, MA30
|
||||
- **相对强弱指数 (RSI)** - 14日RSI指标
|
||||
- **MACD指标** - 快线、慢线、柱状图
|
||||
- **布林带 (Bollinger Bands)** - 上轨、中轨、下轨
|
||||
- **成交量移动平均** - 成交量技术分析
|
||||
|
||||
**计算方法:**
|
||||
```java
|
||||
// 计算技术指标
|
||||
public Dataset<Row> calculateTechnicalIndicators(Dataset<Row> data)
|
||||
|
||||
// 移动平均线计算
|
||||
private Dataset<Row> calculateMovingAverages(Dataset<Row> data)
|
||||
|
||||
// RSI指标计算
|
||||
private Dataset<Row> calculateRSI(Dataset<Row> data)
|
||||
```
|
||||
|
||||
### 4. 市场分析服务 (MarketAnalysisService)
|
||||
|
||||
**功能描述:** 从宏观角度分析市场整体表现和行业分布。
|
||||
|
||||
**分析维度:**
|
||||
- **市场总览** - 涨跌统计、平均涨跌幅、总市值
|
||||
- **行业表现** - 按行业分类的股票表现统计
|
||||
- **市场趋势** - 历史走势分析和预测
|
||||
- **板块轮动** - 不同板块间的资金流向分析
|
||||
|
||||
**核心分析:**
|
||||
```java
|
||||
// 市场总览分析
|
||||
public Map<String, Object> analyzeMarketOverview(Dataset<Row> data)
|
||||
|
||||
// 行业表现分析
|
||||
public Dataset<Row> analyzeIndustryPerformance(Dataset<Row> data)
|
||||
|
||||
// 市场趋势分析
|
||||
public Dataset<Row> analyzeTrends(Dataset<Row> data)
|
||||
```
|
||||
|
||||
### 5. 数据库保存服务 (DatabaseSaveService)
|
||||
|
||||
**功能描述:** 将Spark处理后的结果数据持久化到MySQL数据库。
|
||||
|
||||
**保存策略:**
|
||||
- **增量更新** - 仅保存新增或变更的数据
|
||||
- **批量写入** - 提高数据写入效率
|
||||
- **事务管理** - 保证数据一致性
|
||||
- **连接池管理** - 优化数据库连接性能
|
||||
|
||||
**主要功能:**
|
||||
```java
|
||||
// 保存市场分析结果
|
||||
public void saveMarketAnalysis(Map<String, Object> analysisResult)
|
||||
|
||||
// 保存技术指标数据
|
||||
public void saveTechnicalIndicators(Dataset<Row> indicators)
|
||||
|
||||
// 批量保存处理结果
|
||||
public void batchSaveResults(List<Dataset<Row>> datasets)
|
||||
```
|
||||
|
||||
## 🗄️ 数据库设计
|
||||
|
||||
### 主要数据表
|
||||
|
||||
| 表名 | 描述 | 主要字段 |
|
||||
|------|------|----------|
|
||||
| `stock_data` | 股票基础数据 | stock_code, stock_name, open_price, close_price, volume, trade_date |
|
||||
| `market_analysis` | 市场分析结果 | analysis_date, up_count, down_count, total_market_cap, avg_change_percent |
|
||||
| `stock_technical_indicators` | 技术指标数据 | stock_code, trade_date, ma5, ma10, ma20, rsi, macd |
|
||||
| `industry_analysis` | 行业分析数据 | industry_name, stock_count, avg_change_percent, total_market_cap |
|
||||
| `market_trends` | 市场趋势数据 | trade_date, avg_price, total_volume, stock_count |
|
||||
|
||||
## ⚙️ 配置管理
|
||||
|
||||
### 配置文件 (application.conf)
|
||||
|
||||
```hocon
|
||||
# Spark配置
|
||||
spark {
|
||||
master = "local[*]"
|
||||
app.name = "AgriculturalStockDataProcessor"
|
||||
}
|
||||
|
||||
# MySQL数据库配置
|
||||
mysql {
|
||||
host = "localhost"
|
||||
port = 3306
|
||||
database = "agricultural_stock"
|
||||
user = "root"
|
||||
password = "your_password"
|
||||
}
|
||||
|
||||
# 输出路径配置
|
||||
output {
|
||||
path = "/tmp/spark-output"
|
||||
}
|
||||
```
|
||||
|
||||
### 环境配置说明
|
||||
|
||||
**开发环境:**
|
||||
- Spark Master: `local[*]` (使用所有可用CPU核心)
|
||||
- 内存设置: 建议4GB以上
|
||||
- MySQL: 本地实例
|
||||
|
||||
**生产环境:**
|
||||
- Spark Master: `spark://master:7077` (集群模式)
|
||||
- 内存设置: 根据数据量调整
|
||||
- MySQL: 高可用集群
|
||||
|
||||
## 🚀 部署与运行
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
```bash
|
||||
# 检查Java版本
|
||||
java -version
|
||||
|
||||
# 检查Maven版本
|
||||
mvn -version
|
||||
|
||||
# 启动MySQL服务
|
||||
systemctl start mysql
|
||||
```
|
||||
|
||||
### 2. 编译打包
|
||||
|
||||
```bash
|
||||
# 进入项目目录
|
||||
cd spark-processor
|
||||
|
||||
# 清理并编译
|
||||
mvn clean compile
|
||||
|
||||
# 打包为可执行JAR
|
||||
mvn package
|
||||
```
|
||||
|
||||
### 3. 运行方式
|
||||
|
||||
**批处理模式:**
|
||||
```bash
|
||||
# 方式1: 使用Maven运行
|
||||
mvn exec:java -Dexec.mainClass="com.agricultural.spark.StockDataProcessor"
|
||||
|
||||
# 方式2: 使用JAR包运行
|
||||
java -jar target/spark-data-processor-1.0.0.jar
|
||||
```
|
||||
|
||||
**流处理模式:**
|
||||
```bash
|
||||
# 启动流处理
|
||||
java -jar target/spark-data-processor-1.0.0.jar stream
|
||||
```
|
||||
|
||||
### 4. 集群部署
|
||||
|
||||
```bash
|
||||
# 提交到Spark集群
|
||||
spark-submit \
|
||||
--class com.agricultural.spark.StockDataProcessor \
|
||||
--master spark://master:7077 \
|
||||
--executor-memory 2g \
|
||||
--total-executor-cores 4 \
|
||||
target/spark-data-processor-1.0.0.jar
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### Spark优化配置
|
||||
|
||||
```java
|
||||
SparkSession spark = SparkSession.builder()
|
||||
.appName("AgriculturalStockDataProcessor")
|
||||
.config("spark.sql.adaptive.enabled", "true") // 启用自适应查询
|
||||
.config("spark.sql.adaptive.coalescePartitions.enabled", "true") // 分区合并
|
||||
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer") // 序列化优化
|
||||
.config("spark.sql.shuffle.partitions", "200") // 调整分区数
|
||||
.getOrCreate();
|
||||
```
|
||||
|
||||
### 数据处理优化策略
|
||||
|
||||
1. **分区策略**
|
||||
- 按交易日期分区,提高查询效率
|
||||
- 控制分区大小在128MB-1GB之间
|
||||
|
||||
2. **缓存策略**
|
||||
- 对频繁使用的数据集进行缓存
|
||||
- 使用`MEMORY_AND_DISK`存储级别
|
||||
|
||||
3. **SQL优化**
|
||||
- 使用列式存储减少I/O
|
||||
- 谓词下推减少数据传输
|
||||
|
||||
## 📈 监控与日志
|
||||
|
||||
### 日志配置
|
||||
|
||||
**logback.xml配置:**
|
||||
```xml
|
||||
<configuration>
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>logs/spark-processor.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.agricultural.spark" level="INFO"/>
|
||||
<root level="WARN">
|
||||
<appender-ref ref="FILE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
### 性能监控指标
|
||||
|
||||
- **处理速度:** 每秒处理记录数
|
||||
- **内存使用:** 堆内存和Off-heap内存使用情况
|
||||
- **任务执行时间:** 各阶段耗时统计
|
||||
- **错误率:** 数据处理错误比例
|
||||
|
||||
## 🔧 故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
**1. 内存不足 (OutOfMemoryError)**
|
||||
```bash
|
||||
# 解决方案:增加executor内存
|
||||
--executor-memory 4g --driver-memory 2g
|
||||
```
|
||||
|
||||
**2. 连接超时**
|
||||
```bash
|
||||
# 检查数据库连接
|
||||
telnet localhost 3306
|
||||
# 调整超时配置
|
||||
--conf spark.sql.execution.arrow.pyspark.enabled=false
|
||||
```
|
||||
|
||||
**3. 数据倾斜**
|
||||
```java
|
||||
// 使用salting技术解决
|
||||
data.withColumn("salt", rand().multiply(100).cast("int"))
|
||||
```
|
||||
|
||||
## 📋 API接口
|
||||
|
||||
### 核心处理接口
|
||||
|
||||
**批量数据处理:**
|
||||
```java
|
||||
// 处理全量历史数据
|
||||
processor.runBatchProcessing();
|
||||
|
||||
// 处理增量数据
|
||||
processor.processIncrementalData(startDate, endDate);
|
||||
```
|
||||
|
||||
**实时数据处理:**
|
||||
```java
|
||||
// 启动流处理
|
||||
processor.runStreamProcessing();
|
||||
|
||||
// 停止流处理
|
||||
processor.stopStreamProcessing();
|
||||
```
|
||||
|
||||
## 🤝 开发指南
|
||||
|
||||
### 添加新的技术指标
|
||||
|
||||
1. **在TechnicalIndicatorService中添加计算方法**
|
||||
```java
|
||||
private Dataset<Row> calculateNewIndicator(Dataset<Row> data) {
|
||||
// 指标计算逻辑
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
2. **在主流程中调用**
|
||||
```java
|
||||
Dataset<Row> withIndicators = technicalIndicatorService
|
||||
.calculateNewIndicator(cleanedData);
|
||||
```
|
||||
|
||||
3. **更新数据库表结构**
|
||||
```sql
|
||||
ALTER TABLE stock_technical_indicators
|
||||
ADD COLUMN new_indicator DECIMAL(10,2);
|
||||
```
|
||||
|
||||
### 扩展市场分析功能
|
||||
|
||||
1. **实现新的分析算法**
|
||||
2. **添加相应的数据存储表**
|
||||
3. **更新保存服务方法**
|
||||
4. **编写单元测试**
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本模块采用 MIT 许可证,详见项目根目录 LICENSE 文件。
|
||||
|
||||
---
|
||||
|
||||
**联系信息:**
|
||||
- 项目仓库: [Agricultural Stock Platform](https://github.com/agricultural-stock-platform)
|
||||
- 技术支持: support@agricultural-stock.com
|
||||
- 开发团队: Agricultural Stock Platform Team
|
||||
58
docs/public/docs/zh-cn/index.md
Normal file
58
docs/public/docs/zh-cn/index.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 欢迎使用 Vue Docs UI
|
||||
|
||||
Vue Docs UI 是一个现代化的文档网站构建工具,基于 Vue 3 开发,提供开箱即用的文档解决方案。
|
||||
|
||||
## 🌟 主要特性
|
||||
|
||||
- **🚀 开箱即用** - 只需 3 行代码即可启动文档网站
|
||||
- **🎨 现代设计** - 精美的界面设计,支持明暗主题切换
|
||||
- **📱 移动端适配** - 完美的响应式设计
|
||||
- **🌐 国际化支持** - 内置多语言支持
|
||||
- **🤖 AI 助手** - 集成 AI 聊天助手,支持多种模型
|
||||
- **⚡ 高性能** - 基于 Vite 构建,快速热重载
|
||||
- **🔍 全文搜索** - 智能搜索功能
|
||||
- **📝 Markdown 增强** - 丰富的 Markdown 扩展
|
||||
|
||||
## 🏗️ 架构特点
|
||||
|
||||
- **组件化设计** - 模块化组件,易于扩展
|
||||
- **TypeScript 支持** - 完整的类型支持
|
||||
- **可自定义主题** - 灵活的主题配置
|
||||
- **插件系统** - 可扩展的插件架构
|
||||
|
||||
## 📦 快速开始
|
||||
|
||||
```bash
|
||||
# 创建新项目
|
||||
npm create vue-docs-ui@latest my-docs
|
||||
|
||||
# 进入项目目录
|
||||
cd my-docs
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 📖 使用方式
|
||||
|
||||
```javascript
|
||||
import { createDocsApp } from 'vue-docs-ui'
|
||||
import 'vue-docs-ui/dist/vue-docs-ui.css'
|
||||
|
||||
createDocsApp({
|
||||
configPath: '/config/site.yaml',
|
||||
el: '#app'
|
||||
})
|
||||
```
|
||||
|
||||
就这么简单!无需复杂配置,立即拥有一个功能完整的文档网站。
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [GitHub 仓库](https://github.com/shenjianZ/vue-docs-ui)
|
||||
- [在线演示](https://vue-docs-ui.example.com)
|
||||
- [使用指南](/guide/introduction)
|
||||
- [API 文档](/advanced/api)
|
||||
BIN
docs/public/images/logo.png
Normal file
BIN
docs/public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
25
docs/public/images/placeholder.md
Normal file
25
docs/public/images/placeholder.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 图片目录
|
||||
|
||||
将您的图片文件放在这个目录中。
|
||||
|
||||
支持的格式:
|
||||
- PNG
|
||||
- JPG/JPEG
|
||||
- SVG
|
||||
- WebP
|
||||
- GIF
|
||||
|
||||
## 使用方式
|
||||
|
||||
在 Markdown 中引用图片:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
在配置文件中使用:
|
||||
|
||||
```yaml
|
||||
site:
|
||||
logo: "/images/logo.png"
|
||||
```
|
||||
29
docs/src/main.ts
Normal file
29
docs/src/main.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// 开箱即用示例 - 仅需3行代码!
|
||||
import { createDocsApp } from 'vue-docs-ui'
|
||||
import 'vue-docs-ui/dist/vue-docs-ui.css'
|
||||
// import createDocsApp from '../../../vue-docs-ui/dist/vue-docs-ui.es.js'
|
||||
// import '../../../vue-docs-ui/dist/vue-docs-ui.css'
|
||||
// 等待DOM加载完成后再启动应用
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 启动文档应用
|
||||
createDocsApp({
|
||||
configPath: '/config/site.yaml',
|
||||
el: '#app'
|
||||
}).catch(error => {
|
||||
console.error('启动文档应用失败:', error)
|
||||
// 显示错误信息
|
||||
const app = document.getElementById('app')
|
||||
if (app) {
|
||||
app.innerHTML = `
|
||||
<div style="padding: 2rem; text-align: center; color: #ef4444;">
|
||||
<h2>❌ 加载失败</h2>
|
||||
<p>${error.message || '未知错误'}</p>
|
||||
<button onclick="location.reload()" style="margin-top: 1rem; padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;">重新加载</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 就这么简单!无需创建任何组件
|
||||
|
||||
46
docs/vite.config.js
Normal file
46
docs/vite.config.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
// 使用包含编译器的Vue版本以支持运行时模板编译
|
||||
'vue': 'vue/dist/vue.esm-bundler.js'
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
port: 5173,
|
||||
// 配置响应头,确保所有文件都使用UTF-8编码
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
const url = req.url || ''
|
||||
|
||||
// 为不同文件类型设置正确的Content-Type和charset
|
||||
if (url.match(/\.md$/)) {
|
||||
res.setHeader('Content-Type', 'text/markdown; charset=utf-8')
|
||||
} else if (url.match(/\.(yaml|yml)$/)) {
|
||||
res.setHeader('Content-Type', 'text/yaml; charset=utf-8')
|
||||
} else if (url.match(/\.json$/)) {
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
} else if (url.match(/\.js$/)) {
|
||||
res.setHeader('Content-Type', 'application/javascript; charset=utf-8')
|
||||
} else if (url.match(/\.ts$/)) {
|
||||
res.setHeader('Content-Type', 'text/typescript; charset=utf-8')
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: './index.html'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
15
frontend/index.html
Normal file
15
frontend/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>农业股票数据分析系统</title>
|
||||
<meta name="description" content="基于Spark大数据处理的农业股票市场监控与分析平台" />
|
||||
<meta name="keywords" content="农业股票,大数据,Spark,数据分析,股票监控,Vue.js" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
3878
frontend/package-lock.json
generated
Normal file
3878
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
frontend/package.json
Normal file
41
frontend/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "agricultural-stock-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "农业领域上市公司行情可视化监控平台前端",
|
||||
"author": "Agricultural Stock Platform Team",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vuex": "^4.0.2",
|
||||
"element-plus": "^2.3.0",
|
||||
"echarts": "^5.4.0",
|
||||
"axios": "^1.4.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"sockjs-client": "^1.6.1",
|
||||
"stompjs": "^2.3.3",
|
||||
"@element-plus/icons-vue": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.0",
|
||||
"vite": "^4.3.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-vue": "^9.11.0",
|
||||
"prettier": "^2.8.8",
|
||||
"sass": "^1.62.0",
|
||||
"unplugin-auto-import": "^0.16.0",
|
||||
"unplugin-vue-components": "^0.25.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
2
frontend/public/favicon.ico
Normal file
2
frontend/public/favicon.ico
Normal file
@@ -0,0 +1,2 @@
|
||||
# Favicon placeholder
|
||||
# This file serves as a placeholder to prevent 404 errors for favicon requests
|
||||
203
frontend/src/App.vue
Normal file
203
frontend/src/App.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="app-header">
|
||||
<div class="header-content">
|
||||
<div class="logo-section">
|
||||
<router-link to="/" class="logo">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>农业股票分析平台</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<div class="nav-menu">
|
||||
<el-menu
|
||||
:default-active="$route.path"
|
||||
mode="horizontal"
|
||||
:ellipsis="false"
|
||||
router
|
||||
>
|
||||
<el-menu-item index="/">
|
||||
<el-icon><House /></el-icon>
|
||||
<span>仪表盘</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/search">
|
||||
<el-icon><Search /></el-icon>
|
||||
<span>股票搜索</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/rankings">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
<span>排行榜</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/market-analysis">
|
||||
<el-icon><Pie /></el-icon>
|
||||
<span>市场分析</span>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/health">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>系统监控</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" size="small" @click="refreshPage">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div class="app-main">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
methods: {
|
||||
refreshPage() {
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
|
||||
.header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #303133;
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
margin: 0 40px;
|
||||
|
||||
:deep(.el-menu) {
|
||||
border-bottom: none;
|
||||
|
||||
.el-menu-item {
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-bottom-color: #409eff;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.app-header {
|
||||
.header-content {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
margin: 0 20px;
|
||||
|
||||
:deep(.el-menu) {
|
||||
.el-menu-item {
|
||||
padding: 0 10px;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
.logo {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
33
frontend/src/api/market.js
Normal file
33
frontend/src/api/market.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 市场分析数据API
|
||||
*/
|
||||
|
||||
// 获取最新的市场分析数据
|
||||
export function getLatestMarketAnalysis() {
|
||||
return request({
|
||||
url: '/api/market/latest',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取指定日期范围的市场分析数据
|
||||
export function getMarketAnalysisByDateRange(startDate, endDate) {
|
||||
return request({
|
||||
url: '/api/market/range',
|
||||
method: 'get',
|
||||
params: {
|
||||
startDate,
|
||||
endDate
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取最近N天的市场分析数据
|
||||
export function getRecentMarketAnalysis(days) {
|
||||
return request({
|
||||
url: `/api/market/recent/${days}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
105
frontend/src/api/stock.js
Normal file
105
frontend/src/api/stock.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 股票数据API
|
||||
*/
|
||||
|
||||
// 获取实时股票数据
|
||||
export function getRealtimeStockData() {
|
||||
return request({
|
||||
url: '/api/stock/realtime',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取股票历史数据
|
||||
export function getStockHistory(stockCode, startDate, endDate) {
|
||||
return request({
|
||||
url: `/api/stock/history/${stockCode}`,
|
||||
method: 'get',
|
||||
params: {
|
||||
startDate,
|
||||
endDate
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取涨幅排行榜
|
||||
export function getGrowthRanking(limit = 10) {
|
||||
return request({
|
||||
url: '/api/stock/ranking/growth',
|
||||
method: 'get',
|
||||
params: { limit }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取市值排行榜
|
||||
export function getMarketCapRanking(limit = 10) {
|
||||
return request({
|
||||
url: '/api/stock/ranking/market-cap',
|
||||
method: 'get',
|
||||
params: { limit }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取成交量排行榜
|
||||
export function getVolumeRanking(limit = 10) {
|
||||
return request({
|
||||
url: '/api/stock/ranking/volume',
|
||||
method: 'get',
|
||||
params: { limit }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取股票趋势分析
|
||||
export function getStockTrend(stockCode, days = 30) {
|
||||
return request({
|
||||
url: `/api/stock/trend/${stockCode}`,
|
||||
method: 'get',
|
||||
params: { days }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取市场综合分析
|
||||
export function getMarketAnalysis() {
|
||||
return request({
|
||||
url: '/api/stock/market-analysis',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取股票预测数据
|
||||
export function getStockPrediction(stockCode, days = 7) {
|
||||
return request({
|
||||
url: `/api/stock/prediction/${stockCode}`,
|
||||
method: 'get',
|
||||
params: { days }
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索股票
|
||||
export function searchStocks(keyword) {
|
||||
return request({
|
||||
url: '/api/stock/search',
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
})
|
||||
}
|
||||
|
||||
// 保存股票数据
|
||||
export function saveStockData(stockData) {
|
||||
return request({
|
||||
url: '/api/stock/save',
|
||||
method: 'post',
|
||||
data: stockData
|
||||
})
|
||||
}
|
||||
|
||||
// 批量保存股票数据
|
||||
export function batchSaveStockData(stockDataList) {
|
||||
return request({
|
||||
url: '/api/stock/batch-save',
|
||||
method: 'post',
|
||||
data: stockDataList
|
||||
})
|
||||
}
|
||||
560
frontend/src/components/MarketOverview.vue
Normal file
560
frontend/src/components/MarketOverview.vue
Normal file
@@ -0,0 +1,560 @@
|
||||
<template>
|
||||
<div class="market-overview">
|
||||
<!-- 市场统计卡片 -->
|
||||
<el-row :gutter="20" class="stats-cards">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card up">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ marketStats.upCount }}</div>
|
||||
<div class="stat-label">上涨股票</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card down">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ marketStats.downCount }}</div>
|
||||
<div class="stat-label">下跌股票</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card volume">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<el-icon><Histogram /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ formatVolume(marketStats.totalVolume) }}</div>
|
||||
<div class="stat-label">总成交量</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card market-cap">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ formatMarketCap(marketStats.totalMarketCap) }}</div>
|
||||
<div class="stat-label">总市值</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>市场指数走势</span>
|
||||
<el-button-group size="small">
|
||||
<el-button
|
||||
v-for="period in timePeriods"
|
||||
:key="period.value"
|
||||
:type="selectedPeriod === period.value ? 'primary' : ''"
|
||||
@click="changePeriod(period.value)"
|
||||
>
|
||||
{{ period.label }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="indexChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<span>涨跌分布</span>
|
||||
</template>
|
||||
<div ref="distributionChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<span>行业热力图</span>
|
||||
</template>
|
||||
<div ref="heatmapChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<span>成交量分析</span>
|
||||
</template>
|
||||
<div ref="volumeChart" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { stockApi } from '@/api/stock'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 响应式数据
|
||||
const marketStats = ref({
|
||||
upCount: 0,
|
||||
downCount: 0,
|
||||
totalVolume: 0,
|
||||
totalMarketCap: 0
|
||||
})
|
||||
|
||||
const selectedPeriod = ref('1d')
|
||||
const timePeriods = [
|
||||
{ label: '1日', value: '1d' },
|
||||
{ label: '1周', value: '1w' },
|
||||
{ label: '1月', value: '1m' },
|
||||
{ label: '3月', value: '3m' }
|
||||
]
|
||||
|
||||
// 图表引用
|
||||
const indexChart = ref(null)
|
||||
const distributionChart = ref(null)
|
||||
const heatmapChart = ref(null)
|
||||
const volumeChart = ref(null)
|
||||
|
||||
// 图表实例
|
||||
let indexChartInstance = null
|
||||
let distributionChartInstance = null
|
||||
let heatmapChartInstance = null
|
||||
let volumeChartInstance = null
|
||||
|
||||
// 格式化成交量
|
||||
const formatVolume = (volume) => {
|
||||
if (volume >= 100000000) {
|
||||
return (volume / 100000000).toFixed(1) + '亿'
|
||||
} else if (volume >= 10000) {
|
||||
return (volume / 10000).toFixed(1) + '万'
|
||||
}
|
||||
return volume.toString()
|
||||
}
|
||||
|
||||
// 格式化市值
|
||||
const formatMarketCap = (marketCap) => {
|
||||
if (marketCap >= 1000000000000) {
|
||||
return (marketCap / 1000000000000).toFixed(2) + '万亿'
|
||||
} else if (marketCap >= 100000000) {
|
||||
return (marketCap / 100000000).toFixed(1) + '亿'
|
||||
}
|
||||
return marketCap.toString()
|
||||
}
|
||||
|
||||
// 切换时间周期
|
||||
const changePeriod = (period) => {
|
||||
selectedPeriod.value = period
|
||||
loadIndexData()
|
||||
}
|
||||
|
||||
// 加载市场统计数据
|
||||
const loadMarketStats = async () => {
|
||||
try {
|
||||
const response = await stockApi.getMarketAnalysis()
|
||||
if (response.data) {
|
||||
const overview = response.data.marketOverview
|
||||
marketStats.value = {
|
||||
upCount: overview.upCount || 0,
|
||||
downCount: overview.downCount || 0,
|
||||
totalVolume: overview.totalVolume || 0,
|
||||
totalMarketCap: overview.totalMarketCap || 0
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载市场统计数据失败:', error)
|
||||
ElMessage.error('加载市场数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载指数数据
|
||||
const loadIndexData = async () => {
|
||||
try {
|
||||
// 模拟指数数据
|
||||
const dates = []
|
||||
const values = []
|
||||
const baseValue = 3200
|
||||
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - i)
|
||||
dates.push(date.toISOString().split('T')[0])
|
||||
|
||||
const randomChange = (Math.random() - 0.5) * 100
|
||||
values.push(baseValue + randomChange + i * 2)
|
||||
}
|
||||
|
||||
initIndexChart(dates, values)
|
||||
} catch (error) {
|
||||
console.error('加载指数数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化指数图表
|
||||
const initIndexChart = (dates, values) => {
|
||||
if (!indexChartInstance) {
|
||||
indexChartInstance = echarts.init(indexChart.value)
|
||||
}
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '农业股指数',
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 14 }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params) => {
|
||||
const data = params[0]
|
||||
return `${data.name}<br/>指数: ${data.value.toFixed(2)}`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
formatter: (value) => {
|
||||
return value.split('-').slice(1).join('/')
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '指数',
|
||||
type: 'line',
|
||||
data: values,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
lineStyle: {
|
||||
color: '#1890ff',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#1890ff'
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(24, 144, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(24, 144, 255, 0.1)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
indexChartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化分布图表
|
||||
const initDistributionChart = () => {
|
||||
if (!distributionChartInstance) {
|
||||
distributionChartInstance = echarts.init(distributionChart.value)
|
||||
}
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '涨跌分布',
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 14 }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '涨跌分布',
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
center: ['50%', '60%'],
|
||||
data: [
|
||||
{ value: marketStats.value.upCount, name: '上涨', itemStyle: { color: '#f5222d' } },
|
||||
{ value: marketStats.value.downCount, name: '下跌', itemStyle: { color: '#52c41a' } },
|
||||
{ value: 20, name: '平盘', itemStyle: { color: '#faad14' } }
|
||||
],
|
||||
label: {
|
||||
formatter: '{b}\n{c}只'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
distributionChartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化热力图
|
||||
const initHeatmapChart = () => {
|
||||
if (!heatmapChartInstance) {
|
||||
heatmapChartInstance = echarts.init(heatmapChart.value)
|
||||
}
|
||||
|
||||
// 模拟行业数据
|
||||
const industries = ['种植业', '畜牧业', '渔业', '农产品加工', '农业机械', '农业科技']
|
||||
const data = industries.map((industry, index) => ({
|
||||
name: industry,
|
||||
value: Math.random() * 10 - 5,
|
||||
children: []
|
||||
}))
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '行业涨跌幅热力图',
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 14 }
|
||||
},
|
||||
tooltip: {
|
||||
formatter: (params) => {
|
||||
return `${params.name}<br/>涨跌幅: ${params.value.toFixed(2)}%`
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '行业涨跌幅',
|
||||
type: 'treemap',
|
||||
data: data,
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}\n{c}%'
|
||||
},
|
||||
levels: [
|
||||
{
|
||||
itemStyle: {
|
||||
borderColor: '#777',
|
||||
borderWidth: 0,
|
||||
gapWidth: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
heatmapChartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化成交量图表
|
||||
const initVolumeChart = () => {
|
||||
if (!volumeChartInstance) {
|
||||
volumeChartInstance = echarts.init(volumeChart.value)
|
||||
}
|
||||
|
||||
// 模拟成交量数据
|
||||
const dates = []
|
||||
const volumes = []
|
||||
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - i)
|
||||
dates.push(date.toISOString().split('T')[0])
|
||||
volumes.push(Math.random() * 1000000000 + 500000000)
|
||||
}
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '7日成交量趋势',
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 14 }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params) => {
|
||||
const data = params[0]
|
||||
return `${data.name}<br/>成交量: ${formatVolume(data.value)}`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
formatter: (value) => {
|
||||
return value.split('-').slice(1).join('/')
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: formatVolume
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交量',
|
||||
type: 'bar',
|
||||
data: volumes,
|
||||
itemStyle: {
|
||||
color: '#722ed1'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
volumeChartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化所有图表
|
||||
const initCharts = async () => {
|
||||
await nextTick()
|
||||
await loadIndexData()
|
||||
initDistributionChart()
|
||||
initHeatmapChart()
|
||||
initVolumeChart()
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refresh = async () => {
|
||||
await loadMarketStats()
|
||||
await initCharts()
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(async () => {
|
||||
await loadMarketStats()
|
||||
await initCharts()
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', () => {
|
||||
indexChartInstance?.resize()
|
||||
distributionChartInstance?.resize()
|
||||
heatmapChartInstance?.resize()
|
||||
volumeChartInstance?.resize()
|
||||
})
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
refresh
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.market-overview {
|
||||
.stats-cards {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.stat-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
&.up {
|
||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.down {
|
||||
background: linear-gradient(135deg, #f5222d 0%, #ff4d4f 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.volume {
|
||||
background: linear-gradient(135deg, #722ed1 0%, #9254de 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.market-cap {
|
||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
font-size: 32px;
|
||||
margin-right: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
frontend/src/main.js
Normal file
24
frontend/src/main.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import './styles/index.scss'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
})
|
||||
app.use(store)
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
75
frontend/src/router/index.js
Normal file
75
frontend/src/router/index.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Dashboard from '@/views/Dashboard.vue'
|
||||
import HealthCheck from '@/views/HealthCheck.vue'
|
||||
import Rankings from '@/views/Rankings.vue'
|
||||
import StockSearch from '@/views/StockSearch.vue'
|
||||
import StockDetail from '@/views/StockDetail.vue'
|
||||
import MarketAnalysis from '@/views/MarketAnalysis.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: Dashboard,
|
||||
meta: { title: '仪表盘' }
|
||||
},
|
||||
{
|
||||
path: '/rankings',
|
||||
name: 'Rankings',
|
||||
component: Rankings,
|
||||
meta: { title: '股票排行榜' }
|
||||
},
|
||||
{
|
||||
path: '/search',
|
||||
name: 'StockSearch',
|
||||
component: StockSearch,
|
||||
meta: { title: '股票搜索' }
|
||||
},
|
||||
{
|
||||
path: '/stock/:stockCode',
|
||||
name: 'StockDetail',
|
||||
component: StockDetail,
|
||||
meta: { title: '股票详情' }
|
||||
},
|
||||
{
|
||||
path: '/stock/:stockCode/trend',
|
||||
name: 'StockTrend',
|
||||
component: StockDetail,
|
||||
meta: { title: '股票趋势' }
|
||||
},
|
||||
{
|
||||
path: '/stock/:stockCode/prediction',
|
||||
name: 'StockPrediction',
|
||||
component: StockDetail,
|
||||
meta: { title: '股票预测' }
|
||||
},
|
||||
{
|
||||
path: '/market-analysis',
|
||||
name: 'MarketAnalysis',
|
||||
component: MarketAnalysis,
|
||||
meta: { title: '市场分析' }
|
||||
},
|
||||
{
|
||||
path: '/health',
|
||||
name: 'HealthCheck',
|
||||
component: HealthCheck,
|
||||
meta: { title: '健康检查' }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL || '/'),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫 - 设置页面标题
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - 农业股票数据分析平台`
|
||||
} else {
|
||||
document.title = '农业股票数据分析平台'
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
36
frontend/src/store/index.js
Normal file
36
frontend/src/store/index.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createStore } from 'vuex'
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
marketData: {},
|
||||
recentData: [],
|
||||
loading: false
|
||||
},
|
||||
mutations: {
|
||||
SET_MARKET_DATA(state, data) {
|
||||
state.marketData = data
|
||||
},
|
||||
SET_RECENT_DATA(state, data) {
|
||||
state.recentData = data
|
||||
},
|
||||
SET_LOADING(state, loading) {
|
||||
state.loading = loading
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
updateMarketData({ commit }, data) {
|
||||
commit('SET_MARKET_DATA', data)
|
||||
},
|
||||
updateRecentData({ commit }, data) {
|
||||
commit('SET_RECENT_DATA', data)
|
||||
},
|
||||
setLoading({ commit }, loading) {
|
||||
commit('SET_LOADING', loading)
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
marketData: state => state.marketData,
|
||||
recentData: state => state.recentData,
|
||||
loading: state => state.loading
|
||||
}
|
||||
})
|
||||
50
frontend/src/styles/index.scss
Normal file
50
frontend/src/styles/index.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
@import "./variables.scss";
|
||||
|
||||
// 全局样式
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
background-color: $bg-color-page;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// Element Plus 样式覆盖
|
||||
.el-card {
|
||||
border-radius: $border-radius-base;
|
||||
box-shadow: $box-shadow-light;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
.el-table__header {
|
||||
th {
|
||||
background-color: $fill-color-light;
|
||||
color: $text-color-regular;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn $transition-duration ease-in-out;
|
||||
}
|
||||
69
frontend/src/styles/variables.scss
Normal file
69
frontend/src/styles/variables.scss
Normal file
@@ -0,0 +1,69 @@
|
||||
// 颜色变量
|
||||
$primary-color: #409eff;
|
||||
$success-color: #67c23a;
|
||||
$warning-color: #e6a23c;
|
||||
$danger-color: #f56c6c;
|
||||
$info-color: #909399;
|
||||
|
||||
// 背景颜色
|
||||
$bg-color: #ffffff;
|
||||
$bg-color-page: #f2f3f5;
|
||||
$bg-color-overlay: #ffffff;
|
||||
|
||||
// 文字颜色
|
||||
$text-color-primary: #303133;
|
||||
$text-color-regular: #606266;
|
||||
$text-color-secondary: #909399;
|
||||
$text-color-placeholder: #a8abb2;
|
||||
$text-color-disabled: #c0c4cc;
|
||||
|
||||
// 边框颜色
|
||||
$border-color: #dcdfe6;
|
||||
$border-color-light: #e4e7ed;
|
||||
$border-color-lighter: #ebeef5;
|
||||
$border-color-extra-light: #f2f6fc;
|
||||
$border-color-dark: #d4d7de;
|
||||
|
||||
// 填充颜色
|
||||
$fill-color: #f0f2f5;
|
||||
$fill-color-light: #f5f7fa;
|
||||
$fill-color-lighter: #fafafa;
|
||||
$fill-color-extra-light: #fafcff;
|
||||
$fill-color-dark: #ebedf0;
|
||||
$fill-color-darker: #e6e8eb;
|
||||
$fill-color-blank: #ffffff;
|
||||
|
||||
// 间距
|
||||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-md: 16px;
|
||||
$spacing-lg: 24px;
|
||||
$spacing-xl: 32px;
|
||||
|
||||
// 边框半径
|
||||
$border-radius-base: 4px;
|
||||
$border-radius-small: 2px;
|
||||
$border-radius-round: 20px;
|
||||
$border-radius-circle: 50%;
|
||||
|
||||
// 字体大小
|
||||
$font-size-extra-large: 20px;
|
||||
$font-size-large: 18px;
|
||||
$font-size-medium: 16px;
|
||||
$font-size-base: 14px;
|
||||
$font-size-small: 13px;
|
||||
$font-size-extra-small: 12px;
|
||||
|
||||
// 阴影
|
||||
$box-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
$box-shadow-dark: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.12);
|
||||
$box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 层级
|
||||
$index-normal: 1;
|
||||
$index-top: 1000;
|
||||
$index-popper: 2000;
|
||||
|
||||
// 动画持续时间
|
||||
$transition-duration: 0.3s;
|
||||
$transition-duration-fast: 0.2s;
|
||||
93
frontend/src/utils/request.js
Normal file
93
frontend/src/utils/request.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API || 'http://localhost:8080', // 后端API地址
|
||||
timeout: 15000 // 请求超时时间
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// 在发送请求之前做些什么
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
// 对请求错误做些什么
|
||||
console.log(error) // for debug
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// 如果自定义状态码不是200,则显示错误信息
|
||||
if (res.code !== 200) {
|
||||
ElMessage({
|
||||
message: res.message || '请求失败',
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log('err' + error) // for debug
|
||||
let message = '网络错误'
|
||||
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 400:
|
||||
message = '请求错误'
|
||||
break
|
||||
case 401:
|
||||
message = '未授权,请登录'
|
||||
break
|
||||
case 403:
|
||||
message = '拒绝访问'
|
||||
break
|
||||
case 404:
|
||||
message = '请求地址出错'
|
||||
break
|
||||
case 408:
|
||||
message = '请求超时'
|
||||
break
|
||||
case 500:
|
||||
message = '服务器内部错误'
|
||||
break
|
||||
case 501:
|
||||
message = '服务未实现'
|
||||
break
|
||||
case 502:
|
||||
message = '网关错误'
|
||||
break
|
||||
case 503:
|
||||
message = '服务不可用'
|
||||
break
|
||||
case 504:
|
||||
message = '网关超时'
|
||||
break
|
||||
case 505:
|
||||
message = 'HTTP版本不受支持'
|
||||
break
|
||||
default:
|
||||
message = '网络错误'
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage({
|
||||
message: message,
|
||||
type: 'error',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
558
frontend/src/views/Dashboard.vue
Normal file
558
frontend/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,558 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>农业股票数据分析平台</h1>
|
||||
<p>基于Spark大数据处理的农业股票市场监控系统</p>
|
||||
</div>
|
||||
|
||||
<!-- 市场总览卡片 -->
|
||||
<el-row :gutter="20" class="overview-cards">
|
||||
<el-col :span="6">
|
||||
<el-card class="overview-card up">
|
||||
<div class="card-content">
|
||||
<div class="card-icon">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-title">上涨股票</div>
|
||||
<div class="card-value">{{ marketData.upCount || 0 }}</div>
|
||||
<div class="card-desc">只股票上涨</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="overview-card down">
|
||||
<div class="card-content">
|
||||
<div class="card-icon">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-title">下跌股票</div>
|
||||
<div class="card-value">{{ marketData.downCount || 0 }}</div>
|
||||
<div class="card-desc">只股票下跌</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="overview-card volume">
|
||||
<div class="card-content">
|
||||
<div class="card-icon">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-title">总成交量</div>
|
||||
<div class="card-value">{{ formatNumber(marketData.totalVolume) }}</div>
|
||||
<div class="card-desc">手</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="overview-card market-cap">
|
||||
<div class="card-content">
|
||||
<div class="card-icon">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-title">总市值</div>
|
||||
<div class="card-value">{{ formatNumber(marketData.totalMarketCap) }}</div>
|
||||
<div class="card-desc">亿元</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="chart-section">
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>市场涨跌分布</span>
|
||||
<el-button type="primary" size="small" @click="refreshData">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div id="pie-chart" style="height: 400px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>市场趋势分析</span>
|
||||
<el-button type="primary" size="small" @click="loadTrendData">查看趋势</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div id="line-chart" style="height: 400px;"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 快速导航 -->
|
||||
<el-row :gutter="20" class="quick-nav-section">
|
||||
<el-col :span="6">
|
||||
<el-card class="nav-card" @click="navigateTo('/search')">
|
||||
<div class="nav-content">
|
||||
<div class="nav-icon search">
|
||||
<el-icon><Search /></el-icon>
|
||||
</div>
|
||||
<div class="nav-text">
|
||||
<h3>股票搜索</h3>
|
||||
<p>搜索和查看股票详细信息</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="nav-card" @click="navigateTo('/rankings')">
|
||||
<div class="nav-content">
|
||||
<div class="nav-icon rankings">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="nav-text">
|
||||
<h3>股票排行榜</h3>
|
||||
<p>涨幅、市值、成交量排行</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="nav-card" @click="navigateTo('/market-analysis')">
|
||||
<div class="nav-content">
|
||||
<div class="nav-icon analysis">
|
||||
<el-icon><Pie /></el-icon>
|
||||
</div>
|
||||
<div class="nav-text">
|
||||
<h3>市场分析</h3>
|
||||
<p>深度市场分析和趋势预测</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="nav-card" @click="navigateTo('/health')">
|
||||
<div class="nav-content">
|
||||
<div class="nav-icon health">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
</div>
|
||||
<div class="nav-text">
|
||||
<h3>系统监控</h3>
|
||||
<p>系统健康状态检查</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-card class="table-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>最新市场分析数据</span>
|
||||
<el-button type="success" size="small" @click="runSparkAnalysis">
|
||||
<el-icon><Lightning /></el-icon>
|
||||
运行Spark分析
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="recentData" style="width: 100%">
|
||||
<el-table-column prop="analysisDate" label="分析日期" width="120" />
|
||||
<el-table-column prop="totalCount" label="总股票数" width="100" />
|
||||
<el-table-column prop="upCount" label="上涨" width="80">
|
||||
<template #default="scope">
|
||||
<span style="color: #f56c6c">{{ scope.row.upCount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="downCount" label="下跌" width="80">
|
||||
<template #default="scope">
|
||||
<span style="color: #67c23a">{{ scope.row.downCount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="flatCount" label="平盘" width="80" />
|
||||
<el-table-column prop="totalVolume" label="总成交量" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatNumber(scope.row.totalVolume) }}手
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalMarketCap" label="总市值" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatNumber(scope.row.totalMarketCap) }}亿
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="avgChangePercent" label="平均涨跌幅" width="120">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.avgChangePercent >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.avgChangePercent }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getLatestMarketAnalysis, getRecentMarketAnalysis } from '@/api/market'
|
||||
import { getMarketAnalysis, getRealtimeStockData } from '@/api/stock'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const marketData = ref({})
|
||||
const recentData = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 加载最新市场数据
|
||||
const loadLatestData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getLatestMarketAnalysis()
|
||||
if (response.data) {
|
||||
marketData.value = response.data
|
||||
initPieChart()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载市场数据失败:', error)
|
||||
ElMessage.error('加载市场数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最近数据
|
||||
const loadRecentData = async () => {
|
||||
try {
|
||||
const response = await getRecentMarketAnalysis(7)
|
||||
if (response.data) {
|
||||
recentData.value = response.data
|
||||
initLineChart()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化饼图
|
||||
const initPieChart = async () => {
|
||||
await nextTick()
|
||||
const chartDom = document.getElementById('pie-chart')
|
||||
if (!chartDom) return
|
||||
|
||||
const myChart = echarts.init(chartDom)
|
||||
const option = {
|
||||
title: {
|
||||
text: '市场涨跌分布',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '股票数量',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: [
|
||||
{ value: marketData.value.upCount || 0, name: '上涨股票', itemStyle: { color: '#f56c6c' } },
|
||||
{ value: marketData.value.downCount || 0, name: '下跌股票', itemStyle: { color: '#67c23a' } },
|
||||
{ value: marketData.value.flatCount || 0, name: '平盘股票', itemStyle: { color: '#909399' } }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化折线图
|
||||
const initLineChart = async () => {
|
||||
await nextTick()
|
||||
const chartDom = document.getElementById('line-chart')
|
||||
if (!chartDom) return
|
||||
|
||||
const myChart = echarts.init(chartDom)
|
||||
const dates = recentData.value.map(item => item.analysisDate).reverse()
|
||||
const avgChanges = recentData.value.map(item => item.avgChangePercent).reverse()
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '市场平均涨跌幅趋势',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '涨跌幅(%)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: avgChanges,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#409EFF'
|
||||
},
|
||||
areaStyle: {
|
||||
color: 'rgba(64, 158, 255, 0.1)'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
const formatNumber = (num) => {
|
||||
if (!num) return '0'
|
||||
return (num / 10000).toFixed(1)
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
loadLatestData()
|
||||
loadRecentData()
|
||||
}
|
||||
|
||||
// 加载趋势数据
|
||||
const loadTrendData = () => {
|
||||
loadRecentData()
|
||||
}
|
||||
|
||||
// 模拟运行Spark分析
|
||||
const runSparkAnalysis = () => {
|
||||
ElMessage.success('Spark分析任务已提交,请稍候查看结果')
|
||||
setTimeout(() => {
|
||||
refreshData()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 导航到指定页面
|
||||
const navigateTo = (path) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLatestData()
|
||||
loadRecentData()
|
||||
})
|
||||
|
||||
return {
|
||||
marketData,
|
||||
recentData,
|
||||
loading,
|
||||
refreshData,
|
||||
loadTrendData,
|
||||
runSparkAnalysis,
|
||||
formatNumber,
|
||||
navigateTo
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.up {
|
||||
background: linear-gradient(135deg, #f56c6c, #ff8a80);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.down {
|
||||
background: linear-gradient(135deg, #67c23a, #81c784);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.volume {
|
||||
background: linear-gradient(135deg, #409eff, #64b5f6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.market-cap {
|
||||
background: linear-gradient(135deg, #e6a23c, #ffb74d);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.card-icon {
|
||||
font-size: 40px;
|
||||
margin-right: 15px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-nav-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.nav-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
|
||||
.nav-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
font-size: 24px;
|
||||
|
||||
&.search {
|
||||
background: linear-gradient(135deg, #409eff, #66d9ef);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.rankings {
|
||||
background: linear-gradient(135deg, #f56c6c, #ff9a9e);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.analysis {
|
||||
background: linear-gradient(135deg, #67c23a, #95de64);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.health {
|
||||
background: linear-gradient(135deg, #e6a23c, #ffc53d);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
flex: 1;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card, .table-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
622
frontend/src/views/DataManagement.vue
Normal file
622
frontend/src/views/DataManagement.vue
Normal file
@@ -0,0 +1,622 @@
|
||||
<template>
|
||||
<div class="data-management">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>数据管理</h1>
|
||||
<p>股票数据的导入、导出和批量管理</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据操作区域 -->
|
||||
<el-row :gutter="20" class="operation-section">
|
||||
<!-- 数据导入 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="operation-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>数据导入</span>
|
||||
<el-icon><Upload /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="import-section">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:show-file-list="true"
|
||||
accept=".json,.csv"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 JSON 和 CSV 格式的股票数据文件
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<div class="import-actions" v-if="uploadFile">
|
||||
<el-button type="primary" @click="importData" :loading="importing">
|
||||
<el-icon><Upload /></el-icon>
|
||||
导入数据
|
||||
</el-button>
|
||||
<el-button @click="clearUpload">
|
||||
<el-icon><Close /></el-icon>
|
||||
清除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 数据导出 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="operation-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>数据导出</span>
|
||||
<el-icon><Download /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="export-section">
|
||||
<div class="export-options">
|
||||
<el-form :model="exportForm" label-width="100px">
|
||||
<el-form-item label="导出格式">
|
||||
<el-radio-group v-model="exportForm.format">
|
||||
<el-radio label="json">JSON</el-radio>
|
||||
<el-radio label="csv">CSV</el-radio>
|
||||
<el-radio label="excel">Excel</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="数据范围">
|
||||
<el-select v-model="exportForm.range" style="width: 100%;">
|
||||
<el-option label="全部数据" value="all" />
|
||||
<el-option label="最近一周" value="week" />
|
||||
<el-option label="最近一月" value="month" />
|
||||
<el-option label="自定义日期" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="日期范围" v-if="exportForm.range === 'custom'">
|
||||
<el-date-picker
|
||||
v-model="exportForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="export-actions">
|
||||
<el-button type="success" @click="exportData" :loading="exporting">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 批量操作区域 -->
|
||||
<el-row class="batch-section">
|
||||
<el-col :span="24">
|
||||
<el-card class="batch-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>批量操作</span>
|
||||
<div class="batch-actions">
|
||||
<el-button type="primary" @click="showBatchDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
批量添加
|
||||
</el-button>
|
||||
<el-button type="danger" @click="batchDelete" :disabled="selectedRows.length === 0">
|
||||
<el-icon><Delete /></el-icon>
|
||||
批量删除 ({{ selectedRows.length }})
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
:data="paginatedStockData"
|
||||
v-loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
stripe
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="stockCode" label="股票代码" width="100" />
|
||||
<el-table-column prop="stockName" label="股票名称" width="150" />
|
||||
<el-table-column prop="currentPrice" label="当前价格" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.currentPrice?.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changePercent" label="涨跌幅" width="100">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.changePercent >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.changePercent >= 0 ? '+' : '' }}{{ scope.row.changePercent?.toFixed(2) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="volume" label="成交量" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatVolume(scope.row.volume) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="marketCap" label="市值" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatMarketCap(scope.row.marketCap) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="timestamp" label="更新时间" width="160">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.timestamp) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="editStock(scope.row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="deleteStock(scope.row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="stockData.length > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="stockData.length"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 批量添加对话框 -->
|
||||
<el-dialog
|
||||
v-model="showBatchDialog"
|
||||
title="批量添加股票数据"
|
||||
width="600px"
|
||||
:before-close="closeBatchDialog"
|
||||
>
|
||||
<div class="batch-form">
|
||||
<el-form ref="batchFormRef" :model="batchForm" label-width="100px">
|
||||
<el-form-item label="数据输入">
|
||||
<el-input
|
||||
v-model="batchForm.jsonData"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="请输入JSON格式的股票数据数组..."
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="form-tips">
|
||||
<el-alert
|
||||
title="数据格式示例"
|
||||
type="info"
|
||||
:closable="false"
|
||||
/>
|
||||
<pre class="json-example">{{ jsonExample }}</pre>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeBatchDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveBatchData" :loading="batchSaving">
|
||||
保存数据
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { getRealtimeStockData, saveStockData, batchSaveStockData } from '@/api/stock'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
export default {
|
||||
name: 'DataManagement',
|
||||
setup() {
|
||||
const stockData = ref([])
|
||||
const selectedRows = ref([])
|
||||
const loading = ref(false)
|
||||
const importing = ref(false)
|
||||
const exporting = ref(false)
|
||||
const batchSaving = ref(false)
|
||||
const uploadFile = ref(null)
|
||||
const showBatchDialog = ref(false)
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const exportForm = ref({
|
||||
format: 'json',
|
||||
range: 'all',
|
||||
dateRange: []
|
||||
})
|
||||
|
||||
const batchForm = ref({
|
||||
jsonData: ''
|
||||
})
|
||||
|
||||
const jsonExample = ref(`[
|
||||
{
|
||||
"stockCode": "000001",
|
||||
"stockName": "平安银行",
|
||||
"currentPrice": 12.85,
|
||||
"changePercent": 2.15,
|
||||
"changeAmount": 0.27,
|
||||
"volume": 125643200,
|
||||
"marketCap": 2485632000
|
||||
}
|
||||
]`)
|
||||
|
||||
// 加载股票数据
|
||||
const loadStockData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getRealtimeStockData()
|
||||
if (response.data) {
|
||||
stockData.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载股票数据失败:', error)
|
||||
ElMessage.error('加载股票数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页数据
|
||||
const paginatedStockData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return stockData.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 文件上传处理
|
||||
const handleFileChange = (file) => {
|
||||
uploadFile.value = file
|
||||
}
|
||||
|
||||
// 清除上传
|
||||
const clearUpload = () => {
|
||||
uploadFile.value = null
|
||||
}
|
||||
|
||||
// 导入数据
|
||||
const importData = async () => {
|
||||
if (!uploadFile.value) {
|
||||
ElMessage.warning('请选择要导入的文件')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
importing.value = true
|
||||
// 这里应该实现文件解析和数据导入逻辑
|
||||
ElMessage.success('数据导入成功')
|
||||
uploadFile.value = null
|
||||
await loadStockData()
|
||||
} catch (error) {
|
||||
console.error('导入数据失败:', error)
|
||||
ElMessage.error('导入数据失败')
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const exportData = async () => {
|
||||
try {
|
||||
exporting.value = true
|
||||
|
||||
// 模拟导出过程
|
||||
const exportData = stockData.value
|
||||
const filename = `stock_data_${new Date().toISOString().split('T')[0]}.${exportForm.value.format}`
|
||||
|
||||
// 这里应该实现实际的导出逻辑
|
||||
ElMessage.success(`数据已导出为 ${filename}`)
|
||||
} catch (error) {
|
||||
console.error('导出数据失败:', error)
|
||||
ElMessage.error('导出数据失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存数据
|
||||
const saveBatchData = async () => {
|
||||
if (!batchForm.value.jsonData.trim()) {
|
||||
ElMessage.warning('请输入股票数据')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
batchSaving.value = true
|
||||
const data = JSON.parse(batchForm.value.jsonData)
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('数据格式错误,应为数组格式')
|
||||
}
|
||||
|
||||
const response = await batchSaveStockData(data)
|
||||
if (response.data) {
|
||||
ElMessage.success(`成功保存 ${response.data} 条股票数据`)
|
||||
closeBatchDialog()
|
||||
await loadStockData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量保存失败:', error)
|
||||
ElMessage.error('批量保存失败: ' + error.message)
|
||||
} finally {
|
||||
batchSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭批量对话框
|
||||
const closeBatchDialog = () => {
|
||||
showBatchDialog.value = false
|
||||
batchForm.value.jsonData = ''
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const batchDelete = async () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedRows.value.length} 条数据吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 这里应该调用批量删除API
|
||||
ElMessage.success('批量删除成功')
|
||||
await loadStockData()
|
||||
} catch (error) {
|
||||
// 用户取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑股票
|
||||
const editStock = (stock) => {
|
||||
ElMessage.info(`编辑股票: ${stock.stockName}`)
|
||||
}
|
||||
|
||||
// 删除股票
|
||||
const deleteStock = async (stock) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除股票 ${stock.stockName} 吗?`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 这里应该调用删除API
|
||||
ElMessage.success('删除成功')
|
||||
await loadStockData()
|
||||
} catch (error) {
|
||||
// 用户取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化函数
|
||||
const formatVolume = (volume) => {
|
||||
if (!volume) return '0手'
|
||||
if (volume >= 100000000) {
|
||||
return (volume / 100000000).toFixed(1) + '亿手'
|
||||
} else if (volume >= 10000) {
|
||||
return (volume / 10000).toFixed(1) + '万手'
|
||||
} else {
|
||||
return volume + '手'
|
||||
}
|
||||
}
|
||||
|
||||
const formatMarketCap = (marketCap) => {
|
||||
if (!marketCap) return '0元'
|
||||
if (marketCap >= 100000000) {
|
||||
return (marketCap / 100000000).toFixed(1) + '亿元'
|
||||
} else if (marketCap >= 10000) {
|
||||
return (marketCap / 10000).toFixed(1) + '万元'
|
||||
} else {
|
||||
return marketCap + '元'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateTime = (timestamp) => {
|
||||
if (!timestamp) return ''
|
||||
return new Date(timestamp).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 分页事件
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStockData()
|
||||
})
|
||||
|
||||
return {
|
||||
stockData,
|
||||
selectedRows,
|
||||
loading,
|
||||
importing,
|
||||
exporting,
|
||||
batchSaving,
|
||||
uploadFile,
|
||||
showBatchDialog,
|
||||
currentPage,
|
||||
pageSize,
|
||||
exportForm,
|
||||
batchForm,
|
||||
jsonExample,
|
||||
paginatedStockData,
|
||||
handleFileChange,
|
||||
clearUpload,
|
||||
importData,
|
||||
exportData,
|
||||
saveBatchData,
|
||||
closeBatchDialog,
|
||||
handleSelectionChange,
|
||||
batchDelete,
|
||||
editStock,
|
||||
deleteStock,
|
||||
formatVolume,
|
||||
formatMarketCap,
|
||||
formatDateTime,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data-management {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.operation-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.operation-card, .batch-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.import-section, .export-section {
|
||||
.import-actions, .export-actions {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.export-options {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.batch-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.batch-form {
|
||||
.form-tips {
|
||||
margin-top: 10px;
|
||||
|
||||
.json-example {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
|
||||
.el-upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-table__header {
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
frontend/src/views/HealthCheck.vue
Normal file
108
frontend/src/views/HealthCheck.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="health-check">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>系统健康检查</span>
|
||||
<el-tag type="success">运行正常</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="前端状态">
|
||||
<el-tag type="success">正常运行</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="端口">3000</el-descriptions-item>
|
||||
<el-descriptions-item label="Vue版本">{{ vueVersion }}</el-descriptions-item>
|
||||
<el-descriptions-item label="Element Plus">已加载</el-descriptions-item>
|
||||
<el-descriptions-item label="路由">{{ routerReady ? '已配置' : '未配置' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态管理">{{ storeReady ? '已配置' : '未配置' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<el-button type="primary" @click="testApi">测试API连接</el-button>
|
||||
<el-button type="success" @click="testStyles">测试样式变量</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="apiStatus" style="margin-top: 15px;">
|
||||
<el-alert :title="apiStatus.title" :type="apiStatus.type" show-icon />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { version } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'HealthCheck',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
const apiStatus = ref(null)
|
||||
|
||||
const vueVersion = ref(version)
|
||||
const routerReady = ref(!!router)
|
||||
const storeReady = ref(!!store)
|
||||
|
||||
const testApi = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/health')
|
||||
if (response.ok) {
|
||||
apiStatus.value = {
|
||||
title: 'API连接成功',
|
||||
type: 'success'
|
||||
}
|
||||
} else {
|
||||
apiStatus.value = {
|
||||
title: 'API连接失败',
|
||||
type: 'error'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
apiStatus.value = {
|
||||
title: `API连接错误: ${error.message}`,
|
||||
type: 'warning'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testStyles = () => {
|
||||
ElMessage.success('SCSS变量加载正常!')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('健康检查页面已加载')
|
||||
})
|
||||
|
||||
return {
|
||||
vueVersion,
|
||||
routerReady,
|
||||
storeReady,
|
||||
apiStatus,
|
||||
testApi,
|
||||
testStyles
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.health-check {
|
||||
padding: 20px;
|
||||
background-color: $bg-color-page;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
796
frontend/src/views/MarketAnalysis.vue
Normal file
796
frontend/src/views/MarketAnalysis.vue
Normal file
@@ -0,0 +1,796 @@
|
||||
<template>
|
||||
<div class="market-analysis">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>市场分析</h1>
|
||||
<p>基于大数据的农业股票市场深度分析</p>
|
||||
</div>
|
||||
|
||||
<!-- 最新市场分析卡片 -->
|
||||
<div class="latest-analysis" v-if="latestAnalysis.analysisDate">
|
||||
<el-card class="analysis-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>最新市场分析</span>
|
||||
<div class="analysis-date">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
{{ formatDate(latestAnalysis.analysisDate) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 市场概览指标 -->
|
||||
<div class="market-overview">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="overview-item total">
|
||||
<div class="item-icon">
|
||||
<el-icon><Pie /></el-icon>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-value">{{ latestAnalysis.totalCount }}</div>
|
||||
<div class="item-label">总股票数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="overview-item up">
|
||||
<div class="item-icon">
|
||||
<el-icon><CaretTop /></el-icon>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-value">{{ latestAnalysis.upCount }}</div>
|
||||
<div class="item-label">上涨股票</div>
|
||||
<div class="item-percent">{{ getUpPercent() }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="overview-item down">
|
||||
<div class="item-icon">
|
||||
<el-icon><CaretBottom /></el-icon>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-value">{{ latestAnalysis.downCount }}</div>
|
||||
<div class="item-label">下跌股票</div>
|
||||
<div class="item-percent">{{ getDownPercent() }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="overview-item flat">
|
||||
<div class="item-icon">
|
||||
<el-icon><Minus /></el-icon>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-value">{{ latestAnalysis.flatCount }}</div>
|
||||
<div class="item-label">平盘股票</div>
|
||||
<div class="item-percent">{{ getFlatPercent() }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 市场关键指标 -->
|
||||
<div class="market-metrics">
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="8">
|
||||
<div class="metric-card">
|
||||
<div class="metric-header">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>总成交量</span>
|
||||
</div>
|
||||
<div class="metric-value volume">
|
||||
{{ formatVolume(latestAnalysis.totalVolume) }}
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="metric-card">
|
||||
<div class="metric-header">
|
||||
<el-icon><Money /></el-icon>
|
||||
<span>总市值</span>
|
||||
</div>
|
||||
<div class="metric-value market-cap">
|
||||
{{ formatMarketCap(latestAnalysis.totalMarketCap) }}
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="metric-card">
|
||||
<div class="metric-header">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
<span>平均涨跌幅</span>
|
||||
</div>
|
||||
<div class="metric-value" :class="{ 'positive': latestAnalysis.avgChangePercent >= 0, 'negative': latestAnalysis.avgChangePercent < 0 }">
|
||||
{{ latestAnalysis.avgChangePercent >= 0 ? '+' : '' }}{{ latestAnalysis.avgChangePercent?.toFixed(2) }}%
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 图表分析区域 -->
|
||||
<el-row :gutter="20" class="charts-section">
|
||||
<!-- 市场涨跌分布 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>市场涨跌分布</span>
|
||||
<el-button type="primary" size="small" @click="refreshLatestData" :loading="latestLoading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div id="distribution-chart" style="height: 350px;" v-loading="latestLoading"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 历史趋势分析 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>历史趋势分析</span>
|
||||
<div class="trend-controls">
|
||||
<el-select v-model="trendDays" @change="loadTrendData" size="small" style="width: 120px;">
|
||||
<el-option label="7天" :value="7" />
|
||||
<el-option label="15天" :value="15" />
|
||||
<el-option label="30天" :value="30" />
|
||||
<el-option label="60天" :value="60" />
|
||||
<el-option label="90天" :value="90" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div id="trend-chart" style="height: 350px;" v-loading="trendLoading"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 详细分析数据表格 -->
|
||||
<div class="analysis-table-section">
|
||||
<el-card class="table-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>历史分析数据</span>
|
||||
<div class="table-controls">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
size="small"
|
||||
@change="loadRangeData"
|
||||
style="margin-right: 10px;"
|
||||
/>
|
||||
<el-button type="primary" size="small" @click="loadRangeData" :loading="rangeLoading">
|
||||
<el-icon><Search /></el-icon>
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="paginatedRangeData" v-loading="rangeLoading" stripe>
|
||||
<el-table-column prop="analysisDate" label="分析日期" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.analysisDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalCount" label="总股票数" width="100" align="center" />
|
||||
<el-table-column label="涨跌分布" width="200">
|
||||
<template #default="scope">
|
||||
<div class="distribution-mini">
|
||||
<span class="dist-item up">
|
||||
<el-icon><CaretTop /></el-icon>
|
||||
{{ scope.row.upCount }}
|
||||
</span>
|
||||
<span class="dist-item down">
|
||||
<el-icon><CaretBottom /></el-icon>
|
||||
{{ scope.row.downCount }}
|
||||
</span>
|
||||
<span class="dist-item flat">
|
||||
<el-icon><Minus /></el-icon>
|
||||
{{ scope.row.flatCount }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalVolume" label="总成交量" width="150">
|
||||
<template #default="scope">
|
||||
{{ formatVolume(scope.row.totalVolume) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalMarketCap" label="总市值" width="150">
|
||||
<template #default="scope">
|
||||
{{ formatMarketCap(scope.row.totalMarketCap) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="avgChangePercent" label="平均涨跌幅" width="120">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.avgChangePercent >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.avgChangePercent >= 0 ? '+' : '' }}{{ scope.row.avgChangePercent?.toFixed(2) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="市场情绪" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getMarketSentimentType(scope.row)" size="small">
|
||||
{{ getMarketSentiment(scope.row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="viewDetails(scope.row)">
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="rangeData.length > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="rangeData.length"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||
import { getLatestMarketAnalysis, getRecentMarketAnalysis, getMarketAnalysisByDateRange } from '@/api/market'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'MarketAnalysis',
|
||||
setup() {
|
||||
const latestAnalysis = ref({})
|
||||
const trendData = ref([])
|
||||
const rangeData = ref([])
|
||||
|
||||
const latestLoading = ref(false)
|
||||
const trendLoading = ref(false)
|
||||
const rangeLoading = ref(false)
|
||||
|
||||
const trendDays = ref(30)
|
||||
const dateRange = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
// 加载最新分析数据
|
||||
const loadLatestData = async () => {
|
||||
try {
|
||||
latestLoading.value = true
|
||||
const response = await getLatestMarketAnalysis()
|
||||
if (response.data) {
|
||||
latestAnalysis.value = response.data
|
||||
await nextTick()
|
||||
initDistributionChart()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载最新分析数据失败:', error)
|
||||
ElMessage.error('加载最新分析数据失败')
|
||||
} finally {
|
||||
latestLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载趋势数据
|
||||
const loadTrendData = async () => {
|
||||
try {
|
||||
trendLoading.value = true
|
||||
const response = await getRecentMarketAnalysis(trendDays.value)
|
||||
if (response.data) {
|
||||
trendData.value = response.data
|
||||
await nextTick()
|
||||
initTrendChart()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载趋势数据失败:', error)
|
||||
ElMessage.error('加载趋势数据失败')
|
||||
} finally {
|
||||
trendLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载日期范围数据
|
||||
const loadRangeData = async () => {
|
||||
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||
await loadTrendData() // 如果没有选择日期范围,默认加载最近趋势数据
|
||||
rangeData.value = trendData.value
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
rangeLoading.value = true
|
||||
const [startDate, endDate] = dateRange.value
|
||||
const response = await getMarketAnalysisByDateRange(
|
||||
startDate.toISOString().split('T')[0],
|
||||
endDate.toISOString().split('T')[0]
|
||||
)
|
||||
if (response.data) {
|
||||
rangeData.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日期范围数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
rangeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化分布图表
|
||||
const initDistributionChart = async () => {
|
||||
const chartDom = document.getElementById('distribution-chart')
|
||||
if (!chartDom || !latestAnalysis.value.totalCount) return
|
||||
|
||||
const myChart = echarts.init(chartDom)
|
||||
const option = {
|
||||
title: {
|
||||
text: '市场涨跌分布',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'horizontal',
|
||||
bottom: '10%',
|
||||
left: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '股票分布',
|
||||
type: 'pie',
|
||||
radius: ['30%', '70%'],
|
||||
center: ['50%', '45%'],
|
||||
data: [
|
||||
{
|
||||
value: latestAnalysis.value.upCount,
|
||||
name: '上涨',
|
||||
itemStyle: { color: '#f56c6c' }
|
||||
},
|
||||
{
|
||||
value: latestAnalysis.value.downCount,
|
||||
name: '下跌',
|
||||
itemStyle: { color: '#67c23a' }
|
||||
},
|
||||
{
|
||||
value: latestAnalysis.value.flatCount,
|
||||
name: '平盘',
|
||||
itemStyle: { color: '#909399' }
|
||||
}
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}\n{d}%'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化趋势图表
|
||||
const initTrendChart = async () => {
|
||||
const chartDom = document.getElementById('trend-chart')
|
||||
if (!chartDom || trendData.value.length === 0) return
|
||||
|
||||
const myChart = echarts.init(chartDom)
|
||||
const dates = trendData.value.map(item => formatDate(item.analysisDate)).reverse()
|
||||
const avgChanges = trendData.value.map(item => item.avgChangePercent).reverse()
|
||||
const upCounts = trendData.value.map(item => item.upCount).reverse()
|
||||
const downCounts = trendData.value.map(item => item.downCount).reverse()
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '市场趋势分析',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['平均涨跌幅', '上涨股票数', '下跌股票数'],
|
||||
bottom: '5%'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
rotate: 45
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '涨跌幅(%)',
|
||||
position: 'left'
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '股票数量',
|
||||
position: 'right'
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '平均涨跌幅',
|
||||
type: 'line',
|
||||
data: avgChanges,
|
||||
smooth: true,
|
||||
lineStyle: { color: '#409EFF' },
|
||||
areaStyle: { color: 'rgba(64, 158, 255, 0.1)' }
|
||||
},
|
||||
{
|
||||
name: '上涨股票数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: upCounts,
|
||||
itemStyle: { color: '#f56c6c' }
|
||||
},
|
||||
{
|
||||
name: '下跌股票数',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
data: downCounts,
|
||||
itemStyle: { color: '#67c23a' }
|
||||
}
|
||||
]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
}
|
||||
|
||||
// 分页数据
|
||||
const paginatedRangeData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return rangeData.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 计算百分比
|
||||
const getUpPercent = () => {
|
||||
if (!latestAnalysis.value.totalCount) return 0
|
||||
return ((latestAnalysis.value.upCount / latestAnalysis.value.totalCount) * 100).toFixed(1)
|
||||
}
|
||||
|
||||
const getDownPercent = () => {
|
||||
if (!latestAnalysis.value.totalCount) return 0
|
||||
return ((latestAnalysis.value.downCount / latestAnalysis.value.totalCount) * 100).toFixed(1)
|
||||
}
|
||||
|
||||
const getFlatPercent = () => {
|
||||
if (!latestAnalysis.value.totalCount) return 0
|
||||
return ((latestAnalysis.value.flatCount / latestAnalysis.value.totalCount) * 100).toFixed(1)
|
||||
}
|
||||
|
||||
// 格式化函数
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
return dateStr.split('T')[0]
|
||||
}
|
||||
|
||||
const formatVolume = (volume) => {
|
||||
if (!volume) return '0手'
|
||||
if (volume >= 100000000) {
|
||||
return (volume / 100000000).toFixed(1) + '亿手'
|
||||
} else if (volume >= 10000) {
|
||||
return (volume / 10000).toFixed(1) + '万手'
|
||||
} else {
|
||||
return volume + '手'
|
||||
}
|
||||
}
|
||||
|
||||
const formatMarketCap = (marketCap) => {
|
||||
if (!marketCap) return '0元'
|
||||
if (marketCap >= 100000000) {
|
||||
return (marketCap / 100000000).toFixed(1) + '亿元'
|
||||
} else if (marketCap >= 10000) {
|
||||
return (marketCap / 10000).toFixed(1) + '万元'
|
||||
} else {
|
||||
return marketCap + '元'
|
||||
}
|
||||
}
|
||||
|
||||
// 市场情绪分析
|
||||
const getMarketSentiment = (data) => {
|
||||
const upPercent = (data.upCount / data.totalCount) * 100
|
||||
if (upPercent >= 60) return '乐观'
|
||||
if (upPercent >= 40) return '中性'
|
||||
return '悲观'
|
||||
}
|
||||
|
||||
const getMarketSentimentType = (data) => {
|
||||
const upPercent = (data.upCount / data.totalCount) * 100
|
||||
if (upPercent >= 60) return 'success'
|
||||
if (upPercent >= 40) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
// 分页事件
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val
|
||||
}
|
||||
|
||||
// 刷新最新数据
|
||||
const refreshLatestData = () => {
|
||||
loadLatestData()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (data) => {
|
||||
ElMessage.info(`查看 ${formatDate(data.analysisDate)} 的详细分析数据`)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadLatestData()
|
||||
await loadTrendData()
|
||||
rangeData.value = trendData.value
|
||||
})
|
||||
|
||||
return {
|
||||
latestAnalysis,
|
||||
trendData,
|
||||
rangeData,
|
||||
latestLoading,
|
||||
trendLoading,
|
||||
rangeLoading,
|
||||
trendDays,
|
||||
dateRange,
|
||||
currentPage,
|
||||
pageSize,
|
||||
paginatedRangeData,
|
||||
loadTrendData,
|
||||
loadRangeData,
|
||||
getUpPercent,
|
||||
getDownPercent,
|
||||
getFlatPercent,
|
||||
formatDate,
|
||||
formatVolume,
|
||||
formatMarketCap,
|
||||
getMarketSentiment,
|
||||
getMarketSentimentType,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshLatestData,
|
||||
viewDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.market-analysis {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.latest-analysis {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.analysis-card, .chart-card, .table-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.analysis-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.market-overview {
|
||||
.overview-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
|
||||
.item-icon {
|
||||
font-size: 40px;
|
||||
margin-right: 15px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
|
||||
.item-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.item-percent {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&.total {
|
||||
background: linear-gradient(135deg, #606266, #909399);
|
||||
}
|
||||
|
||||
&.up {
|
||||
background: linear-gradient(135deg, #f56c6c, #ff8a80);
|
||||
}
|
||||
|
||||
&.down {
|
||||
background: linear-gradient(135deg, #67c23a, #81c784);
|
||||
}
|
||||
|
||||
&.flat {
|
||||
background: linear-gradient(135deg, #909399, #b0b3b8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.market-metrics {
|
||||
.metric-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.metric-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
|
||||
&.volume {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.market-cap {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.positive {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.trend-controls, .table-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.analysis-table-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.distribution-mini {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
|
||||
.dist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 12px;
|
||||
|
||||
&.up {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.flat {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-table__header {
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
407
frontend/src/views/Rankings.vue
Normal file
407
frontend/src/views/Rankings.vue
Normal file
@@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<div class="rankings">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>股票排行榜</h1>
|
||||
<p>实时股票市场排行数据</p>
|
||||
</div>
|
||||
|
||||
<!-- 排行榜类型切换 -->
|
||||
<div class="ranking-tabs">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="涨幅排行" name="growth">
|
||||
<div class="ranking-content">
|
||||
<div class="ranking-header">
|
||||
<span class="ranking-title">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
涨幅排行榜
|
||||
</span>
|
||||
<div class="ranking-controls">
|
||||
<el-select v-model="rankingLimit" @change="loadRankingData" size="small" style="width: 100px;">
|
||||
<el-option label="前10名" :value="10" />
|
||||
<el-option label="前20名" :value="20" />
|
||||
<el-option label="前50名" :value="50" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="loadRankingData" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="growthRanking" v-loading="loading" stripe>
|
||||
<el-table-column label="排名" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.$index < 3 ? 'danger' : 'info'"
|
||||
effect="dark"
|
||||
size="small"
|
||||
>
|
||||
{{ scope.$index + 1 }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stockCode" label="股票代码" width="100" />
|
||||
<el-table-column prop="stockName" label="股票名称" width="150" />
|
||||
<el-table-column prop="currentPrice" label="当前价格" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.currentPrice?.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changePercent" label="涨跌幅" width="120">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.changePercent >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.changePercent >= 0 ? '+' : '' }}{{ scope.row.changePercent?.toFixed(2) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changeAmount" label="涨跌额" width="100">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.changeAmount >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.changeAmount >= 0 ? '+' : '' }}{{ scope.row.changeAmount?.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="volume" label="成交量" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatVolume(scope.row.volume) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="marketCap" label="市值" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatMarketCap(scope.row.marketCap) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="viewDetails(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="viewTrend(scope.row)">
|
||||
趋势
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="市值排行" name="marketCap">
|
||||
<div class="ranking-content">
|
||||
<div class="ranking-header">
|
||||
<span class="ranking-title">
|
||||
<el-icon><Money /></el-icon>
|
||||
市值排行榜
|
||||
</span>
|
||||
<div class="ranking-controls">
|
||||
<el-select v-model="rankingLimit" @change="loadRankingData" size="small" style="width: 100px;">
|
||||
<el-option label="前10名" :value="10" />
|
||||
<el-option label="前20名" :value="20" />
|
||||
<el-option label="前50名" :value="50" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="loadRankingData" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="marketCapRanking" v-loading="loading" stripe>
|
||||
<el-table-column label="排名" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.$index < 3 ? 'warning' : 'info'"
|
||||
effect="dark"
|
||||
size="small"
|
||||
>
|
||||
{{ scope.$index + 1 }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stockCode" label="股票代码" width="100" />
|
||||
<el-table-column prop="stockName" label="股票名称" width="150" />
|
||||
<el-table-column prop="currentPrice" label="当前价格" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.currentPrice?.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="marketCap" label="市值" width="150">
|
||||
<template #default="scope">
|
||||
<span style="font-weight: bold; color: #e6a23c;">
|
||||
{{ formatMarketCap(scope.row.marketCap) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changePercent" label="涨跌幅" width="120">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.changePercent >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.changePercent >= 0 ? '+' : '' }}{{ scope.row.changePercent?.toFixed(2) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="volume" label="成交量" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatVolume(scope.row.volume) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="viewDetails(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="viewTrend(scope.row)">
|
||||
趋势
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="成交量排行" name="volume">
|
||||
<div class="ranking-content">
|
||||
<div class="ranking-header">
|
||||
<span class="ranking-title">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
成交量排行榜
|
||||
</span>
|
||||
<div class="ranking-controls">
|
||||
<el-select v-model="rankingLimit" @change="loadRankingData" size="small" style="width: 100px;">
|
||||
<el-option label="前10名" :value="10" />
|
||||
<el-option label="前20名" :value="20" />
|
||||
<el-option label="前50名" :value="50" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="loadRankingData" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="volumeRanking" v-loading="loading" stripe>
|
||||
<el-table-column label="排名" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.$index < 3 ? 'success' : 'info'"
|
||||
effect="dark"
|
||||
size="small"
|
||||
>
|
||||
{{ scope.$index + 1 }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stockCode" label="股票代码" width="100" />
|
||||
<el-table-column prop="stockName" label="股票名称" width="150" />
|
||||
<el-table-column prop="currentPrice" label="当前价格" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.currentPrice?.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="volume" label="成交量" width="150">
|
||||
<template #default="scope">
|
||||
<span style="font-weight: bold; color: #409eff;">
|
||||
{{ formatVolume(scope.row.volume) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changePercent" label="涨跌幅" width="120">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.changePercent >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.changePercent >= 0 ? '+' : '' }}{{ scope.row.changePercent?.toFixed(2) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="marketCap" label="市值" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatMarketCap(scope.row.marketCap) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="viewDetails(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="viewTrend(scope.row)">
|
||||
趋势
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getGrowthRanking, getMarketCapRanking, getVolumeRanking } from '@/api/stock'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
export default {
|
||||
name: 'Rankings',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const activeTab = ref('growth')
|
||||
const rankingLimit = ref(10)
|
||||
const loading = ref(false)
|
||||
|
||||
const growthRanking = ref([])
|
||||
const marketCapRanking = ref([])
|
||||
const volumeRanking = ref([])
|
||||
|
||||
// 加载排行榜数据
|
||||
const loadRankingData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
if (activeTab.value === 'growth') {
|
||||
const response = await getGrowthRanking(rankingLimit.value)
|
||||
if (response.data) {
|
||||
growthRanking.value = response.data
|
||||
}
|
||||
} else if (activeTab.value === 'marketCap') {
|
||||
const response = await getMarketCapRanking(rankingLimit.value)
|
||||
if (response.data) {
|
||||
marketCapRanking.value = response.data
|
||||
}
|
||||
} else if (activeTab.value === 'volume') {
|
||||
const response = await getVolumeRanking(rankingLimit.value)
|
||||
if (response.data) {
|
||||
volumeRanking.value = response.data
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载排行榜数据失败:', error)
|
||||
ElMessage.error('加载排行榜数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 标签页切换
|
||||
const handleTabChange = (tabName) => {
|
||||
activeTab.value = tabName
|
||||
loadRankingData()
|
||||
}
|
||||
|
||||
// 格式化成交量
|
||||
const formatVolume = (volume) => {
|
||||
if (!volume) return '0手'
|
||||
if (volume >= 100000000) {
|
||||
return (volume / 100000000).toFixed(1) + '亿手'
|
||||
} else if (volume >= 10000) {
|
||||
return (volume / 10000).toFixed(1) + '万手'
|
||||
} else {
|
||||
return volume + '手'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化市值
|
||||
const formatMarketCap = (marketCap) => {
|
||||
if (!marketCap) return '0元'
|
||||
if (marketCap >= 100000000) {
|
||||
return (marketCap / 100000000).toFixed(1) + '亿元'
|
||||
} else if (marketCap >= 10000) {
|
||||
return (marketCap / 10000).toFixed(1) + '万元'
|
||||
} else {
|
||||
return marketCap + '元'
|
||||
}
|
||||
}
|
||||
|
||||
// 查看股票详情
|
||||
const viewDetails = (stock) => {
|
||||
router.push(`/stock/${stock.stockCode}`)
|
||||
}
|
||||
|
||||
// 查看股票趋势
|
||||
const viewTrend = (stock) => {
|
||||
router.push(`/stock/${stock.stockCode}/trend`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRankingData()
|
||||
})
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
rankingLimit,
|
||||
loading,
|
||||
growthRanking,
|
||||
marketCapRanking,
|
||||
volumeRanking,
|
||||
loadRankingData,
|
||||
handleTabChange,
|
||||
formatVolume,
|
||||
formatMarketCap,
|
||||
viewDetails,
|
||||
viewTrend
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rankings {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-tabs {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ranking-content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.ranking-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.ranking-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ranking-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-table__header {
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
772
frontend/src/views/StockDetail.vue
Normal file
772
frontend/src/views/StockDetail.vue
Normal file
@@ -0,0 +1,772 @@
|
||||
<template>
|
||||
<div class="stock-detail">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<div class="stock-info">
|
||||
<h1>{{ stockInfo.stockName || stockCode }}</h1>
|
||||
<p>股票代码: {{ stockCode }}</p>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<el-button @click="goBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<el-button type="primary" @click="refreshData" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 股票基本信息卡片 -->
|
||||
<div class="stock-overview" v-if="stockInfo.stockCode">
|
||||
<el-card class="overview-card">
|
||||
<div class="overview-content">
|
||||
<div class="price-section">
|
||||
<div class="current-price">
|
||||
¥{{ stockInfo.currentPrice?.toFixed(2) }}
|
||||
</div>
|
||||
<div class="price-change" :class="{ 'positive': stockInfo.changePercent >= 0, 'negative': stockInfo.changePercent < 0 }">
|
||||
{{ stockInfo.changePercent >= 0 ? '+' : '' }}{{ stockInfo.changePercent?.toFixed(2) }}%
|
||||
({{ stockInfo.changePercent >= 0 ? '+' : '' }}{{ stockInfo.changeAmount?.toFixed(2) }})
|
||||
</div>
|
||||
</div>
|
||||
<div class="stock-metrics">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">成交量</div>
|
||||
<div class="metric-value">{{ formatVolume(stockInfo.volume) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">市值</div>
|
||||
<div class="metric-value">{{ formatMarketCap(stockInfo.marketCap) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">最高价</div>
|
||||
<div class="metric-value">¥{{ stockInfo.highPrice?.toFixed(2) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">最低价</div>
|
||||
<div class="metric-value">¥{{ stockInfo.lowPrice?.toFixed(2) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 图表和分析区域 -->
|
||||
<el-row :gutter="20" class="charts-section">
|
||||
<!-- 历史价格走势 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>历史价格走势</span>
|
||||
<div class="chart-controls">
|
||||
<el-select v-model="historyDays" @change="loadHistoryData" size="small" style="width: 120px;">
|
||||
<el-option label="7天" :value="7" />
|
||||
<el-option label="30天" :value="30" />
|
||||
<el-option label="90天" :value="90" />
|
||||
<el-option label="180天" :value="180" />
|
||||
<el-option label="1年" :value="365" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div id="price-chart" style="height: 400px;" v-loading="historyLoading"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 趋势分析 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>趋势分析</span>
|
||||
<div class="chart-controls">
|
||||
<el-select v-model="trendDays" @change="loadTrendData" size="small" style="width: 120px;">
|
||||
<el-option label="7天" :value="7" />
|
||||
<el-option label="30天" :value="30" />
|
||||
<el-option label="60天" :value="60" />
|
||||
<el-option label="90天" :value="90" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="trend-content" v-loading="trendLoading">
|
||||
<div v-if="trendData.trend" class="trend-summary">
|
||||
<div class="trend-direction" :class="trendData.trend">
|
||||
<el-icon v-if="trendData.trend === 'up'"><CaretTop /></el-icon>
|
||||
<el-icon v-else-if="trendData.trend === 'down'"><CaretBottom /></el-icon>
|
||||
<el-icon v-else><Minus /></el-icon>
|
||||
{{ getTrendText(trendData.trend) }}
|
||||
</div>
|
||||
<div class="trend-details">
|
||||
<p><strong>趋势强度:</strong> {{ trendData.strength || '中等' }}</p>
|
||||
<p><strong>支撑位:</strong> ¥{{ trendData.supportLevel?.toFixed(2) }}</p>
|
||||
<p><strong>阻力位:</strong> ¥{{ trendData.resistanceLevel?.toFixed(2) }}</p>
|
||||
<p><strong>建议:</strong> {{ trendData.recommendation || '持续观察' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="trend-chart" style="height: 300px;"></div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 预测数据 -->
|
||||
<el-row class="prediction-section">
|
||||
<el-col :span="24">
|
||||
<el-card class="prediction-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>股票预测</span>
|
||||
<div class="prediction-controls">
|
||||
<el-select v-model="predictionDays" @change="loadPredictionData" size="small" style="width: 120px;">
|
||||
<el-option label="3天" :value="3" />
|
||||
<el-option label="7天" :value="7" />
|
||||
<el-option label="15天" :value="15" />
|
||||
<el-option label="30天" :value="30" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="loadPredictionData" :loading="predictionLoading">
|
||||
生成预测
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="prediction-content">
|
||||
<div class="prediction-chart-container">
|
||||
<div id="prediction-chart" style="height: 400px;" v-loading="predictionLoading"></div>
|
||||
</div>
|
||||
<div class="prediction-summary" v-if="predictionData.length > 0">
|
||||
<el-alert
|
||||
:title="`预测未来${predictionDays}天的股价走势`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-top: 20px;"
|
||||
/>
|
||||
<div class="prediction-stats">
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="6">
|
||||
<div class="prediction-stat">
|
||||
<div class="stat-label">预测最高价</div>
|
||||
<div class="stat-value positive">¥{{ getPredictionMax() }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="prediction-stat">
|
||||
<div class="stat-label">预测最低价</div>
|
||||
<div class="stat-value negative">¥{{ getPredictionMin() }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="prediction-stat">
|
||||
<div class="stat-label">平均预测价</div>
|
||||
<div class="stat-value">¥{{ getPredictionAvg() }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="prediction-stat">
|
||||
<div class="stat-label">预期收益率</div>
|
||||
<div class="stat-value" :class="{ 'positive': getPredictionReturn() > 0, 'negative': getPredictionReturn() < 0 }">
|
||||
{{ getPredictionReturn() > 0 ? '+' : '' }}{{ getPredictionReturn() }}%
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getStockHistory, getStockTrend, getStockPrediction, searchStocks } from '@/api/stock'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'StockDetail',
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const stockCode = ref(route.params.stockCode)
|
||||
const stockInfo = ref({})
|
||||
const historyData = ref([])
|
||||
const trendData = ref({})
|
||||
const predictionData = ref([])
|
||||
|
||||
const loading = ref(false)
|
||||
const historyLoading = ref(false)
|
||||
const trendLoading = ref(false)
|
||||
const predictionLoading = ref(false)
|
||||
|
||||
const historyDays = ref(30)
|
||||
const trendDays = ref(30)
|
||||
const predictionDays = ref(7)
|
||||
|
||||
// 加载股票基本信息
|
||||
const loadStockInfo = async () => {
|
||||
try {
|
||||
const response = await searchStocks(stockCode.value)
|
||||
if (response.data && response.data.length > 0) {
|
||||
stockInfo.value = response.data[0]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载股票信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载历史数据
|
||||
const loadHistoryData = async () => {
|
||||
try {
|
||||
historyLoading.value = true
|
||||
const endDate = new Date()
|
||||
const startDate = new Date()
|
||||
startDate.setDate(endDate.getDate() - historyDays.value)
|
||||
|
||||
const response = await getStockHistory(
|
||||
stockCode.value,
|
||||
startDate.toISOString(),
|
||||
endDate.toISOString()
|
||||
)
|
||||
if (response.data) {
|
||||
historyData.value = response.data
|
||||
await nextTick()
|
||||
initPriceChart()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史数据失败:', error)
|
||||
ElMessage.error('加载历史数据失败')
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载趋势数据
|
||||
const loadTrendData = async () => {
|
||||
try {
|
||||
trendLoading.value = true
|
||||
const response = await getStockTrend(stockCode.value, trendDays.value)
|
||||
if (response.data) {
|
||||
trendData.value = response.data
|
||||
await nextTick()
|
||||
initTrendChart()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载趋势数据失败:', error)
|
||||
ElMessage.error('加载趋势数据失败')
|
||||
} finally {
|
||||
trendLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载预测数据
|
||||
const loadPredictionData = async () => {
|
||||
try {
|
||||
predictionLoading.value = true
|
||||
const response = await getStockPrediction(stockCode.value, predictionDays.value)
|
||||
if (response.data) {
|
||||
predictionData.value = response.data
|
||||
await nextTick()
|
||||
initPredictionChart()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载预测数据失败:', error)
|
||||
ElMessage.error('加载预测数据失败')
|
||||
} finally {
|
||||
predictionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化价格图表
|
||||
const initPriceChart = async () => {
|
||||
const chartDom = document.getElementById('price-chart')
|
||||
if (!chartDom || historyData.value.length === 0) return
|
||||
|
||||
const myChart = echarts.init(chartDom)
|
||||
const dates = historyData.value.map(item => item.timestamp?.split('T')[0] || item.date)
|
||||
const prices = historyData.value.map(item => item.currentPrice)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '价格走势',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params) => {
|
||||
const dataIndex = params[0].dataIndex
|
||||
const data = historyData.value[dataIndex]
|
||||
return `
|
||||
<div>
|
||||
<p>日期: ${dates[dataIndex]}</p>
|
||||
<p>价格: ¥${data.currentPrice?.toFixed(2)}</p>
|
||||
<p>涨跌幅: ${data.changePercent?.toFixed(2)}%</p>
|
||||
<p>成交量: ${formatVolume(data.volume)}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '价格(¥)',
|
||||
scale: true
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: prices,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#409EFF',
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
color: 'rgba(64, 158, 255, 0.1)'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化趋势图表
|
||||
const initTrendChart = async () => {
|
||||
const chartDom = document.getElementById('trend-chart')
|
||||
if (!chartDom || !trendData.value.priceHistory) return
|
||||
|
||||
const myChart = echarts.init(chartDom)
|
||||
const data = trendData.value.priceHistory || []
|
||||
const dates = data.map(item => item.date)
|
||||
const prices = data.map(item => item.price)
|
||||
const ma5 = data.map(item => item.ma5)
|
||||
const ma20 = data.map(item => item.ma20)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['股价', 'MA5', 'MA20']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '价格(¥)',
|
||||
scale: true
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '股价',
|
||||
data: prices,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: { color: '#409EFF' }
|
||||
},
|
||||
{
|
||||
name: 'MA5',
|
||||
data: ma5,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: { color: '#67C23A' }
|
||||
},
|
||||
{
|
||||
name: 'MA20',
|
||||
data: ma20,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: { color: '#E6A23C' }
|
||||
}
|
||||
]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
}
|
||||
|
||||
// 初始化预测图表
|
||||
const initPredictionChart = async () => {
|
||||
const chartDom = document.getElementById('prediction-chart')
|
||||
if (!chartDom || predictionData.value.length === 0) return
|
||||
|
||||
const myChart = echarts.init(chartDom)
|
||||
|
||||
// 历史数据(最近10天)
|
||||
const historyDates = historyData.value.slice(-10).map(item => item.timestamp?.split('T')[0] || item.date)
|
||||
const historyPrices = historyData.value.slice(-10).map(item => item.currentPrice)
|
||||
|
||||
// 预测数据
|
||||
const predictionDates = predictionData.value.map(item => item.timestamp?.split('T')[0] || item.date)
|
||||
const predictionPrices = predictionData.value.map(item => item.currentPrice)
|
||||
|
||||
const allDates = [...historyDates, ...predictionDates]
|
||||
const historySeries = [...historyPrices, ...new Array(predictionDates.length).fill(null)]
|
||||
const predictionSeries = [...new Array(historyDates.length).fill(null), ...predictionPrices]
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '股价预测',
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['历史价格', '预测价格']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: allDates
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '价格(¥)',
|
||||
scale: true
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '历史价格',
|
||||
data: historySeries,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: { color: '#409EFF' },
|
||||
areaStyle: { color: 'rgba(64, 158, 255, 0.1)' }
|
||||
},
|
||||
{
|
||||
name: '预测价格',
|
||||
data: predictionSeries,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
color: '#F56C6C',
|
||||
type: 'dashed'
|
||||
},
|
||||
areaStyle: { color: 'rgba(245, 108, 108, 0.1)' }
|
||||
}
|
||||
]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
}
|
||||
|
||||
// 格式化函数
|
||||
const formatVolume = (volume) => {
|
||||
if (!volume) return '0手'
|
||||
if (volume >= 100000000) {
|
||||
return (volume / 100000000).toFixed(1) + '亿手'
|
||||
} else if (volume >= 10000) {
|
||||
return (volume / 10000).toFixed(1) + '万手'
|
||||
} else {
|
||||
return volume + '手'
|
||||
}
|
||||
}
|
||||
|
||||
const formatMarketCap = (marketCap) => {
|
||||
if (!marketCap) return '0元'
|
||||
if (marketCap >= 100000000) {
|
||||
return (marketCap / 100000000).toFixed(1) + '亿元'
|
||||
} else if (marketCap >= 10000) {
|
||||
return (marketCap / 10000).toFixed(1) + '万元'
|
||||
} else {
|
||||
return marketCap + '元'
|
||||
}
|
||||
}
|
||||
|
||||
const getTrendText = (trend) => {
|
||||
switch (trend) {
|
||||
case 'up': return '上升趋势'
|
||||
case 'down': return '下降趋势'
|
||||
default: return '震荡整理'
|
||||
}
|
||||
}
|
||||
|
||||
// 预测统计
|
||||
const getPredictionMax = () => {
|
||||
if (predictionData.value.length === 0) return '0.00'
|
||||
return Math.max(...predictionData.value.map(item => item.currentPrice)).toFixed(2)
|
||||
}
|
||||
|
||||
const getPredictionMin = () => {
|
||||
if (predictionData.value.length === 0) return '0.00'
|
||||
return Math.min(...predictionData.value.map(item => item.currentPrice)).toFixed(2)
|
||||
}
|
||||
|
||||
const getPredictionAvg = () => {
|
||||
if (predictionData.value.length === 0) return '0.00'
|
||||
const avg = predictionData.value.reduce((sum, item) => sum + item.currentPrice, 0) / predictionData.value.length
|
||||
return avg.toFixed(2)
|
||||
}
|
||||
|
||||
const getPredictionReturn = () => {
|
||||
if (predictionData.value.length === 0 || !stockInfo.value.currentPrice) return 0
|
||||
const lastPrediction = predictionData.value[predictionData.value.length - 1]
|
||||
const returnRate = ((lastPrediction.currentPrice - stockInfo.value.currentPrice) / stockInfo.value.currentPrice) * 100
|
||||
return returnRate.toFixed(2)
|
||||
}
|
||||
|
||||
// 刷新所有数据
|
||||
const refreshData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
loadStockInfo(),
|
||||
loadHistoryData(),
|
||||
loadTrendData(),
|
||||
loadPredictionData()
|
||||
])
|
||||
ElMessage.success('数据刷新成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('数据刷新失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadStockInfo()
|
||||
await loadHistoryData()
|
||||
await loadTrendData()
|
||||
await loadPredictionData()
|
||||
})
|
||||
|
||||
return {
|
||||
stockCode,
|
||||
stockInfo,
|
||||
historyData,
|
||||
trendData,
|
||||
predictionData,
|
||||
loading,
|
||||
historyLoading,
|
||||
trendLoading,
|
||||
predictionLoading,
|
||||
historyDays,
|
||||
trendDays,
|
||||
predictionDays,
|
||||
loadHistoryData,
|
||||
loadTrendData,
|
||||
loadPredictionData,
|
||||
formatVolume,
|
||||
formatMarketCap,
|
||||
getTrendText,
|
||||
getPredictionMax,
|
||||
getPredictionMin,
|
||||
getPredictionAvg,
|
||||
getPredictionReturn,
|
||||
refreshData,
|
||||
goBack
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stock-detail {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.stock-info {
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.stock-overview {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.overview-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.price-section {
|
||||
.current-price {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.price-change {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
||||
&.positive {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stock-metrics {
|
||||
flex: 1;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
text-align: center;
|
||||
|
||||
.metric-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card, .prediction-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-controls, .prediction-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trend-content {
|
||||
.trend-summary {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.trend-direction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&.up {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.flat {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-details {
|
||||
p {
|
||||
margin: 5px 0;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prediction-content {
|
||||
.prediction-chart-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.prediction-stats {
|
||||
.prediction-stat {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
||||
&.positive {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
542
frontend/src/views/StockSearch.vue
Normal file
542
frontend/src/views/StockSearch.vue
Normal file
@@ -0,0 +1,542 @@
|
||||
<template>
|
||||
<div class="stock-search">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>股票搜索</h1>
|
||||
<p>搜索和查看股票详细信息</p>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-section">
|
||||
<el-card class="search-card">
|
||||
<div class="search-header">
|
||||
<h3>股票搜索</h3>
|
||||
</div>
|
||||
<div class="search-form">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="18">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="请输入股票代码或股票名称"
|
||||
size="large"
|
||||
@keyup.enter="handleSearch"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleSearch"
|
||||
:loading="searchLoading"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<div class="search-results" v-if="searchResults.length > 0">
|
||||
<el-card class="results-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>搜索结果 ({{ searchResults.length }}条)</span>
|
||||
<el-button type="text" @click="clearSearch">清空结果</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="searchResults" stripe @row-click="viewStockDetail">
|
||||
<el-table-column prop="stockCode" label="股票代码" width="120" />
|
||||
<el-table-column prop="stockName" label="股票名称" width="200" />
|
||||
<el-table-column prop="currentPrice" label="当前价格" width="120">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.currentPrice?.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changePercent" label="涨跌幅" width="120">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.changePercent >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.changePercent >= 0 ? '+' : '' }}{{ scope.row.changePercent?.toFixed(2) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changeAmount" label="涨跌额" width="100">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.changeAmount >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.changeAmount >= 0 ? '+' : '' }}{{ scope.row.changeAmount?.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="volume" label="成交量" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatVolume(scope.row.volume) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="marketCap" label="市值" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatMarketCap(scope.row.marketCap) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="viewStockDetail(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="viewTrend(scope.row)">
|
||||
趋势
|
||||
</el-button>
|
||||
<el-button type="warning" size="small" @click="viewPrediction(scope.row)">
|
||||
预测
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 实时股票数据 -->
|
||||
<div class="realtime-section">
|
||||
<el-card class="realtime-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>实时股票数据</span>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" size="small" @click="loadRealtimeData" :loading="realtimeLoading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-switch
|
||||
v-model="autoRefresh"
|
||||
active-text="自动刷新"
|
||||
@change="toggleAutoRefresh"
|
||||
style="margin-left: 10px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="realtime-stats" v-if="realtimeData.length > 0">
|
||||
<el-row :gutter="16" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<div class="stat-item up">
|
||||
<div class="stat-value">{{ getUpCount() }}</div>
|
||||
<div class="stat-label">上涨股票</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item down">
|
||||
<div class="stat-value">{{ getDownCount() }}</div>
|
||||
<div class="stat-label">下跌股票</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item volume">
|
||||
<div class="stat-value">{{ getTotalVolume() }}</div>
|
||||
<div class="stat-label">总成交量</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item market-cap">
|
||||
<div class="stat-value">{{ getTotalMarketCap() }}</div>
|
||||
<div class="stat-label">总市值</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-table :data="paginatedRealtimeData" v-loading="realtimeLoading" stripe>
|
||||
<el-table-column prop="stockCode" label="股票代码" width="100" />
|
||||
<el-table-column prop="stockName" label="股票名称" width="150" />
|
||||
<el-table-column prop="currentPrice" label="当前价格" width="100">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.currentPrice?.toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changePercent" label="涨跌幅" width="100">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.changePercent >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.changePercent >= 0 ? '+' : '' }}{{ scope.row.changePercent?.toFixed(2) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="changeAmount" label="涨跌额" width="100">
|
||||
<template #default="scope">
|
||||
<span :style="{ color: scope.row.changeAmount >= 0 ? '#f56c6c' : '#67c23a' }">
|
||||
{{ scope.row.changeAmount >= 0 ? '+' : '' }}{{ scope.row.changeAmount?.toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="volume" label="成交量" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatVolume(scope.row.volume) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="marketCap" label="市值" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatMarketCap(scope.row.marketCap) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="viewStockDetail(scope.row)">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="viewTrend(scope.row)">
|
||||
趋势
|
||||
</el-button>
|
||||
<el-button type="warning" size="small" @click="viewPrediction(scope.row)">
|
||||
预测
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="realtimeData.length > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="realtimeData.length"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { searchStocks, getRealtimeStockData } from '@/api/stock'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
export default {
|
||||
name: 'StockSearch',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const searchKeyword = ref('')
|
||||
const searchResults = ref([])
|
||||
const searchLoading = ref(false)
|
||||
const realtimeData = ref([])
|
||||
const realtimeLoading = ref(false)
|
||||
const autoRefresh = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
let refreshTimer = null
|
||||
|
||||
// 搜索股票
|
||||
const handleSearch = async () => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
ElMessage.warning('请输入搜索关键词')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
searchLoading.value = true
|
||||
const response = await searchStocks(searchKeyword.value.trim())
|
||||
if (response.data) {
|
||||
searchResults.value = response.data
|
||||
if (response.data.length === 0) {
|
||||
ElMessage.info('未找到相关股票')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索股票失败:', error)
|
||||
ElMessage.error('搜索失败')
|
||||
} finally {
|
||||
searchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空搜索结果
|
||||
const clearSearch = () => {
|
||||
searchResults.value = []
|
||||
searchKeyword.value = ''
|
||||
}
|
||||
|
||||
// 加载实时数据
|
||||
const loadRealtimeData = async () => {
|
||||
try {
|
||||
realtimeLoading.value = true
|
||||
const response = await getRealtimeStockData()
|
||||
if (response.data) {
|
||||
realtimeData.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载实时数据失败:', error)
|
||||
ElMessage.error('加载实时数据失败')
|
||||
} finally {
|
||||
realtimeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换自动刷新
|
||||
const toggleAutoRefresh = (value) => {
|
||||
if (value) {
|
||||
refreshTimer = setInterval(() => {
|
||||
loadRealtimeData()
|
||||
}, 30000) // 30秒刷新一次
|
||||
ElMessage.success('已开启自动刷新,每30秒更新一次')
|
||||
} else {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
ElMessage.info('已关闭自动刷新')
|
||||
}
|
||||
}
|
||||
|
||||
// 分页数据
|
||||
const paginatedRealtimeData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return realtimeData.value.slice(start, end)
|
||||
})
|
||||
|
||||
// 分页事件
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val
|
||||
}
|
||||
|
||||
// 统计函数
|
||||
const getUpCount = () => {
|
||||
return realtimeData.value.filter(item => item.changePercent > 0).length
|
||||
}
|
||||
|
||||
const getDownCount = () => {
|
||||
return realtimeData.value.filter(item => item.changePercent < 0).length
|
||||
}
|
||||
|
||||
const getTotalVolume = () => {
|
||||
const total = realtimeData.value.reduce((sum, item) => sum + (item.volume || 0), 0)
|
||||
return formatVolume(total)
|
||||
}
|
||||
|
||||
const getTotalMarketCap = () => {
|
||||
const total = realtimeData.value.reduce((sum, item) => sum + (item.marketCap || 0), 0)
|
||||
return formatMarketCap(total)
|
||||
}
|
||||
|
||||
// 格式化函数
|
||||
const formatVolume = (volume) => {
|
||||
if (!volume) return '0手'
|
||||
if (volume >= 100000000) {
|
||||
return (volume / 100000000).toFixed(1) + '亿手'
|
||||
} else if (volume >= 10000) {
|
||||
return (volume / 10000).toFixed(1) + '万手'
|
||||
} else {
|
||||
return volume + '手'
|
||||
}
|
||||
}
|
||||
|
||||
const formatMarketCap = (marketCap) => {
|
||||
if (!marketCap) return '0元'
|
||||
if (marketCap >= 100000000) {
|
||||
return (marketCap / 100000000).toFixed(1) + '亿元'
|
||||
} else if (marketCap >= 10000) {
|
||||
return (marketCap / 10000).toFixed(1) + '万元'
|
||||
} else {
|
||||
return marketCap + '元'
|
||||
}
|
||||
}
|
||||
|
||||
// 查看股票详情
|
||||
const viewStockDetail = (stock) => {
|
||||
router.push(`/stock/${stock.stockCode}`)
|
||||
}
|
||||
|
||||
// 查看股票趋势
|
||||
const viewTrend = (stock) => {
|
||||
router.push(`/stock/${stock.stockCode}/trend`)
|
||||
}
|
||||
|
||||
// 查看股票预测
|
||||
const viewPrediction = (stock) => {
|
||||
router.push(`/stock/${stock.stockCode}/prediction`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRealtimeData()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
searchKeyword,
|
||||
searchResults,
|
||||
searchLoading,
|
||||
realtimeData,
|
||||
realtimeLoading,
|
||||
autoRefresh,
|
||||
currentPage,
|
||||
pageSize,
|
||||
paginatedRealtimeData,
|
||||
handleSearch,
|
||||
clearSearch,
|
||||
loadRealtimeData,
|
||||
toggleAutoRefresh,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
getUpCount,
|
||||
getDownCount,
|
||||
getTotalVolume,
|
||||
getTotalMarketCap,
|
||||
formatVolume,
|
||||
formatMarketCap,
|
||||
viewStockDetail,
|
||||
viewTrend,
|
||||
viewPrediction
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stock-search {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-card, .results-card, .realtime-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.realtime-stats {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
|
||||
&.up {
|
||||
background: linear-gradient(135deg, #f56c6c, #ff8a80);
|
||||
}
|
||||
|
||||
&.down {
|
||||
background: linear-gradient(135deg, #67c23a, #81c784);
|
||||
}
|
||||
|
||||
&.volume {
|
||||
background: linear-gradient(135deg, #409eff, #64b5f6);
|
||||
}
|
||||
|
||||
&.market-cap {
|
||||
background: linear-gradient(135deg, #e6a23c, #ffb74d);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-table__header {
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
74
frontend/vite.config.js
Normal file
74
frontend/vite.config.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { resolve } from 'path'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// 为了兼容一些依赖包
|
||||
global: 'globalThis',
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
'/ws': {
|
||||
target: 'ws://localhost:8080',
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
chunkSizeWarningLimit: 1600,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(fileURLToPath(new URL('./', import.meta.url)), 'index.html')
|
||||
},
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
'vuex',
|
||||
'axios',
|
||||
'element-plus',
|
||||
'@element-plus/icons-vue',
|
||||
'echarts',
|
||||
'dayjs'
|
||||
],
|
||||
},
|
||||
})
|
||||
193
logs/agricultural-stock-platform.log
Normal file
193
logs/agricultural-stock-platform.log
Normal file
@@ -0,0 +1,193 @@
|
||||
2025-06-04 20:18:02 [main] INFO c.a.s.AgriculturalStockPlatformApplication - Starting AgriculturalStockPlatformApplication using Java 1.8.0_202 on WIN11 with PID 10364 (D:\VScodeProject\work_4\backend\target\classes started by shenjianZ in D:\VScodeProject\work_4)
|
||||
2025-06-04 20:18:02 [main] INFO c.a.s.AgriculturalStockPlatformApplication - No active profile set, falling back to 1 default profile: "default"
|
||||
2025-06-04 20:18:05 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||
2025-06-04 20:18:05 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.63]
|
||||
2025-06-04 20:18:05 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||
2025-06-04 20:18:06 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||
2025-06-04 20:18:06 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 5.6.9.Final
|
||||
2025-06-04 20:18:06 [main] INFO o.h.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
|
||||
2025-06-04 20:18:06 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||
2025-06-04 20:18:06 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
|
||||
2025-06-04 20:18:06 [main] INFO org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
2025-06-04 20:18:07 [main] INFO o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
|
||||
2025-06-04 20:18:08 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'stockController' defined in file [D:\VScodeProject\work_4\backend\target\classes\com\agricultural\stock\controller\StockController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.agricultural.stock.service.StockService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
|
||||
2025-06-04 20:18:08 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
||||
2025-06-04 20:18:08 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
||||
2025-06-04 20:18:08 [main] INFO o.a.catalina.core.StandardService - Stopping service [Tomcat]
|
||||
2025-06-04 20:18:08 [main] ERROR o.s.b.d.LoggingFailureAnalysisReporter -
|
||||
|
||||
***************************
|
||||
APPLICATION FAILED TO START
|
||||
***************************
|
||||
|
||||
Description:
|
||||
|
||||
Parameter 0 of constructor in com.agricultural.stock.controller.StockController required a bean of type 'com.agricultural.stock.service.StockService' that could not be found.
|
||||
|
||||
|
||||
Action:
|
||||
|
||||
Consider defining a bean of type 'com.agricultural.stock.service.StockService' in your configuration.
|
||||
|
||||
2025-06-04 20:29:56 [main] INFO c.a.s.AgriculturalStockPlatformApplication - Starting AgriculturalStockPlatformApplication using Java 1.8.0_202 on WIN11 with PID 22200 (D:\VScodeProject\work_4\backend\target\classes started by shenjianZ in D:\VScodeProject\work_4)
|
||||
2025-06-04 20:29:56 [main] INFO c.a.s.AgriculturalStockPlatformApplication - No active profile set, falling back to 1 default profile: "default"
|
||||
2025-06-04 20:29:58 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||
2025-06-04 20:29:58 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.63]
|
||||
2025-06-04 20:29:59 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||
2025-06-04 20:29:59 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||
2025-06-04 20:29:59 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 5.6.9.Final
|
||||
2025-06-04 20:29:59 [main] INFO o.h.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
|
||||
2025-06-04 20:29:59 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||
2025-06-04 20:29:59 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
|
||||
2025-06-04 20:29:59 [main] INFO org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
2025-06-04 20:30:00 [main] INFO o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
|
||||
2025-06-04 20:30:01 [main] WARN o.s.b.a.o.j.JpaBaseConfiguration$JpaWebConfiguration - spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
|
||||
2025-06-04 20:30:02 [main] WARN c.b.m.core.metadata.TableInfoHelper - Can not find table primary key in Class: "com.agricultural.stock.entity.TechnicalIndicator".
|
||||
2025-06-04 20:30:02 [main] WARN c.b.m.c.injector.DefaultSqlInjector - class com.agricultural.stock.entity.TechnicalIndicator ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.
|
||||
2025-06-04 20:30:02 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
|
||||
2025-06-04 20:30:02 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
||||
2025-06-04 20:30:02 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
||||
2025-06-04 20:30:02 [main] INFO o.a.catalina.core.StandardService - Stopping service [Tomcat]
|
||||
2025-06-04 20:30:02 [main] ERROR o.s.boot.SpringApplication - Application run failed
|
||||
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356)
|
||||
at java.lang.Iterable.forEach(Iterable.java:75)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)
|
||||
at com.agricultural.stock.AgriculturalStockPlatformApplication.main(AgriculturalStockPlatformApplication.java:27)
|
||||
Caused by: java.lang.NullPointerException: null
|
||||
at springfox.documentation.spring.web.WebMvcPatternsRequestConditionWrapper.getPatterns(WebMvcPatternsRequestConditionWrapper.java:56)
|
||||
at springfox.documentation.RequestHandler.sortedPaths(RequestHandler.java:113)
|
||||
at springfox.documentation.spi.service.contexts.Orderings.lambda$byPatternsCondition$3(Orderings.java:89)
|
||||
at java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:469)
|
||||
at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
|
||||
at java.util.TimSort.sort(TimSort.java:220)
|
||||
at java.util.Arrays.sort(Arrays.java:1512)
|
||||
at java.util.ArrayList.sort(ArrayList.java:1462)
|
||||
at java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:387)
|
||||
at java.util.stream.Sink$ChainedReference.end(Sink.java:258)
|
||||
at java.util.stream.Sink$ChainedReference.end(Sink.java:258)
|
||||
at java.util.stream.Sink$ChainedReference.end(Sink.java:258)
|
||||
at java.util.stream.Sink$ChainedReference.end(Sink.java:258)
|
||||
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
|
||||
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
|
||||
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
|
||||
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
|
||||
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
|
||||
at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:81)
|
||||
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
|
||||
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382)
|
||||
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
|
||||
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
|
||||
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
|
||||
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
|
||||
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
|
||||
at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.withDefaults(AbstractDocumentationPluginsBootstrapper.java:107)
|
||||
at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.buildContext(AbstractDocumentationPluginsBootstrapper.java:91)
|
||||
at springfox.documentation.spring.web.plugins.AbstractDocumentationPluginsBootstrapper.bootstrapDocumentationPlugins(AbstractDocumentationPluginsBootstrapper.java:82)
|
||||
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:100)
|
||||
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178)
|
||||
... 14 common frames omitted
|
||||
2025-06-04 20:30:56 [main] INFO c.a.s.AgriculturalStockPlatformApplication - Starting AgriculturalStockPlatformApplication using Java 1.8.0_202 on WIN11 with PID 19328 (D:\VScodeProject\work_4\backend\target\classes started by shenjianZ in D:\VScodeProject\work_4)
|
||||
2025-06-04 20:30:56 [main] INFO c.a.s.AgriculturalStockPlatformApplication - No active profile set, falling back to 1 default profile: "default"
|
||||
2025-06-04 20:30:59 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||
2025-06-04 20:30:59 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.63]
|
||||
2025-06-04 20:30:59 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||
2025-06-04 20:31:00 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||
2025-06-04 20:31:00 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 5.6.9.Final
|
||||
2025-06-04 20:31:00 [main] INFO o.h.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
|
||||
2025-06-04 20:31:00 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||
2025-06-04 20:31:00 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
|
||||
2025-06-04 20:31:01 [main] INFO org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
2025-06-04 20:31:01 [main] INFO o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
|
||||
2025-06-04 20:31:01 [main] WARN o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'marketAnalysisController' defined in file [D:\VScodeProject\work_4\backend\target\classes\com\agricultural\stock\controller\MarketAnalysisController.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.agricultural.stock.controller.MarketAnalysisController]: Constructor threw exception; nested exception is java.lang.Error: Unresolved compilation problems:
|
||||
Api cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
|
||||
2025-06-04 20:31:01 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
||||
2025-06-04 20:31:01 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
||||
2025-06-04 20:31:01 [main] INFO o.a.catalina.core.StandardService - Stopping service [Tomcat]
|
||||
2025-06-04 20:31:01 [main] ERROR o.s.boot.SpringApplication - Application run failed
|
||||
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'marketAnalysisController' defined in file [D:\VScodeProject\work_4\backend\target\classes\com\agricultural\stock\controller\MarketAnalysisController.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.agricultural.stock.controller.MarketAnalysisController]: Constructor threw exception; nested exception is java.lang.Error: Unresolved compilation problems:
|
||||
Api cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1334)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953)
|
||||
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
|
||||
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147)
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734)
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)
|
||||
at com.agricultural.stock.AgriculturalStockPlatformApplication.main(AgriculturalStockPlatformApplication.java:27)
|
||||
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.agricultural.stock.controller.MarketAnalysisController]: Constructor threw exception; nested exception is java.lang.Error: Unresolved compilation problems:
|
||||
Api cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
|
||||
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:224)
|
||||
at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:87)
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1326)
|
||||
... 17 common frames omitted
|
||||
Caused by: java.lang.Error: Unresolved compilation problems:
|
||||
Api cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
ApiOperation cannot be resolved to a type
|
||||
|
||||
at com.agricultural.stock.controller.MarketAnalysisController.<init>(MarketAnalysisController.java:22)
|
||||
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
|
||||
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
|
||||
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
|
||||
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
|
||||
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:211)
|
||||
... 19 common frames omitted
|
||||
2025-06-04 20:33:00 [main] INFO c.a.s.AgriculturalStockPlatformApplication - Starting AgriculturalStockPlatformApplication using Java 1.8.0_202 on WIN11 with PID 11952 (D:\VScodeProject\work_4\backend\target\classes started by shenjianZ in D:\VScodeProject\work_4)
|
||||
2025-06-04 20:33:00 [main] INFO c.a.s.AgriculturalStockPlatformApplication - No active profile set, falling back to 1 default profile: "default"
|
||||
2025-06-04 20:33:01 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
|
||||
2025-06-04 20:33:01 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.63]
|
||||
2025-06-04 20:33:02 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
|
||||
2025-06-04 20:33:02 [main] INFO o.h.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: default]
|
||||
2025-06-04 20:33:02 [main] INFO org.hibernate.Version - HHH000412: Hibernate ORM core version 5.6.9.Final
|
||||
2025-06-04 20:33:02 [main] INFO o.h.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
|
||||
2025-06-04 20:33:02 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
|
||||
2025-06-04 20:33:02 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
|
||||
2025-06-04 20:33:02 [main] INFO org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
2025-06-04 20:33:03 [main] INFO o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
|
||||
2025-06-04 20:33:03 [main] WARN o.s.b.a.o.j.JpaBaseConfiguration$JpaWebConfiguration - spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
|
||||
2025-06-04 20:33:04 [main] WARN c.b.m.core.metadata.TableInfoHelper - Can not find table primary key in Class: "com.agricultural.stock.entity.TechnicalIndicator".
|
||||
2025-06-04 20:33:04 [main] WARN c.b.m.c.injector.DefaultSqlInjector - class com.agricultural.stock.entity.TechnicalIndicator ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.
|
||||
2025-06-04 20:33:04 [main] INFO c.a.s.AgriculturalStockPlatformApplication - Started AgriculturalStockPlatformApplication in 5.049 seconds (JVM running for 5.764)
|
||||
2025-06-04 20:33:11 [http-nio-8080-exec-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||
2025-06-04 20:35:00 [http-nio-8080-exec-7] INFO o.s.api.AbstractOpenApiResource - Init duration for springdoc-openapi is: 534 ms
|
||||
2025-06-04 21:09:37 [http-nio-8080-exec-10] INFO c.a.s.service.impl.StockServiceImpl - 股票预测功能暂未实现,股票代码: sz300630, 预测天数: 7
|
||||
2025-06-04 21:09:52 [http-nio-8080-exec-9] INFO c.a.s.service.impl.StockServiceImpl - 股票预测功能暂未实现,股票代码: sz300630, 预测天数: 7
|
||||
2025-06-04 21:09:56 [http-nio-8080-exec-10] INFO c.a.s.service.impl.StockServiceImpl - 股票预测功能暂未实现,股票代码: sz300630, 预测天数: 7
|
||||
2025-06-04 21:10:00 [http-nio-8080-exec-9] INFO c.a.s.service.impl.StockServiceImpl - 股票预测功能暂未实现,股票代码: sz300630, 预测天数: 7
|
||||
2025-06-04 21:13:44 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
|
||||
2025-06-04 21:13:44 [SpringApplicationShutdownHook] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
|
||||
192
spark-processor/README_DATABASE.md
Normal file
192
spark-processor/README_DATABASE.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# 🗄️ 农业股票数据处理器 - MySQL数据库存储
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本文档说明如何配置和使用农业股票数据处理器的MySQL数据库存储功能。处理器现在支持将Spark分析结果直接保存到MySQL数据库中,而不仅仅是文件系统。
|
||||
|
||||
## 🔧 数据库配置
|
||||
|
||||
### 1. 数据库表结构
|
||||
|
||||
首先需要创建必要的数据库表。执行以下SQL脚本:
|
||||
|
||||
```bash
|
||||
# 执行扩展表结构脚本
|
||||
mysql -u your_username -p your_database < database_tables.sql
|
||||
```
|
||||
|
||||
### 2. 配置文件
|
||||
|
||||
在 `application.conf` 中配置数据库连接(如果文件不存在,程序会使用默认值):
|
||||
|
||||
```hocon
|
||||
mysql {
|
||||
host = "localhost"
|
||||
port = 3306
|
||||
database = "agricultural_stock"
|
||||
user = "root"
|
||||
password = "your_password"
|
||||
}
|
||||
|
||||
spark {
|
||||
master = "local[*]"
|
||||
}
|
||||
|
||||
output {
|
||||
path = "/tmp/spark-output" # 备用文件输出路径
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 数据存储结构
|
||||
|
||||
### 处理结果存储到以下表:
|
||||
|
||||
#### 1. `market_analysis` - 市场分析表
|
||||
存储每日市场总览数据:
|
||||
- 上涨/下跌/平盘股票数量
|
||||
- 总市值、成交量、成交额
|
||||
- 平均涨跌幅
|
||||
|
||||
#### 2. `stock_technical_indicators` - 技术指标表
|
||||
存储股票技术指标:
|
||||
- 移动平均线 (MA5, MA10, MA20, MA30)
|
||||
- RSI相对强弱指标
|
||||
- MACD指标 (DIF, DEA)
|
||||
- 布林带 (上轨、中轨、下轨)
|
||||
|
||||
#### 3. `industry_analysis` - 行业分析表
|
||||
存储行业表现数据:
|
||||
- 行业平均涨跌幅
|
||||
- 行业股票数量
|
||||
- 行业总市值和成交量
|
||||
|
||||
#### 4. `market_trends` - 市场趋势表
|
||||
存储历史趋势数据:
|
||||
- 每日平均价格和涨跌幅
|
||||
- 每日总成交量和成交额
|
||||
|
||||
## 🚀 运行方式
|
||||
|
||||
### 1. 标准运行
|
||||
```bash
|
||||
# 编译项目
|
||||
mvn clean compile
|
||||
|
||||
# 运行处理器(批处理模式)
|
||||
mvn exec:java -Dexec.mainClass="com.agricultural.spark.StockDataProcessor"
|
||||
```
|
||||
|
||||
### 2. 打包运行
|
||||
```bash
|
||||
# 打包成可执行JAR
|
||||
mvn package
|
||||
|
||||
# 运行JAR包
|
||||
java -jar target/spark-data-processor-1.0.0.jar
|
||||
```
|
||||
|
||||
## 📈 数据流程
|
||||
|
||||
```
|
||||
MySQL stock_data (输入)
|
||||
↓
|
||||
数据清洗和处理
|
||||
↓
|
||||
技术指标计算
|
||||
↓
|
||||
市场分析计算
|
||||
↓
|
||||
保存到MySQL数据库表
|
||||
↓ ↓
|
||||
主要表 备用文件
|
||||
```
|
||||
|
||||
## 🔍 数据查询示例
|
||||
|
||||
### 查看最新市场分析
|
||||
```sql
|
||||
SELECT * FROM market_analysis
|
||||
ORDER BY analysis_date DESC
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
### 查看特定股票技术指标
|
||||
```sql
|
||||
SELECT stock_name, trade_date, close_price, ma5, ma20, rsi
|
||||
FROM stock_technical_indicators
|
||||
WHERE stock_code = 'sz000876'
|
||||
ORDER BY trade_date DESC
|
||||
LIMIT 30;
|
||||
```
|
||||
|
||||
### 查看行业表现排行
|
||||
```sql
|
||||
SELECT industry, avg_change_percent, stock_count, total_market_cap
|
||||
FROM industry_analysis
|
||||
WHERE analysis_date = (SELECT MAX(analysis_date) FROM industry_analysis)
|
||||
ORDER BY avg_change_percent DESC;
|
||||
```
|
||||
|
||||
### 查看市场趋势
|
||||
```sql
|
||||
SELECT trade_date, avg_change_percent, total_volume
|
||||
FROM market_trends
|
||||
ORDER BY trade_date DESC
|
||||
LIMIT 30;
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 错误处理
|
||||
- 程序会首先测试数据库连接
|
||||
- 如果数据库连接失败,会回退到文件保存模式
|
||||
- 所有数据库操作都有异常处理,不会中断主流程
|
||||
|
||||
### 2. 性能考虑
|
||||
- 技术指标数据量较大,采用批量写入方式
|
||||
- 市场分析数据使用 `ON DUPLICATE KEY UPDATE` 避免重复
|
||||
- 添加了必要的数据库索引优化查询性能
|
||||
|
||||
### 3. 数据一致性
|
||||
- `market_analysis` 表按日期去重
|
||||
- 技术指标表按股票代码和日期建立组合索引
|
||||
- 所有表都包含创建时间和更新时间字段
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### 1. 数据库连接失败
|
||||
检查配置文件中的数据库连接信息:
|
||||
- 主机地址和端口
|
||||
- 数据库名称
|
||||
- 用户名和密码
|
||||
- 确保MySQL服务正在运行
|
||||
|
||||
### 2. 表不存在错误
|
||||
确保已执行 `database_tables.sql` 脚本创建所有必要的表。
|
||||
|
||||
### 3. 权限问题
|
||||
确保数据库用户具有以下权限:
|
||||
- SELECT (读取 stock_data)
|
||||
- INSERT (写入分析结果)
|
||||
- UPDATE (更新已有数据)
|
||||
|
||||
### 4. 内存不足
|
||||
对于大量数据,可能需要调整Spark配置:
|
||||
```bash
|
||||
# 增加内存限制
|
||||
export SPARK_DRIVER_MEMORY=2g
|
||||
export SPARK_EXECUTOR_MEMORY=2g
|
||||
```
|
||||
|
||||
## 📝 日志说明
|
||||
|
||||
程序运行时会输出详细日志,包括:
|
||||
- 数据库连接状态
|
||||
- 各阶段处理进度
|
||||
- 数据保存结果
|
||||
- 错误信息和回退操作
|
||||
|
||||
关键日志信息:
|
||||
- `数据库连接测试成功` - 数据库连接正常
|
||||
- `市场分析结果保存成功` - 数据已保存到数据库
|
||||
- `保存到数据库失败,回退到文件保存` - 使用备用保存方式
|
||||
88
spark-processor/database_tables.sql
Normal file
88
spark-processor/database_tables.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
-- ================================
|
||||
-- 农业股票数据处理系统 - 扩展表结构
|
||||
-- 用于存储Spark处理后的分析结果
|
||||
-- ================================
|
||||
|
||||
-- 技术指标数据表
|
||||
CREATE TABLE IF NOT EXISTS stock_technical_indicators (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
stock_code VARCHAR(10) NOT NULL COMMENT '股票代码',
|
||||
stock_name VARCHAR(100) NOT NULL COMMENT '股票名称',
|
||||
trade_date DATE NOT NULL COMMENT '交易日期',
|
||||
close_price DECIMAL(10,2) COMMENT '收盘价',
|
||||
ma5 DECIMAL(10,2) COMMENT '5日移动平均',
|
||||
ma10 DECIMAL(10,2) COMMENT '10日移动平均',
|
||||
ma20 DECIMAL(10,2) COMMENT '20日移动平均',
|
||||
ma30 DECIMAL(10,2) COMMENT '30日移动平均',
|
||||
rsi DECIMAL(5,2) COMMENT 'RSI相对强弱指标',
|
||||
macd_dif DECIMAL(10,4) COMMENT 'MACD DIF值',
|
||||
macd_dea DECIMAL(10,4) COMMENT 'MACD DEA值',
|
||||
bb_upper DECIMAL(10,2) COMMENT '布林带上轨',
|
||||
bb_middle DECIMAL(10,2) COMMENT '布林带中轨',
|
||||
bb_lower DECIMAL(10,2) COMMENT '布林带下轨',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='股票技术指标表';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_technical_stock ON stock_technical_indicators(stock_code);
|
||||
CREATE INDEX idx_technical_date ON stock_technical_indicators(trade_date);
|
||||
CREATE INDEX idx_technical_stock_date ON stock_technical_indicators(stock_code, trade_date);
|
||||
|
||||
-- 行业分析表
|
||||
CREATE TABLE IF NOT EXISTS industry_analysis (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
industry VARCHAR(50) NOT NULL COMMENT '行业名称',
|
||||
analysis_date DATE NOT NULL COMMENT '分析日期',
|
||||
stock_count INT COMMENT '股票数量',
|
||||
avg_change_percent DECIMAL(5,2) COMMENT '平均涨跌幅',
|
||||
total_market_cap DECIMAL(15,2) COMMENT '行业总市值',
|
||||
total_volume BIGINT COMMENT '行业总成交量',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='行业分析表';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_industry_date ON industry_analysis(analysis_date);
|
||||
CREATE INDEX idx_industry_name ON industry_analysis(industry);
|
||||
CREATE INDEX idx_industry_date_name ON industry_analysis(analysis_date, industry);
|
||||
|
||||
-- 市场趋势表
|
||||
CREATE TABLE IF NOT EXISTS market_trends (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
trade_date DATE NOT NULL COMMENT '交易日期',
|
||||
avg_price DECIMAL(10,2) COMMENT '平均价格',
|
||||
avg_change_percent DECIMAL(5,2) COMMENT '平均涨跌幅',
|
||||
total_volume BIGINT COMMENT '总成交量',
|
||||
total_turnover DECIMAL(15,2) COMMENT '总成交额',
|
||||
stock_count INT COMMENT '股票数量',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场趋势表';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_trends_date ON market_trends(trade_date);
|
||||
|
||||
-- 为已有的market_analysis表添加唯一索引(防止重复数据)
|
||||
CREATE UNIQUE INDEX idx_market_analysis_date ON market_analysis(analysis_date);
|
||||
|
||||
-- ================================
|
||||
-- 示例查询语句
|
||||
-- ================================
|
||||
|
||||
-- 查询最新的市场分析数据
|
||||
-- SELECT * FROM market_analysis ORDER BY analysis_date DESC LIMIT 1;
|
||||
|
||||
-- 查询特定股票的技术指标
|
||||
-- SELECT * FROM stock_technical_indicators
|
||||
-- WHERE stock_code = 'sz000876'
|
||||
-- ORDER BY trade_date DESC LIMIT 30;
|
||||
|
||||
-- 查询行业表现排行
|
||||
-- SELECT industry, avg_change_percent, stock_count
|
||||
-- FROM industry_analysis
|
||||
-- WHERE analysis_date = (SELECT MAX(analysis_date) FROM industry_analysis)
|
||||
-- ORDER BY avg_change_percent DESC;
|
||||
|
||||
-- 查询市场趋势
|
||||
-- SELECT trade_date, avg_change_percent, total_volume
|
||||
-- FROM market_trends
|
||||
-- ORDER BY trade_date DESC LIMIT 30;
|
||||
169
spark-processor/pom.xml
Normal file
169
spark-processor/pom.xml
Normal file
@@ -0,0 +1,169 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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.agricultural</groupId>
|
||||
<artifactId>spark-data-processor</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Agricultural Stock Spark Processor</name>
|
||||
<description>基于Apache Spark的农业股票数据处理器</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spark.version>3.4.0</spark.version>
|
||||
<scala.binary.version>2.12</scala.binary.version>
|
||||
|
||||
<mysql.version>8.0.33</mysql.version>
|
||||
<jackson.version>2.14.3</jackson.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Apache Spark Core -->
|
||||
<dependency>
|
||||
<groupId>org.apache.spark</groupId>
|
||||
<artifactId>spark-core_${scala.binary.version}</artifactId>
|
||||
<version>${spark.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Spark SQL -->
|
||||
<dependency>
|
||||
<groupId>org.apache.spark</groupId>
|
||||
<artifactId>spark-sql_${scala.binary.version}</artifactId>
|
||||
<version>${spark.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Spark Streaming -->
|
||||
<dependency>
|
||||
<groupId>org.apache.spark</groupId>
|
||||
<artifactId>spark-streaming_${scala.binary.version}</artifactId>
|
||||
<version>${spark.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spark Kafka Integration - 已移除 -->
|
||||
|
||||
<!-- MySQL JDBC Driver -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>${mysql.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson for JSON processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Configuration -->
|
||||
<dependency>
|
||||
<groupId>com.typesafe</groupId>
|
||||
<artifactId>config</artifactId>
|
||||
<version>1.4.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.36</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.2.12</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>4.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- 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 Shade Plugin for fat JAR -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>com.agricultural.spark.StockDataProcessor</mainClass>
|
||||
</transformer>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Maven Surefire Plugin for tests -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,228 @@
|
||||
package com.agricultural.spark;
|
||||
|
||||
import com.agricultural.spark.config.SparkConfig;
|
||||
import com.agricultural.spark.service.DataCleaningService;
|
||||
import com.agricultural.spark.service.DatabaseSaveService;
|
||||
import com.agricultural.spark.service.MarketAnalysisService;
|
||||
// StreamProcessingService 已移除 (Kafka相关)
|
||||
import com.agricultural.spark.service.TechnicalIndicatorService;
|
||||
import org.apache.spark.sql.Dataset;
|
||||
import org.apache.spark.sql.Row;
|
||||
import org.apache.spark.sql.SparkSession;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 农业股票数据处理器主类
|
||||
* 基于Apache Spark的大数据处理平台
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
public class StockDataProcessor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StockDataProcessor.class);
|
||||
|
||||
private final SparkSession spark;
|
||||
private final SparkConfig config;
|
||||
private final DataCleaningService dataCleaningService;
|
||||
private final MarketAnalysisService marketAnalysisService;
|
||||
private final TechnicalIndicatorService technicalIndicatorService;
|
||||
private final DatabaseSaveService databaseSaveService;
|
||||
// StreamProcessingService 已移除
|
||||
|
||||
public StockDataProcessor(SparkConfig config) {
|
||||
this.config = config;
|
||||
this.spark = initializeSparkSession();
|
||||
this.dataCleaningService = new DataCleaningService(spark);
|
||||
this.marketAnalysisService = new MarketAnalysisService(spark);
|
||||
this.technicalIndicatorService = new TechnicalIndicatorService(spark);
|
||||
this.databaseSaveService = new DatabaseSaveService(spark, config);
|
||||
// StreamProcessingService 初始化已移除
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Spark会话
|
||||
*/
|
||||
private SparkSession initializeSparkSession() {
|
||||
logger.info("正在初始化Spark会话...");
|
||||
|
||||
SparkSession.Builder builder = SparkSession.builder()
|
||||
.appName("AgriculturalStockDataProcessor")
|
||||
.config("spark.sql.adaptive.enabled", "true")
|
||||
.config("spark.sql.adaptive.coalescePartitions.enabled", "true")
|
||||
.config("spark.sql.warehouse.dir", "/tmp/spark-warehouse");
|
||||
|
||||
// 如果是本地模式
|
||||
if (config.getSparkMaster().startsWith("local")) {
|
||||
builder.master(config.getSparkMaster());
|
||||
}
|
||||
|
||||
SparkSession session = builder.getOrCreate();
|
||||
session.sparkContext().setLogLevel("WARN");
|
||||
|
||||
logger.info("Spark会话初始化成功");
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MySQL加载股票数据
|
||||
*/
|
||||
public Dataset<Row> loadStockDataFromMySQL() {
|
||||
logger.info("从MySQL加载股票数据...");
|
||||
|
||||
String jdbcUrl = String.format("jdbc:mysql://%s:%d/%s?useSSL=false&serverTimezone=Asia/Shanghai",
|
||||
config.getMysqlHost(), config.getMysqlPort(), config.getMysqlDatabase());
|
||||
|
||||
Dataset<Row> df = spark.read()
|
||||
.format("jdbc")
|
||||
.option("url", jdbcUrl)
|
||||
.option("dbtable", "stock_data")
|
||||
.option("user", config.getMysqlUser())
|
||||
.option("password", config.getMysqlPassword())
|
||||
.option("driver", "com.mysql.cj.jdbc.Driver")
|
||||
.load();
|
||||
|
||||
long count = df.count();
|
||||
logger.info("从MySQL加载数据完成,共 {} 条记录", count);
|
||||
return df;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批处理分析
|
||||
*/
|
||||
public void runBatchProcessing() {
|
||||
logger.info("开始执行批处理分析...");
|
||||
|
||||
try {
|
||||
// 0. 测试数据库连接
|
||||
if (!databaseSaveService.testConnection()) {
|
||||
logger.warn("数据库连接测试失败,将仅保存到文件系统");
|
||||
}
|
||||
|
||||
// 1. 加载数据
|
||||
Dataset<Row> rawData = loadStockDataFromMySQL();
|
||||
|
||||
// 2. 数据清洗
|
||||
Dataset<Row> cleanedData = dataCleaningService.cleanData(rawData);
|
||||
|
||||
// 3. 市场总览分析
|
||||
Map<String, Object> marketOverview = marketAnalysisService.analyzeMarketOverview(cleanedData);
|
||||
logger.info("市场总览分析完成: {}", marketOverview);
|
||||
|
||||
// 4. 涨跌幅排行分析
|
||||
Map<String, Object> rankingAnalysis = marketAnalysisService.analyzeTopGainersLosers(cleanedData, 10);
|
||||
logger.info("涨跌幅排行分析完成");
|
||||
|
||||
// 5. 行业表现分析
|
||||
Dataset<Row> industryAnalysis = marketAnalysisService.analyzeIndustryPerformance(cleanedData);
|
||||
logger.info("行业表现分析完成,共 {} 个行业", industryAnalysis.count());
|
||||
|
||||
// 6. 历史趋势分析
|
||||
Dataset<Row> trendAnalysis = marketAnalysisService.analyzeHistoricalTrends(cleanedData, null, 30);
|
||||
logger.info("历史趋势分析完成,共 {} 个数据点", trendAnalysis.count());
|
||||
|
||||
// 7. 技术指标计算
|
||||
Dataset<Row> dataWithIndicators = technicalIndicatorService.calculateTechnicalIndicators(cleanedData);
|
||||
logger.info("技术指标计算完成");
|
||||
|
||||
// 8. 保存处理结果到数据库
|
||||
try {
|
||||
databaseSaveService.saveMarketAnalysis(marketOverview);
|
||||
databaseSaveService.saveIndustryAnalysis(industryAnalysis);
|
||||
databaseSaveService.saveHistoricalTrends(trendAnalysis);
|
||||
databaseSaveService.saveTechnicalIndicators(dataWithIndicators);
|
||||
logger.info("数据已成功保存到MySQL数据库");
|
||||
} catch (Exception e) {
|
||||
logger.error("保存到数据库失败,回退到文件保存", e);
|
||||
// 回退到原有的文件保存方式
|
||||
saveResults(dataWithIndicators, "processed_data");
|
||||
saveAnalysisResults(marketOverview, "market_overview");
|
||||
}
|
||||
|
||||
logger.info("批处理分析完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("批处理分析过程中出现错误", e);
|
||||
throw new RuntimeException("批处理分析失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时流处理已移除(原Kafka功能)
|
||||
*/
|
||||
public void runStreamProcessing() {
|
||||
logger.warn("实时流处理功能已移除,仅支持批处理模式");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存处理结果到文件
|
||||
*/
|
||||
private void saveResults(Dataset<Row> data, String outputPath) {
|
||||
try {
|
||||
String fullPath = config.getOutputPath() + "/" + outputPath;
|
||||
data.write()
|
||||
.mode("overwrite")
|
||||
.parquet(fullPath);
|
||||
logger.info("数据已保存到: {}", fullPath);
|
||||
} catch (Exception e) {
|
||||
logger.error("保存数据失败: {}", outputPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存分析结果
|
||||
*/
|
||||
private void saveAnalysisResults(Map<String, Object> results, String outputPath) {
|
||||
try {
|
||||
String fullPath = config.getOutputPath() + "/" + outputPath + ".json";
|
||||
// 这里可以将Map转换为JSON并保存
|
||||
logger.info("分析结果已保存到: {}", fullPath);
|
||||
} catch (Exception e) {
|
||||
logger.error("保存分析结果失败: {}", outputPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭Spark会话
|
||||
*/
|
||||
public void close() {
|
||||
if (spark != null) {
|
||||
spark.stop();
|
||||
logger.info("Spark会话已关闭");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主方法
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
logger.info("农业股票数据处理器启动");
|
||||
|
||||
StockDataProcessor processor = null;
|
||||
try {
|
||||
// 加载配置
|
||||
SparkConfig config = SparkConfig.load();
|
||||
processor = new StockDataProcessor(config);
|
||||
|
||||
// 根据参数决定运行模式
|
||||
if (args.length > 0 && "stream".equals(args[0])) {
|
||||
// 流处理模式
|
||||
processor.runStreamProcessing();
|
||||
} else {
|
||||
// 批处理模式
|
||||
processor.runBatchProcessing();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("程序运行过程中出现错误", e);
|
||||
System.exit(1);
|
||||
} finally {
|
||||
if (processor != null) {
|
||||
processor.close();
|
||||
}
|
||||
logger.info("农业股票数据处理器已停止");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.agricultural.spark.config;
|
||||
|
||||
import com.typesafe.config.Config;
|
||||
import com.typesafe.config.ConfigFactory;
|
||||
|
||||
/**
|
||||
* Spark配置类
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
public class SparkConfig {
|
||||
|
||||
private final Config config;
|
||||
|
||||
private SparkConfig(Config config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public static SparkConfig load() {
|
||||
Config config = ConfigFactory.load();
|
||||
return new SparkConfig(config);
|
||||
}
|
||||
|
||||
public String getSparkMaster() {
|
||||
return config.hasPath("spark.master") ?
|
||||
config.getString("spark.master") : "local[*]";
|
||||
}
|
||||
|
||||
public String getMysqlHost() {
|
||||
return config.hasPath("mysql.host") ?
|
||||
config.getString("mysql.host") : "localhost";
|
||||
}
|
||||
|
||||
public int getMysqlPort() {
|
||||
return config.hasPath("mysql.port") ?
|
||||
config.getInt("mysql.port") : 3306;
|
||||
}
|
||||
|
||||
public String getMysqlDatabase() {
|
||||
return config.hasPath("mysql.database") ?
|
||||
config.getString("mysql.database") : "agricultural_stock";
|
||||
}
|
||||
|
||||
public String getMysqlUser() {
|
||||
return config.hasPath("mysql.user") ?
|
||||
config.getString("mysql.user") : "root";
|
||||
}
|
||||
|
||||
public String getMysqlPassword() {
|
||||
return config.hasPath("mysql.password") ?
|
||||
config.getString("mysql.password") : "root";
|
||||
}
|
||||
|
||||
// Kafka配置已移除
|
||||
|
||||
public String getOutputPath() {
|
||||
return config.hasPath("output.path") ?
|
||||
config.getString("output.path") : "/tmp/spark-output";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.agricultural.spark.service;
|
||||
|
||||
import org.apache.spark.sql.Dataset;
|
||||
import org.apache.spark.sql.Row;
|
||||
import org.apache.spark.sql.SparkSession;
|
||||
import org.apache.spark.sql.functions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.apache.spark.sql.functions.*;
|
||||
|
||||
/**
|
||||
* 数据清洗服务类
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
public class DataCleaningService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DataCleaningService.class);
|
||||
|
||||
private final SparkSession spark;
|
||||
|
||||
public DataCleaningService(SparkSession spark) {
|
||||
this.spark = spark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据清洗
|
||||
*
|
||||
* @param rawData 原始数据
|
||||
* @return 清洗后的数据
|
||||
*/
|
||||
public Dataset<Row> cleanData(Dataset<Row> rawData) {
|
||||
logger.info("开始执行数据清洗,原始数据条数: {}", rawData.count());
|
||||
|
||||
Dataset<Row> cleanedData = rawData;
|
||||
|
||||
// 1. 去除重复数据
|
||||
cleanedData = cleanedData.dropDuplicates("stock_code", "trade_date");
|
||||
logger.info("去重后数据条数: {}", cleanedData.count());
|
||||
|
||||
// 2. 处理缺失值
|
||||
cleanedData = handleMissingValues(cleanedData);
|
||||
|
||||
// 3. 数据类型转换
|
||||
cleanedData = convertDataTypes(cleanedData);
|
||||
|
||||
// 4. 异常值处理
|
||||
cleanedData = handleOutliers(cleanedData);
|
||||
|
||||
// 5. 计算派生字段
|
||||
cleanedData = calculateDerivedFields(cleanedData);
|
||||
|
||||
long finalCount = cleanedData.count();
|
||||
logger.info("数据清洗完成,最终数据条数: {}", finalCount);
|
||||
|
||||
return cleanedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理缺失值
|
||||
*/
|
||||
private Dataset<Row> handleMissingValues(Dataset<Row> data) {
|
||||
logger.info("处理缺失值...");
|
||||
|
||||
// 数值字段填充为0
|
||||
String[] numericCols = {
|
||||
"open_price", "close_price", "high_price", "low_price",
|
||||
"volume", "turnover", "change_percent", "change_amount",
|
||||
"pe_ratio", "pb_ratio", "market_cap", "float_market_cap"
|
||||
};
|
||||
|
||||
for (String col : numericCols) {
|
||||
if (hasColumn(data, col)) {
|
||||
data = data.na().fill(0.0, new String[]{col});
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串字段填充为空字符串
|
||||
String[] stringCols = {"stock_name"};
|
||||
for (String col : stringCols) {
|
||||
if (hasColumn(data, col)) {
|
||||
data = data.na().fill("", new String[]{col});
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据类型转换
|
||||
*/
|
||||
private Dataset<Row> convertDataTypes(Dataset<Row> data) {
|
||||
logger.info("执行数据类型转换...");
|
||||
|
||||
// 转换时间戳字段
|
||||
if (hasColumn(data, "trade_date")) {
|
||||
data = data.withColumn("trade_date",
|
||||
to_timestamp(col("trade_date"), "yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
|
||||
// 确保数值字段为正确的数据类型
|
||||
String[] numericCols = {
|
||||
"open_price", "close_price", "high_price", "low_price",
|
||||
"volume", "turnover", "change_percent", "change_amount",
|
||||
"pe_ratio", "pb_ratio", "market_cap", "float_market_cap"
|
||||
};
|
||||
|
||||
for (String colName : numericCols) {
|
||||
if (hasColumn(data, colName)) {
|
||||
data = data.withColumn(colName, col(colName).cast("double"));
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理异常值
|
||||
*/
|
||||
private Dataset<Row> handleOutliers(Dataset<Row> data) {
|
||||
logger.info("处理异常值...");
|
||||
|
||||
// 过滤异常数据
|
||||
data = data.filter(
|
||||
col("open_price").$greater$eq(0)
|
||||
.and(col("close_price").$greater$eq(0))
|
||||
.and(col("high_price").$greater$eq(0))
|
||||
.and(col("low_price").$greater$eq(0))
|
||||
.and(col("volume").$greater$eq(0))
|
||||
.and(col("high_price").$greater$eq(col("low_price")))
|
||||
);
|
||||
|
||||
// 过滤极端的涨跌幅数据(超过±20%的数据需要特别检查)
|
||||
data = data.filter(
|
||||
col("change_percent").$greater$eq(-20.0)
|
||||
.and(col("change_percent").$less$eq(20.0))
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算派生字段
|
||||
*/
|
||||
private Dataset<Row> calculateDerivedFields(Dataset<Row> data) {
|
||||
logger.info("计算派生字段...");
|
||||
|
||||
// 计算价格变动
|
||||
data = data.withColumn("price_change",
|
||||
col("close_price").minus(col("open_price")));
|
||||
|
||||
// 计算价格变动百分比
|
||||
data = data.withColumn("price_change_pct",
|
||||
when(col("open_price").notEqual(0),
|
||||
col("close_price").minus(col("open_price"))
|
||||
.divide(col("open_price")).multiply(100))
|
||||
.otherwise(0));
|
||||
|
||||
// 计算振幅
|
||||
data = data.withColumn("amplitude",
|
||||
when(col("open_price").notEqual(0),
|
||||
col("high_price").minus(col("low_price"))
|
||||
.divide(col("open_price")).multiply(100))
|
||||
.otherwise(0));
|
||||
|
||||
// 计算换手率(如果有流通股本数据)
|
||||
if (hasColumn(data, "float_shares")) {
|
||||
data = data.withColumn("turnover_rate",
|
||||
when(col("float_shares").notEqual(0),
|
||||
col("volume").divide(col("float_shares")).multiply(100))
|
||||
.otherwise(0));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查DataFrame是否包含指定列
|
||||
*/
|
||||
private boolean hasColumn(Dataset<Row> data, String columnName) {
|
||||
String[] columns = data.columns();
|
||||
for (String col : columns) {
|
||||
if (col.equals(columnName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.agricultural.spark.service;
|
||||
|
||||
import com.agricultural.spark.config.SparkConfig;
|
||||
import org.apache.spark.sql.Dataset;
|
||||
import org.apache.spark.sql.Row;
|
||||
import org.apache.spark.sql.SaveMode;
|
||||
import org.apache.spark.sql.SparkSession;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 数据库保存服务类
|
||||
* 负责将Spark处理结果保存到MySQL数据库
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
public class DatabaseSaveService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DatabaseSaveService.class);
|
||||
|
||||
private final SparkSession spark;
|
||||
private final SparkConfig config;
|
||||
private final String jdbcUrl;
|
||||
private final Properties connectionProps;
|
||||
|
||||
public DatabaseSaveService(SparkSession spark, SparkConfig config) {
|
||||
this.spark = spark;
|
||||
this.config = config;
|
||||
this.jdbcUrl = String.format("jdbc:mysql://%s:%d/%s?useSSL=false&serverTimezone=Asia/Shanghai",
|
||||
config.getMysqlHost(), config.getMysqlPort(), config.getMysqlDatabase());
|
||||
|
||||
this.connectionProps = new Properties();
|
||||
this.connectionProps.put("user", config.getMysqlUser());
|
||||
this.connectionProps.put("password", config.getMysqlPassword());
|
||||
this.connectionProps.put("driver", "com.mysql.cj.jdbc.Driver");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存市场分析结果到market_analysis表
|
||||
*/
|
||||
public void saveMarketAnalysis(Map<String, Object> marketOverview) {
|
||||
logger.info("开始保存市场分析结果到数据库...");
|
||||
|
||||
String sql = "INSERT INTO market_analysis (analysis_date, up_count, down_count, flat_count, " +
|
||||
"total_count, total_market_cap, total_volume, total_turnover, avg_change_percent) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE " +
|
||||
"up_count = VALUES(up_count), down_count = VALUES(down_count), " +
|
||||
"flat_count = VALUES(flat_count), total_count = VALUES(total_count), " +
|
||||
"total_market_cap = VALUES(total_market_cap), total_volume = VALUES(total_volume), " +
|
||||
"total_turnover = VALUES(total_turnover), avg_change_percent = VALUES(avg_change_percent)";
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(jdbcUrl, connectionProps);
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
// 解析交易日期
|
||||
String tradeDateStr = (String) marketOverview.get("trade_date");
|
||||
LocalDate tradeDate = LocalDate.parse(tradeDateStr.substring(0, 10)); // 提取日期部分
|
||||
|
||||
stmt.setDate(1, java.sql.Date.valueOf(tradeDate));
|
||||
stmt.setLong(2, ((Number) marketOverview.get("up_count")).longValue());
|
||||
stmt.setLong(3, ((Number) marketOverview.get("down_count")).longValue());
|
||||
stmt.setLong(4, ((Number) marketOverview.get("flat_count")).longValue());
|
||||
stmt.setLong(5, ((Number) marketOverview.get("total_count")).longValue());
|
||||
stmt.setDouble(6, ((Number) marketOverview.get("total_market_cap")).doubleValue());
|
||||
stmt.setLong(7, ((Number) marketOverview.get("total_volume")).longValue());
|
||||
stmt.setDouble(8, ((Number) marketOverview.get("total_turnover")).doubleValue());
|
||||
stmt.setDouble(9, ((Number) marketOverview.get("avg_change_percent")).doubleValue());
|
||||
|
||||
int rowsAffected = stmt.executeUpdate();
|
||||
logger.info("市场分析结果保存成功,影响 {} 行数据", rowsAffected);
|
||||
|
||||
} catch (SQLException e) {
|
||||
logger.error("保存市场分析结果失败", e);
|
||||
throw new RuntimeException("保存市场分析结果到数据库失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存处理后的技术指标数据到新表(可选)
|
||||
*/
|
||||
public void saveTechnicalIndicators(Dataset<Row> dataWithIndicators) {
|
||||
logger.info("开始保存技术指标数据到数据库...");
|
||||
|
||||
try {
|
||||
// 选择需要保存的字段
|
||||
Dataset<Row> selectedData = dataWithIndicators.select(
|
||||
"stock_code", "stock_name", "trade_date", "close_price",
|
||||
"ma5", "ma10", "ma20", "ma30",
|
||||
"rsi", "macd_dif", "macd_dea",
|
||||
"bb_upper", "bb_middle", "bb_lower"
|
||||
);
|
||||
|
||||
// 保存到stock_technical_indicators表(需要先创建此表)
|
||||
selectedData.write()
|
||||
.mode(SaveMode.Overwrite)
|
||||
.format("jdbc")
|
||||
.option("url", jdbcUrl)
|
||||
.option("dbtable", "stock_technical_indicators")
|
||||
.option("user", config.getMysqlUser())
|
||||
.option("password", config.getMysqlPassword())
|
||||
.option("driver", "com.mysql.cj.jdbc.Driver")
|
||||
.save();
|
||||
|
||||
logger.info("技术指标数据保存成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("保存技术指标数据失败", e);
|
||||
// 不抛出异常,允许程序继续执行
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存行业分析结果
|
||||
*/
|
||||
public void saveIndustryAnalysis(Dataset<Row> industryAnalysis) {
|
||||
logger.info("开始保存行业分析结果到数据库...");
|
||||
|
||||
try {
|
||||
// 添加分析日期字段
|
||||
Dataset<Row> industryWithDate = industryAnalysis.withColumn("analysis_date",
|
||||
org.apache.spark.sql.functions.current_date());
|
||||
|
||||
// 保存到industry_analysis表(需要先创建此表)
|
||||
industryWithDate.write()
|
||||
.mode(SaveMode.Append)
|
||||
.format("jdbc")
|
||||
.option("url", jdbcUrl)
|
||||
.option("dbtable", "industry_analysis")
|
||||
.option("user", config.getMysqlUser())
|
||||
.option("password", config.getMysqlPassword())
|
||||
.option("driver", "com.mysql.cj.jdbc.Driver")
|
||||
.save();
|
||||
|
||||
logger.info("行业分析结果保存成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("保存行业分析结果失败", e);
|
||||
// 不抛出异常,允许程序继续执行
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存历史趋势数据
|
||||
*/
|
||||
public void saveHistoricalTrends(Dataset<Row> trendAnalysis) {
|
||||
logger.info("开始保存历史趋势数据到数据库...");
|
||||
|
||||
try {
|
||||
// 保存到market_trends表(需要先创建此表)
|
||||
trendAnalysis.write()
|
||||
.mode(SaveMode.Overwrite)
|
||||
.format("jdbc")
|
||||
.option("url", jdbcUrl)
|
||||
.option("dbtable", "market_trends")
|
||||
.option("user", config.getMysqlUser())
|
||||
.option("password", config.getMysqlPassword())
|
||||
.option("driver", "com.mysql.cj.jdbc.Driver")
|
||||
.save();
|
||||
|
||||
logger.info("历史趋势数据保存成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("保存历史趋势数据失败", e);
|
||||
// 不抛出异常,允许程序继续执行
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试数据库连接
|
||||
*/
|
||||
public boolean testConnection() {
|
||||
try (Connection conn = DriverManager.getConnection(jdbcUrl, connectionProps)) {
|
||||
logger.info("数据库连接测试成功");
|
||||
return true;
|
||||
} catch (SQLException e) {
|
||||
logger.error("数据库连接测试失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.agricultural.spark.service;
|
||||
|
||||
import org.apache.spark.sql.Dataset;
|
||||
import org.apache.spark.sql.Row;
|
||||
import org.apache.spark.sql.SparkSession;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.spark.sql.functions.*;
|
||||
|
||||
/**
|
||||
* 市场分析服务类
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
public class MarketAnalysisService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MarketAnalysisService.class);
|
||||
|
||||
private final SparkSession spark;
|
||||
|
||||
public MarketAnalysisService(SparkSession spark) {
|
||||
this.spark = spark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 市场总览分析
|
||||
*/
|
||||
public Map<String, Object> analyzeMarketOverview(Dataset<Row> data) {
|
||||
logger.info("执行市场总览分析...");
|
||||
|
||||
// 获取最新交易日的数据
|
||||
Row latestDateRow = data.agg(max("trade_date")).head();
|
||||
if (latestDateRow.isNullAt(0)) {
|
||||
logger.warn("无法获取最新交易日期");
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
Object latestDate = latestDateRow.get(0);
|
||||
Dataset<Row> latestData = data.filter(col("trade_date").equalTo(latestDate));
|
||||
|
||||
// 统计涨跌股票数量
|
||||
long upCount = latestData.filter(col("change_percent").gt(0)).count();
|
||||
long downCount = latestData.filter(col("change_percent").lt(0)).count();
|
||||
long flatCount = latestData.filter(col("change_percent").equalTo(0)).count();
|
||||
long totalCount = latestData.count();
|
||||
|
||||
// 计算总市值和成交量
|
||||
Row aggregateRow = latestData.agg(
|
||||
sum("market_cap").alias("total_market_cap"),
|
||||
sum("volume").alias("total_volume"),
|
||||
sum("turnover").alias("total_turnover"),
|
||||
avg("change_percent").alias("avg_change_percent")
|
||||
).head();
|
||||
|
||||
double totalMarketCap = aggregateRow.isNullAt(0) ? 0.0 : aggregateRow.getDouble(0);
|
||||
long totalVolume = aggregateRow.isNullAt(1) ? 0L : Math.round(aggregateRow.getDouble(1));
|
||||
double totalTurnover = aggregateRow.isNullAt(2) ? 0.0 : aggregateRow.getDouble(2);
|
||||
double avgChangePercent = aggregateRow.isNullAt(3) ? 0.0 : aggregateRow.getDouble(3);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("trade_date", latestDate.toString());
|
||||
result.put("up_count", upCount);
|
||||
result.put("down_count", downCount);
|
||||
result.put("flat_count", flatCount);
|
||||
result.put("total_count", totalCount);
|
||||
result.put("total_market_cap", totalMarketCap);
|
||||
result.put("total_volume", totalVolume);
|
||||
result.put("total_turnover", totalTurnover);
|
||||
result.put("avg_change_percent", Math.round(avgChangePercent * 100.0) / 100.0);
|
||||
|
||||
logger.info("市场总览分析完成: {}", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析涨跌幅榜单
|
||||
*/
|
||||
public Map<String, Object> analyzeTopGainersLosers(Dataset<Row> data, int limit) {
|
||||
logger.info("分析涨跌幅榜单(前{}名)...", limit);
|
||||
|
||||
// 获取最新交易日数据
|
||||
Row latestDateRow = data.agg(max("trade_date")).head();
|
||||
Object latestDate = latestDateRow.get(0);
|
||||
Dataset<Row> latestData = data.filter(col("trade_date").equalTo(latestDate));
|
||||
|
||||
// 涨幅榜
|
||||
List<Row> topGainers = latestData
|
||||
.orderBy(col("change_percent").desc())
|
||||
.select("stock_code", "stock_name", "close_price", "change_percent",
|
||||
"volume", "market_cap")
|
||||
.limit(limit)
|
||||
.collectAsList();
|
||||
|
||||
// 跌幅榜
|
||||
List<Row> topLosers = latestData
|
||||
.orderBy(col("change_percent").asc())
|
||||
.select("stock_code", "stock_name", "close_price", "change_percent",
|
||||
"volume", "market_cap")
|
||||
.limit(limit)
|
||||
.collectAsList();
|
||||
|
||||
// 成交量榜
|
||||
List<Row> topVolume = latestData
|
||||
.orderBy(col("volume").desc())
|
||||
.select("stock_code", "stock_name", "close_price", "change_percent",
|
||||
"volume", "turnover")
|
||||
.limit(limit)
|
||||
.collectAsList();
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("top_gainers", topGainers);
|
||||
result.put("top_losers", topLosers);
|
||||
result.put("top_volume", topVolume);
|
||||
|
||||
logger.info("涨跌幅榜单分析完成");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行业表现分析
|
||||
*/
|
||||
public Dataset<Row> analyzeIndustryPerformance(Dataset<Row> data) {
|
||||
logger.info("执行行业表现分析...");
|
||||
|
||||
// 注册UDF函数用于行业分类
|
||||
spark.udf().register("getIndustry", (String stockCode) -> {
|
||||
if (stockCode == null || stockCode.length() < 6) {
|
||||
return "其他农业";
|
||||
}
|
||||
|
||||
String code = stockCode.substring(stockCode.length() - 6);
|
||||
switch (code) {
|
||||
case "000876":
|
||||
case "002714":
|
||||
return "畜牧业";
|
||||
case "600519":
|
||||
case "000858":
|
||||
case "600887":
|
||||
case "002304":
|
||||
return "食品饮料";
|
||||
default:
|
||||
return "其他农业";
|
||||
}
|
||||
}, org.apache.spark.sql.types.DataTypes.StringType);
|
||||
|
||||
// 添加行业字段
|
||||
Dataset<Row> dataWithIndustry = data.withColumn("industry",
|
||||
callUDF("getIndustry", col("stock_code")));
|
||||
|
||||
// 获取最新交易日数据
|
||||
Row latestDateRow = dataWithIndustry.agg(max("trade_date")).head();
|
||||
Object latestDate = latestDateRow.get(0);
|
||||
Dataset<Row> latestData = dataWithIndustry.filter(col("trade_date").equalTo(latestDate));
|
||||
|
||||
// 按行业统计
|
||||
Dataset<Row> industryStats = latestData.groupBy("industry")
|
||||
.agg(
|
||||
count("*").alias("stock_count"),
|
||||
avg("change_percent").alias("avg_change_percent"),
|
||||
sum("market_cap").alias("total_market_cap"),
|
||||
sum("volume").alias("total_volume")
|
||||
)
|
||||
.orderBy(col("avg_change_percent").desc());
|
||||
|
||||
logger.info("行业表现分析完成,共 {} 个行业", industryStats.count());
|
||||
return industryStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史趋势分析
|
||||
*/
|
||||
public Dataset<Row> analyzeHistoricalTrends(Dataset<Row> data, String stockCode, int days) {
|
||||
logger.info("分析历史趋势({}天)...", days);
|
||||
|
||||
// 计算起始日期
|
||||
Row latestDateRow = data.agg(max("trade_date")).head();
|
||||
Object endDate = latestDateRow.get(0);
|
||||
|
||||
// 这里简化处理,实际应该使用日期计算
|
||||
Dataset<Row> trendData = data;
|
||||
|
||||
if (stockCode != null && !stockCode.isEmpty()) {
|
||||
trendData = trendData.filter(col("stock_code").equalTo(stockCode));
|
||||
}
|
||||
|
||||
// 按日期聚合
|
||||
Dataset<Row> dailyStats = trendData.groupBy("trade_date")
|
||||
.agg(
|
||||
avg("close_price").alias("avg_price"),
|
||||
avg("change_percent").alias("avg_change_percent"),
|
||||
sum("volume").alias("total_volume"),
|
||||
sum("turnover").alias("total_turnover"),
|
||||
count("*").alias("stock_count")
|
||||
)
|
||||
.orderBy("trade_date");
|
||||
|
||||
logger.info("历史趋势分析完成");
|
||||
return dailyStats;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.agricultural.spark.service;
|
||||
|
||||
import org.apache.spark.sql.Dataset;
|
||||
import org.apache.spark.sql.Row;
|
||||
import org.apache.spark.sql.SparkSession;
|
||||
import org.apache.spark.sql.expressions.Window;
|
||||
import org.apache.spark.sql.expressions.WindowSpec;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static org.apache.spark.sql.functions.*;
|
||||
|
||||
/**
|
||||
* 技术指标服务类
|
||||
*
|
||||
* @author Agricultural Stock Platform Team
|
||||
*/
|
||||
public class TechnicalIndicatorService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TechnicalIndicatorService.class);
|
||||
|
||||
private final SparkSession spark;
|
||||
|
||||
public TechnicalIndicatorService(SparkSession spark) {
|
||||
this.spark = spark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算技术指标
|
||||
*/
|
||||
public Dataset<Row> calculateTechnicalIndicators(Dataset<Row> data) {
|
||||
logger.info("开始计算技术指标...");
|
||||
|
||||
// 按股票代码和日期排序的窗口
|
||||
WindowSpec windowSpec = Window.partitionBy("stock_code")
|
||||
.orderBy("trade_date")
|
||||
.rowsBetween(-29, 0); // 30天窗口
|
||||
|
||||
WindowSpec windowSpec5 = Window.partitionBy("stock_code")
|
||||
.orderBy("trade_date")
|
||||
.rowsBetween(-4, 0); // 5天窗口
|
||||
|
||||
WindowSpec windowSpec10 = Window.partitionBy("stock_code")
|
||||
.orderBy("trade_date")
|
||||
.rowsBetween(-9, 0); // 10天窗口
|
||||
|
||||
WindowSpec windowSpec20 = Window.partitionBy("stock_code")
|
||||
.orderBy("trade_date")
|
||||
.rowsBetween(-19, 0); // 20天窗口
|
||||
|
||||
Dataset<Row> result = data;
|
||||
|
||||
// 1. 移动平均线 (MA)
|
||||
result = result.withColumn("ma5", avg("close_price").over(windowSpec5))
|
||||
.withColumn("ma10", avg("close_price").over(windowSpec10))
|
||||
.withColumn("ma20", avg("close_price").over(windowSpec20))
|
||||
.withColumn("ma30", avg("close_price").over(windowSpec));
|
||||
|
||||
// 2. 成交量移动平均
|
||||
result = result.withColumn("volume_ma5", avg("volume").over(windowSpec5))
|
||||
.withColumn("volume_ma10", avg("volume").over(windowSpec10));
|
||||
|
||||
// 3. 计算价格相对于移动平均线的位置
|
||||
result = result.withColumn("price_vs_ma5",
|
||||
when(col("ma5").notEqual(0),
|
||||
col("close_price").divide(col("ma5")).multiply(100).minus(100))
|
||||
.otherwise(0))
|
||||
.withColumn("price_vs_ma20",
|
||||
when(col("ma20").notEqual(0),
|
||||
col("close_price").divide(col("ma20")).multiply(100).minus(100))
|
||||
.otherwise(0));
|
||||
|
||||
// 4. 计算波动率 (30天)
|
||||
result = result.withColumn("volatility_30d",
|
||||
stddev("change_percent").over(windowSpec));
|
||||
|
||||
// 5. 计算相对强弱指标 RSI (简化版)
|
||||
result = calculateRSI(result, 14);
|
||||
|
||||
// 6. 计算MACD指标
|
||||
result = calculateMACD(result);
|
||||
|
||||
// 7. 计算布林带
|
||||
result = calculateBollingerBands(result, 20);
|
||||
|
||||
logger.info("技术指标计算完成");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算RSI相对强弱指标
|
||||
*/
|
||||
private Dataset<Row> calculateRSI(Dataset<Row> data, int period) {
|
||||
WindowSpec windowSpec = Window.partitionBy("stock_code")
|
||||
.orderBy("trade_date")
|
||||
.rowsBetween(-(period-1), 0);
|
||||
|
||||
// 计算价格变化
|
||||
data = data.withColumn("price_diff",
|
||||
col("close_price").minus(lag("close_price", 1)
|
||||
.over(Window.partitionBy("stock_code").orderBy("trade_date"))));
|
||||
|
||||
// 计算涨跌
|
||||
data = data.withColumn("gain", when(col("price_diff").gt(0), col("price_diff")).otherwise(0))
|
||||
.withColumn("loss", when(col("price_diff").lt(0), abs(col("price_diff"))).otherwise(0));
|
||||
|
||||
// 计算平均涨跌
|
||||
data = data.withColumn("avg_gain", avg("gain").over(windowSpec))
|
||||
.withColumn("avg_loss", avg("loss").over(windowSpec));
|
||||
|
||||
// 计算RSI
|
||||
data = data.withColumn("rsi",
|
||||
when(col("avg_loss").notEqual(0),
|
||||
lit(100).minus(lit(100).divide(lit(1).plus(col("avg_gain").divide(col("avg_loss"))))))
|
||||
.otherwise(50));
|
||||
|
||||
return data.drop("price_diff", "gain", "loss", "avg_gain", "avg_loss");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算MACD指标
|
||||
*/
|
||||
private Dataset<Row> calculateMACD(Dataset<Row> data) {
|
||||
// 计算EMA12和EMA26
|
||||
data = data.withColumn("ema12", calculateEMA(col("close_price"), 12))
|
||||
.withColumn("ema26", calculateEMA(col("close_price"), 26));
|
||||
|
||||
// 计算MACD线 (DIF)
|
||||
data = data.withColumn("macd_dif", col("ema12").minus(col("ema26")));
|
||||
|
||||
// 计算DEA (MACD的9日EMA)
|
||||
data = data.withColumn("macd_dea", calculateEMA(col("macd_dif"), 9));
|
||||
|
||||
// 计算MACD柱状图
|
||||
data = data.withColumn("macd_histogram",
|
||||
col("macd_dif").minus(col("macd_dea")).multiply(2));
|
||||
|
||||
return data.drop("ema12", "ema26");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算布林带
|
||||
*/
|
||||
private Dataset<Row> calculateBollingerBands(Dataset<Row> data, int period) {
|
||||
WindowSpec windowSpec = Window.partitionBy("stock_code")
|
||||
.orderBy("trade_date")
|
||||
.rowsBetween(-(period-1), 0);
|
||||
|
||||
// 计算中轨(移动平均)
|
||||
data = data.withColumn("bb_middle", avg("close_price").over(windowSpec));
|
||||
|
||||
// 计算标准差
|
||||
data = data.withColumn("bb_std", stddev("close_price").over(windowSpec));
|
||||
|
||||
// 计算上轨和下轨
|
||||
data = data.withColumn("bb_upper",
|
||||
col("bb_middle").plus(col("bb_std").multiply(2)))
|
||||
.withColumn("bb_lower",
|
||||
col("bb_middle").minus(col("bb_std").multiply(2)));
|
||||
|
||||
// 计算布林带宽度
|
||||
data = data.withColumn("bb_width",
|
||||
when(col("bb_middle").notEqual(0),
|
||||
col("bb_upper").minus(col("bb_lower")).divide(col("bb_middle")).multiply(100))
|
||||
.otherwise(0));
|
||||
|
||||
// 计算价格在布林带中的位置
|
||||
data = data.withColumn("bb_position",
|
||||
when(col("bb_upper").notEqual(col("bb_lower")),
|
||||
col("close_price").minus(col("bb_lower"))
|
||||
.divide(col("bb_upper").minus(col("bb_lower"))))
|
||||
.otherwise(0.5));
|
||||
|
||||
return data.drop("bb_std");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算指数移动平均 (EMA) - 简化版
|
||||
*/
|
||||
private org.apache.spark.sql.Column calculateEMA(org.apache.spark.sql.Column priceCol, int period) {
|
||||
// 简化实现,实际应该使用更复杂的EMA计算
|
||||
WindowSpec windowSpec = Window.partitionBy("stock_code")
|
||||
.orderBy("trade_date")
|
||||
.rowsBetween(-(period-1), 0);
|
||||
|
||||
return avg(priceCol).over(windowSpec);
|
||||
}
|
||||
}
|
||||
52
spark-processor/src/main/resources/logback.xml
Normal file
52
spark-processor/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<!-- 控制台输出配置 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 文件输出配置(可选) -->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>logs/spark-processor.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>logs/spark-processor.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 设置特定包的日志级别 -->
|
||||
|
||||
<!-- 我们自己的应用程序日志 - 保持INFO级别 -->
|
||||
<logger name="com.agricultural.spark" level="INFO" additivity="false">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
</logger>
|
||||
|
||||
<!-- Spark框架日志 - 设置为WARN级别,减少输出 -->
|
||||
<logger name="org.apache.spark" level="WARN"/>
|
||||
<logger name="org.apache.hadoop" level="WARN"/>
|
||||
<logger name="org.apache.hive" level="WARN"/>
|
||||
<logger name="org.apache.parquet" level="WARN"/>
|
||||
|
||||
<!-- 数据库连接日志 - 设置为WARN级别 -->
|
||||
<logger name="com.mysql" level="WARN"/>
|
||||
<logger name="mysql" level="WARN"/>
|
||||
|
||||
<!-- Maven和其他框架日志 - 设置为ERROR级别 -->
|
||||
<logger name="org.apache.maven" level="ERROR"/>
|
||||
<logger name="org.eclipse.jetty" level="ERROR"/>
|
||||
<logger name="io.netty" level="ERROR"/>
|
||||
|
||||
<!-- 根日志级别 -->
|
||||
<root level="WARN">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
206
start-system.sh
Normal file
206
start-system.sh
Normal file
@@ -0,0 +1,206 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ===========================================
|
||||
# 农业股票数据分析系统启动脚本
|
||||
# ===========================================
|
||||
|
||||
echo "🌾 农业股票数据分析系统启动中..."
|
||||
echo "=========================================="
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 检查必要的软件
|
||||
check_requirements() {
|
||||
echo -e "${BLUE}📋 检查系统环境...${NC}"
|
||||
|
||||
# 检查Java
|
||||
if ! command -v java &> /dev/null; then
|
||||
echo -e "${RED}❌ 未安装Java,请先安装JDK 8+${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查Maven
|
||||
if ! command -v mvn &> /dev/null; then
|
||||
echo -e "${RED}❌ 未安装Maven,请先安装Maven${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查Node.js
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo -e "${RED}❌ 未安装Node.js,请先安装Node.js 16+${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查MySQL
|
||||
if ! command -v mysql &> /dev/null; then
|
||||
echo -e "${RED}❌ 未安装MySQL,请先安装MySQL 8.0+${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ 系统环境检查通过${NC}"
|
||||
}
|
||||
|
||||
# 初始化数据库
|
||||
init_database() {
|
||||
echo -e "${BLUE}🗄️ 初始化数据库...${NC}"
|
||||
|
||||
# 检查数据库连接
|
||||
mysql -u root -p -e "SELECT 1;" &> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}❌ 无法连接到MySQL数据库${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建数据库和表结构
|
||||
echo -e "${YELLOW}📝 创建数据库表结构...${NC}"
|
||||
mysql -u root -p agricultural_stock < spark-processor/database_tables.sql
|
||||
|
||||
echo -e "${GREEN}✅ 数据库初始化完成${NC}"
|
||||
}
|
||||
|
||||
# 启动Spark数据处理器
|
||||
start_spark_processor() {
|
||||
echo -e "${BLUE}⚡ 启动Spark数据处理器...${NC}"
|
||||
|
||||
cd spark-processor
|
||||
|
||||
# 编译项目
|
||||
echo -e "${YELLOW}🔨 编译Spark项目...${NC}"
|
||||
mvn clean compile > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}❌ Spark项目编译失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 运行数据处理
|
||||
echo -e "${YELLOW}📊 执行数据分析...${NC}"
|
||||
mvn exec:java -Dexec.mainClass="com.agricultural.spark.StockDataProcessor" > ../logs/spark.log 2>&1 &
|
||||
SPARK_PID=$!
|
||||
|
||||
cd ..
|
||||
echo -e "${GREEN}✅ Spark数据处理器已启动 (PID: $SPARK_PID)${NC}"
|
||||
}
|
||||
|
||||
# 启动后端服务
|
||||
start_backend() {
|
||||
echo -e "${BLUE}🚀 启动后端服务...${NC}"
|
||||
|
||||
cd backend
|
||||
|
||||
# 编译项目
|
||||
echo -e "${YELLOW}🔨 编译后端项目...${NC}"
|
||||
mvn clean compile > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}❌ 后端项目编译失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 启动Spring Boot应用
|
||||
echo -e "${YELLOW}🌐 启动Spring Boot应用...${NC}"
|
||||
mvn spring-boot:run > ../logs/backend.log 2>&1 &
|
||||
BACKEND_PID=$!
|
||||
|
||||
cd ..
|
||||
echo -e "${GREEN}✅ 后端服务已启动 (PID: $BACKEND_PID)${NC}"
|
||||
echo -e "${GREEN} API地址: http://localhost:8080${NC}"
|
||||
}
|
||||
|
||||
# 启动前端服务
|
||||
start_frontend() {
|
||||
echo -e "${BLUE}🎨 启动前端服务...${NC}"
|
||||
|
||||
cd frontend
|
||||
|
||||
# 安装依赖
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo -e "${YELLOW}📦 安装前端依赖...${NC}"
|
||||
npm install > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}❌ 前端依赖安装失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 启动开发服务器
|
||||
echo -e "${YELLOW}🌐 启动Vue开发服务器...${NC}"
|
||||
npm run dev > ../logs/frontend.log 2>&1 &
|
||||
FRONTEND_PID=$!
|
||||
|
||||
cd ..
|
||||
echo -e "${GREEN}✅ 前端服务已启动 (PID: $FRONTEND_PID)${NC}"
|
||||
echo -e "${GREEN} 访问地址: http://localhost:3000${NC}"
|
||||
}
|
||||
|
||||
# 创建日志目录
|
||||
create_log_dir() {
|
||||
if [ ! -d "logs" ]; then
|
||||
mkdir -p logs
|
||||
fi
|
||||
}
|
||||
|
||||
# 保存进程ID
|
||||
save_pids() {
|
||||
echo "$SPARK_PID" > logs/spark.pid
|
||||
echo "$BACKEND_PID" > logs/backend.pid
|
||||
echo "$FRONTEND_PID" > logs/frontend.pid
|
||||
echo -e "${BLUE}📝 进程ID已保存到logs目录${NC}"
|
||||
}
|
||||
|
||||
# 显示系统状态
|
||||
show_status() {
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 农业股票数据分析系统启动完成!${NC}"
|
||||
echo "=========================================="
|
||||
echo -e "${BLUE}📊 Spark数据处理器:${NC} PID $SPARK_PID"
|
||||
echo -e "${BLUE}🚀 后端API服务:${NC} http://localhost:8080"
|
||||
echo -e "${BLUE}🎨 前端Web界面:${NC} http://localhost:3000"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📋 系统功能:${NC}"
|
||||
echo " • 实时股票数据分析"
|
||||
echo " • 技术指标计算"
|
||||
echo " • 市场趋势预测"
|
||||
echo " • 数据可视化展示"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📝 查看日志:${NC}"
|
||||
echo " • Spark日志: tail -f logs/spark.log"
|
||||
echo " • 后端日志: tail -f logs/backend.log"
|
||||
echo " • 前端日志: tail -f logs/frontend.log"
|
||||
echo ""
|
||||
echo -e "${YELLOW}🛑 停止系统:${NC}"
|
||||
echo " • 运行: ./stop-system.sh"
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
check_requirements
|
||||
create_log_dir
|
||||
init_database
|
||||
|
||||
# 等待用户确认
|
||||
echo -e "${YELLOW}准备启动系统,按Enter继续...${NC}"
|
||||
read
|
||||
|
||||
start_spark_processor
|
||||
sleep 5 # 等待Spark处理完成
|
||||
|
||||
start_backend
|
||||
sleep 10 # 等待后端启动
|
||||
|
||||
start_frontend
|
||||
sleep 5 # 等待前端启动
|
||||
|
||||
save_pids
|
||||
show_status
|
||||
}
|
||||
|
||||
# 错误处理
|
||||
trap 'echo -e "${RED}❌ 启动过程中发生错误,正在清理...${NC}"; kill $SPARK_PID $BACKEND_PID $FRONTEND_PID 2>/dev/null; exit 1' ERR
|
||||
|
||||
# 执行主函数
|
||||
main
|
||||
315
start.sh
Normal file
315
start.sh
Normal file
@@ -0,0 +1,315 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 农业领域上市公司行情可视化监控平台启动脚本
|
||||
# 作者: Agricultural Stock Platform Team
|
||||
|
||||
echo "=========================================="
|
||||
echo "农业上市公司行情监控平台启动脚本"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查Docker和Docker Compose是否安装
|
||||
check_dependencies() {
|
||||
echo "检查依赖..."
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo "❌ Docker Compose 未安装,请先安装 Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Docker 和 Docker Compose 已安装"
|
||||
}
|
||||
|
||||
# 创建必要的目录
|
||||
create_directories() {
|
||||
echo "创建项目目录..."
|
||||
|
||||
mkdir -p sql
|
||||
mkdir -p nginx/conf.d
|
||||
mkdir -p data
|
||||
mkdir -p logs
|
||||
mkdir -p output
|
||||
|
||||
echo "✅ 目录创建完成"
|
||||
}
|
||||
|
||||
# 创建数据库初始化脚本
|
||||
create_init_sql() {
|
||||
echo "创建数据库初始化脚本..."
|
||||
|
||||
cat > sql/init.sql << 'EOF'
|
||||
-- 农业股票数据库初始化脚本
|
||||
CREATE DATABASE IF NOT EXISTS agricultural_stock CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE agricultural_stock;
|
||||
|
||||
-- 股票数据表
|
||||
CREATE TABLE IF NOT EXISTS stock_data (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
stock_code VARCHAR(10) NOT NULL COMMENT '股票代码',
|
||||
stock_name VARCHAR(100) NOT NULL COMMENT '股票名称',
|
||||
open_price DECIMAL(10,2) COMMENT '开盘价',
|
||||
close_price DECIMAL(10,2) COMMENT '收盘价',
|
||||
high_price DECIMAL(10,2) COMMENT '最高价',
|
||||
low_price DECIMAL(10,2) COMMENT '最低价',
|
||||
volume BIGINT COMMENT '成交量',
|
||||
turnover DECIMAL(15,2) COMMENT '成交额',
|
||||
change_percent DECIMAL(5,2) COMMENT '涨跌幅(%)',
|
||||
change_amount DECIMAL(10,2) COMMENT '涨跌额',
|
||||
total_shares BIGINT COMMENT '总股本',
|
||||
float_shares BIGINT COMMENT '流通股本',
|
||||
market_cap DECIMAL(15,2) COMMENT '总市值',
|
||||
float_market_cap DECIMAL(15,2) COMMENT '流通市值',
|
||||
pe_ratio DECIMAL(8,2) COMMENT '市盈率',
|
||||
pb_ratio DECIMAL(8,2) COMMENT '市净率',
|
||||
trade_date DATETIME NOT NULL COMMENT '交易日期',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted TINYINT DEFAULT 0 COMMENT '是否删除'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='股票数据表';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_stock_code ON stock_data(stock_code);
|
||||
CREATE INDEX idx_trade_date ON stock_data(trade_date);
|
||||
CREATE INDEX idx_stock_trade ON stock_data(stock_code, trade_date);
|
||||
|
||||
-- 预测数据表
|
||||
CREATE TABLE IF NOT EXISTS stock_prediction (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
stock_code VARCHAR(10) NOT NULL COMMENT '股票代码',
|
||||
stock_name VARCHAR(100) NOT NULL COMMENT '股票名称',
|
||||
predict_date DATE NOT NULL COMMENT '预测日期',
|
||||
predict_price DECIMAL(10,2) COMMENT '预测价格',
|
||||
confidence DECIMAL(5,2) COMMENT '置信度',
|
||||
model_version VARCHAR(50) COMMENT '模型版本',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='股票预测数据表';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_prediction_stock ON stock_prediction(stock_code);
|
||||
CREATE INDEX idx_prediction_date ON stock_prediction(predict_date);
|
||||
|
||||
-- 市场分析表
|
||||
CREATE TABLE IF NOT EXISTS market_analysis (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
analysis_date DATE NOT NULL COMMENT '分析日期',
|
||||
up_count INT COMMENT '上涨股票数',
|
||||
down_count INT COMMENT '下跌股票数',
|
||||
flat_count INT COMMENT '平盘股票数',
|
||||
total_count INT COMMENT '总股票数',
|
||||
total_market_cap DECIMAL(15,2) COMMENT '总市值',
|
||||
total_volume BIGINT COMMENT '总成交量',
|
||||
total_turnover DECIMAL(15,2) COMMENT '总成交额',
|
||||
avg_change_percent DECIMAL(5,2) COMMENT '平均涨跌幅',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场分析表';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_analysis_date ON market_analysis(analysis_date);
|
||||
|
||||
-- 插入示例数据
|
||||
INSERT INTO stock_data (stock_code, stock_name, open_price, close_price, high_price, low_price, volume, turnover, change_percent, change_amount, market_cap, trade_date) VALUES
|
||||
('sz000876', '新希望', 15.20, 15.45, 15.68, 15.10, 5678900, 87654321.50, 1.64, 0.25, 12500000000.00, '2024-01-15 15:00:00'),
|
||||
('sz002714', '牧原股份', 52.30, 53.20, 54.50, 51.80, 3456789, 183456789.20, 1.72, 0.90, 35600000000.00, '2024-01-15 15:00:00'),
|
||||
('sh600519', '贵州茅台', 1685.00, 1698.50, 1705.00, 1680.00, 1234567, 2098765432.10, 0.80, 13.50, 2135000000000.00, '2024-01-15 15:00:00'),
|
||||
('sz000858', '五粮液', 138.50, 140.20, 142.00, 137.80, 2345678, 328765432.60, 1.23, 1.70, 541200000000.00, '2024-01-15 15:00:00'),
|
||||
('sh600887', '伊利股份', 32.10, 32.65, 33.20, 31.95, 4567890, 149876543.20, 1.71, 0.55, 205800000000.00, '2024-01-15 15:00:00');
|
||||
|
||||
COMMIT;
|
||||
EOF
|
||||
|
||||
echo "✅ 数据库初始化脚本创建完成"
|
||||
}
|
||||
|
||||
# 创建Nginx配置
|
||||
create_nginx_config() {
|
||||
echo "创建Nginx配置..."
|
||||
|
||||
cat > nginx/nginx.conf << 'EOF'
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1000;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > nginx/conf.d/default.conf << 'EOF'
|
||||
upstream backend {
|
||||
server backend:8080;
|
||||
}
|
||||
|
||||
upstream frontend {
|
||||
server frontend:80;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# 前端静态资源
|
||||
location / {
|
||||
proxy_pass http://frontend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 后端API
|
||||
location /api/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# WebSocket连接
|
||||
location /ws/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "✅ Nginx配置创建完成"
|
||||
}
|
||||
|
||||
# 构建和启动服务
|
||||
start_services() {
|
||||
echo "构建和启动服务..."
|
||||
|
||||
# 停止现有服务
|
||||
docker-compose down
|
||||
|
||||
# 构建镜像
|
||||
echo "构建Docker镜像..."
|
||||
docker-compose build
|
||||
|
||||
# 启动服务
|
||||
echo "启动服务..."
|
||||
docker-compose up -d
|
||||
|
||||
echo "✅ 服务启动完成"
|
||||
}
|
||||
|
||||
# 等待服务就绪
|
||||
wait_for_services() {
|
||||
echo "等待服务启动..."
|
||||
|
||||
# 等待MySQL就绪
|
||||
echo "等待MySQL就绪..."
|
||||
for i in {1..30}; do
|
||||
if docker-compose exec -T mysql mysqladmin ping -h localhost --silent; then
|
||||
echo "✅ MySQL已就绪"
|
||||
break
|
||||
fi
|
||||
echo "等待MySQL启动... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 等待后端服务就绪
|
||||
echo "等待后端服务就绪..."
|
||||
for i in {1..30}; do
|
||||
if curl -f http://localhost:8080/api/actuator/health > /dev/null 2>&1; then
|
||||
echo "✅ 后端服务已就绪"
|
||||
break
|
||||
fi
|
||||
echo "等待后端服务启动... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "✅ 所有服务已就绪"
|
||||
}
|
||||
|
||||
# 显示访问信息
|
||||
show_access_info() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "🎉 农业上市公司行情监控平台启动成功!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "📊 前端访问地址:"
|
||||
echo " http://localhost:3000"
|
||||
echo ""
|
||||
echo "🔧 后端API地址:"
|
||||
echo " http://localhost:8080/api"
|
||||
echo ""
|
||||
echo "📖 API文档地址:"
|
||||
echo " http://localhost:8080/api/swagger-ui/"
|
||||
echo ""
|
||||
echo "💾 数据库管理:"
|
||||
echo " 主机: localhost:3306"
|
||||
echo " 用户: root"
|
||||
echo " 密码: 123456"
|
||||
echo " 数据库: agricultural_stock"
|
||||
echo ""
|
||||
echo "⚡ Spark Web UI:"
|
||||
echo " http://localhost:8081"
|
||||
echo ""
|
||||
echo "📓 Jupyter Notebook:"
|
||||
echo " http://localhost:8888"
|
||||
echo ""
|
||||
echo "🔍 查看服务状态:"
|
||||
echo " docker-compose ps"
|
||||
echo ""
|
||||
echo "📋 查看日志:"
|
||||
echo " docker-compose logs -f [service_name]"
|
||||
echo ""
|
||||
echo "🛑 停止服务:"
|
||||
echo " docker-compose down"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
check_dependencies
|
||||
create_directories
|
||||
create_init_sql
|
||||
create_nginx_config
|
||||
start_services
|
||||
wait_for_services
|
||||
show_access_info
|
||||
}
|
||||
|
||||
# 如果直接执行脚本
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
84
stop-system.sh
Normal file
84
stop-system.sh
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ===========================================
|
||||
# 农业股票数据分析系统停止脚本
|
||||
# ===========================================
|
||||
|
||||
echo "🛑 正在停止农业股票数据分析系统..."
|
||||
echo "=========================================="
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 停止服务的函数
|
||||
stop_service() {
|
||||
local service_name=$1
|
||||
local pid_file=$2
|
||||
|
||||
if [ -f "$pid_file" ]; then
|
||||
local pid=$(cat "$pid_file")
|
||||
if ps -p $pid > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}🛑 停止 $service_name (PID: $pid)...${NC}"
|
||||
kill $pid
|
||||
sleep 2
|
||||
|
||||
# 检查是否还在运行
|
||||
if ps -p $pid > /dev/null 2>&1; then
|
||||
echo -e "${RED}⚠️ 强制停止 $service_name...${NC}"
|
||||
kill -9 $pid
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ $service_name 已停止${NC}"
|
||||
else
|
||||
echo -e "${BLUE}ℹ️ $service_name 未运行${NC}"
|
||||
fi
|
||||
rm -f "$pid_file"
|
||||
else
|
||||
echo -e "${BLUE}ℹ️ $service_name PID文件不存在${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
# 检查logs目录是否存在
|
||||
if [ ! -d "logs" ]; then
|
||||
echo -e "${BLUE}ℹ️ 没有找到运行中的服务${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 停止各个服务
|
||||
stop_service "前端服务" "logs/frontend.pid"
|
||||
stop_service "后端服务" "logs/backend.pid"
|
||||
stop_service "Spark数据处理器" "logs/spark.pid"
|
||||
|
||||
# 清理端口(如果需要)
|
||||
echo -e "${YELLOW}🧹 清理可能占用的端口...${NC}"
|
||||
|
||||
# 检查并杀死可能占用8080端口的进程
|
||||
local backend_port_pid=$(lsof -ti:8080 2>/dev/null)
|
||||
if [ ! -z "$backend_port_pid" ]; then
|
||||
echo -e "${YELLOW}🛑 停止占用8080端口的进程...${NC}"
|
||||
kill $backend_port_pid 2>/dev/null
|
||||
fi
|
||||
|
||||
# 检查并杀死可能占用3000端口的进程
|
||||
local frontend_port_pid=$(lsof -ti:3000 2>/dev/null)
|
||||
if [ ! -z "$frontend_port_pid" ]; then
|
||||
echo -e "${YELLOW}🛑 停止占用3000端口的进程...${NC}"
|
||||
kill $frontend_port_pid 2>/dev/null
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 农业股票数据分析系统已完全停止!${NC}"
|
||||
echo "=========================================="
|
||||
echo -e "${BLUE}📝 日志文件已保留在logs目录中${NC}"
|
||||
echo -e "${BLUE}🔄 重新启动系统: ./start-system.sh${NC}"
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main
|
||||
Reference in New Issue
Block a user