first commit

This commit is contained in:
2025-06-12 19:37:54 +08:00
parent bb2eb010f7
commit 1c6093fa9a
87 changed files with 18432 additions and 0 deletions

11
.gitignore vendored Normal file
View 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
View 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
View 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>

View File

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

View File

@@ -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("生产环境")
));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View 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
View 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数据库

View 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

View 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 只股票

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

File diff suppressed because it is too large Load Diff

21
docs/package.json Normal file
View 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"
}
}

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

View 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"

View 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"

View 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.

View 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

View 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

View 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

View 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

View 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)

View 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
通过以上协作开发流程,四个子项目能够有机结合,形成一个高效、稳定、可扩展的农业股票数据分析系统。每个团队在专注自身模块开发的同时,通过标准化的接口和协作机制,确保系统整体的一致性和可靠性。

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,25 @@
# 图片目录
将您的图片文件放在这个目录中。
支持的格式:
- PNG
- JPG/JPEG
- SVG
- WebP
- GIF
## 使用方式
在 Markdown 中引用图片:
```markdown
![图片描述](/images/your-image.png)
```
在配置文件中使用:
```yaml
site:
logo: "/images/logo.png"
```

29
docs/src/main.ts Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

41
frontend/package.json Normal file
View 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"
}
}

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

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

View 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
View 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')

View 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

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

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

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

View 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

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

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

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

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

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

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

View 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
View 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'
],
},
})

View 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.

View 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
```
## 📝 日志说明
程序运行时会输出详细日志,包括:
- 数据库连接状态
- 各阶段处理进度
- 数据保存结果
- 错误信息和回退操作
关键日志信息:
- `数据库连接测试成功` - 数据库连接正常
- `市场分析结果保存成功` - 数据已保存到数据库
- `保存到数据库失败,回退到文件保存` - 使用备用保存方式

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

View File

@@ -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("农业股票数据处理器已停止");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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