feat(impact): Impact Agent MVP —— 变更点定位 + 影响调查书(追加改修场景)

- 门控:用户提供 existing_system 路径 → 进入影响调查;未提供 → 原流程不变
- CodeParser 解析 Java(@RestController/@Service/@Entity/@TableName)+ ExistingSystemExplorer 组装
- ImpactAgent 变更点定位(变更区分×既存対応 确定性比对,无 LLM)→ ImpactReport(JSON 可下载)
- 影响调查结果作为 Writer 生成概要设计书的主上下文({{impact}},无专用影响章)
- source_aggregator 解除 existing_system=None 硬编码
- 既有系统样本 sunOnly/stock-trade-system(无 LICENSE,仅测试输入,保留来源标注)
- 新造股票交易域追加改修样本 要件定義_追加改修_股票.xlsx(对齐 sunOnly 真实类名)
- 全量 351 passed / 99.27% 覆盖;门禁 PASS(16 要素:新规5/変更8/削除3/未受影响50)
This commit is contained in:
lhl
2026-08-23 17:59:08 +08:00
parent da33df92e1
commit 2e11720a2d
383 changed files with 18411 additions and 10 deletions
@@ -0,0 +1,17 @@
package com.trade.marketdata;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* 行情服务启动类
* Created by macro on 2020/8/3.
*/
@EnableDiscoveryClient
@SpringBootApplication
public class TradeMarketDataApplication {
public static void main(String[] args) {
SpringApplication.run(TradeMarketDataApplication.class, args);
}
}
@@ -0,0 +1,27 @@
package com.trade.marketdata.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author mac
* @date 2024/7/16
* @description Tushare API 配置类
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "tushare")
public class TushareConfig {
/**
* Tushare API 的基础 URL
*/
private String apiUrl;
/**
* Tushare API 的 Token
*/
private String token;
}
@@ -0,0 +1,32 @@
package com.trade.marketdata.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author mac
* @date 2024/7/16
* @description WebClient 配置类
*/
@Configuration
public class WebClientConfig {
private final TushareConfig tushareConfig;
public WebClientConfig(TushareConfig tushareConfig) {
this.tushareConfig = tushareConfig;
}
/**
* 配置 WebClient Bean,用于调用 Tushare API
* @return WebClient 实例
*/
@Bean
public WebClient tushareWebClient() {
return WebClient.builder()
.baseUrl(tushareConfig.getApiUrl())
.defaultHeader("Content-Type", "application/json")
.build();
}
}
@@ -0,0 +1,4 @@
/**
* 配置类
*/
package com.trade.marketdata.config;
@@ -0,0 +1,83 @@
package com.trade.marketdata.controller;
import com.trade.marketdata.entity.DailyMarketData;
import com.trade.marketdata.service.DailyMarketDataService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
/**
* 股票日线行情数据 Controller
*
* @author Trae
* @since 2024-07-26
*/
@RestController
@RequestMapping("/market-data/daily")
@RequiredArgsConstructor
@Tag(name = "股票日线行情管理", description = "提供股票日线行情数据的同步和查询功能")
public class DailyMarketDataController {
private final DailyMarketDataService dailyMarketDataService;
/**
* 从Tushare同步指定日期的所有股票日线行情数据
*
* @param tradeDate 交易日期,格式 yyyyMMdd
* @return 同步结果
*/
@PostMapping("/sync/{tradeDate}")
@Operation(summary = "同步指定日期的股票日线行情数据", description = "从Tushare同步指定日期的所有股票日线行情数据")
public String syncDailyMarketData(@PathVariable String tradeDate) {
dailyMarketDataService.syncDailyMarketData(tradeDate);
return "Sync daily market data for " + tradeDate + " successfully.";
}
/**
* 从Tushare同步指定日期范围的所有股票日线行情数据
*
* @param startDate 开始日期,格式 yyyyMMdd
* @param endDate 结束日期,格式 yyyyMMdd
* @return 同步结果
*/
@PostMapping("/sync/range")
@Operation(summary = "同步指定日期范围的股票日线行情数据", description = "从Tushare同步指定日期范围内的所有股票日线行情数据")
public String syncDailyMarketDataByDateRange(@RequestParam String startDate, @RequestParam String endDate) {
dailyMarketDataService.syncDailyMarketDataByDateRange(startDate, endDate);
return "Sync daily market data from " + startDate + " to " + endDate + " successfully.";
}
/**
* 查询指定股票在指定日期范围内的日线行情数据
*
* @param tsCode 股票代码
* @param startDate 开始日期
* @param endDate 结束日期
* @return 日线行情数据列表
*/
@GetMapping("/query")
@Operation(summary = "查询日线行情数据", description = "查询指定股票在指定日期范围内的日线行情数据")
public List<DailyMarketData> getDailyMarketData(
@RequestParam String tsCode,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
return dailyMarketDataService.getDailyMarketData(tsCode, startDate, endDate);
}
/**
* 查询指定日期的所有股票日线行情数据
*
* @param tradeDate 交易日期
* @return 日线行情数据列表
*/
@GetMapping("/query/{tradeDate}")
@Operation(summary = "查询指定日期的所有股票日线行情数据", description = "查询指定日期的所有股票日线行情数据")
public List<DailyMarketData> getDailyMarketDataByTradeDate(@PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate tradeDate) {
return dailyMarketDataService.getDailyMarketDataByTradeDate(tradeDate);
}
}
@@ -0,0 +1,57 @@
package com.trade.marketdata.controller;
import com.trade.marketdata.entity.RealtimeMarketData;
import com.trade.marketdata.service.RealtimeMarketDataService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 股票实时行情数据 Controller
*
* @author Trae
* @since 2024-07-26
*/
@RestController
@RequestMapping("/market-data/realtime")
@RequiredArgsConstructor
@Tag(name = "股票实时行情管理", description = "提供股票实时行情数据的查询功能")
public class RealtimeMarketDataController {
private final RealtimeMarketDataService realtimeMarketDataService;
/**
* 获取指定股票代码列表的实时行情数据
*
* @param tsCodes 股票代码列表,逗号分隔,例如 "600000.SH,000001.SZ"
* @return 实时行情数据列表
*/
@GetMapping("/query")
@Operation(summary = "查询实时行情数据", description = "获取指定股票代码列表的实时行情数据")
public List<RealtimeMarketData> getRealtimeMarketData(@RequestParam String tsCodes) {
List<RealtimeMarketData> realtimeData = realtimeMarketDataService.getRealtimeMarketData(tsCodes);
// 可以选择是否在这里保存获取到的数据
// realtimeMarketDataService.saveRealtimeMarketData(realtimeData);
return realtimeData;
}
/**
* 获取并保存指定股票代码列表的实时行情数据
*
* @param tsCodes 股票代码列表,逗号分隔
* @return 操作结果
*/
@PostMapping("/sync-and-save")
@Operation(summary = "同步并保存实时行情数据", description = "获取并保存指定股票代码列表的实时行情数据")
public String syncAndSaveRealtimeMarketData(@RequestParam String tsCodes) {
List<RealtimeMarketData> realtimeData = realtimeMarketDataService.getRealtimeMarketData(tsCodes);
if (realtimeData != null && !realtimeData.isEmpty()) {
realtimeMarketDataService.saveRealtimeMarketData(realtimeData);
return "Successfully fetched and saved realtime market data for: " + tsCodes;
}
return "No realtime market data found for: " + tsCodes;
}
}
@@ -0,0 +1,52 @@
package com.trade.marketdata.controller;
import com.trade.common.api.CommonResult;
import com.trade.marketdata.entity.StockBasic;
import com.trade.marketdata.service.StockBasicService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息控制器
*/
@RestController
@RequestMapping("/stockBasic")
@Tag(name = "StockBasicController", description = "股票基本信息管理")
public class StockBasicController {
private final StockBasicService stockBasicService;
public StockBasicController(StockBasicService stockBasicService) {
this.stockBasicService = stockBasicService;
}
/**
* 同步股票基本信息
* @return 同步结果
*/
@Operation(summary = "同步股票基本信息")
@PostMapping("/sync")
public CommonResult<Integer> syncStockBasic() {
int count = stockBasicService.syncStockBasicFromTushare();
return CommonResult.success(count, "成功同步 " + count + " 条股票基本信息");
}
/**
* 获取所有股票基本信息
* @return 股票基本信息列表
*/
@Operation(summary = "获取所有股票基本信息")
@GetMapping("/listAll")
public CommonResult<List<StockBasic>> listAllStockBasic() {
List<StockBasic> stockBasics = stockBasicService.listAllStockBasic();
return CommonResult.success(stockBasics);
}
}
@@ -0,0 +1,98 @@
package com.trade.marketdata.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 lombok.experimental.Accessors;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* @author mac
* @date 2024/7/16
* @description 股票日线行情数据实体类
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("daily_market_data")
public class DailyMarketData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 股票代码
*/
private String tsCode;
/**
* 交易日期
*/
private LocalDate tradeDate;
/**
* 开盘价
*/
private BigDecimal open;
/**
* 最高价
*/
private BigDecimal high;
/**
* 最低价
*/
private BigDecimal low;
/**
* 收盘价
*/
private BigDecimal close;
/**
* 昨收价
*/
private BigDecimal preClose;
/**
* 涨跌额
*/
private BigDecimal change;
/**
* 涨跌幅
*/
private BigDecimal pctChg;
/**
* 成交量 (手)
*/
private BigDecimal vol;
/**
* 成交额 (千元)
*/
private BigDecimal amount;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,86 @@
package com.trade.marketdata.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.LocalDateTime;
/**
* 股票实时行情数据实体类
*
* @author Trae
* @since 2024-07-26
*/
@Data
@TableName("realtime_market_data")
public class RealtimeMarketData {
/**
* 自增主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 股票代码
*/
private String tsCode;
/**
* 股票名称
*/
private String name;
/**
* 当前价格
*/
private BigDecimal price;
/**
* 涨跌额
*/
private BigDecimal change;
/**
* 涨跌幅
*/
private BigDecimal pctChange;
/**
* 成交量(手)
*/
private Long volume;
/**
* 成交额(万元)
*/
private BigDecimal amount;
/**
* 开盘价
*/
private BigDecimal open;
/**
* 昨日收盘价
*/
private BigDecimal preClose;
/**
* 最高价
*/
private BigDecimal high;
/**
* 最低价
*/
private BigDecimal low;
/**
* 时间戳
*/
private LocalDateTime timestamp;
}
@@ -0,0 +1,118 @@
package com.trade.marketdata.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息实体类
*/
@Data
@TableName("stock_basic")
public class StockBasic implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* TS股票代码
*/
private String tsCode;
/**
* 股票代码
*/
private String symbol;
/**
* 股票名称
*/
private String name;
/**
* 股票简称
*/
private String area;
/**
* 所属省份
*/
private String province;
/**
* 所属城市
*/
private String city;
/**
* 所属行业
*/
private String industry;
/**
* 股票全称
*/
private String fullname;
/**
* 英文全称
*/
private String enname;
/**
* 市场类型 (主板/创业板/科创板等)
*/
private String market;
/**
* 交易所代码
*/
private String exchange;
/**
* 交易货币
*/
private String currType;
/**
* 上市状态 L上市 D退市 P暂停上市
*/
private String listStatus;
/**
* 上市日期
*/
private LocalDate listDate;
/**
* 退市日期
*/
private LocalDate delistDate;
/**
* 是否沪深港通标的,N否 H沪股通 S深股通
*/
private String isHs;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,32 @@
package com.trade.marketdata.entity;
import lombok.Data;
/**
* @author mac
* @date 2024/7/16
* @description Tushare API 请求实体类
*/
@Data
public class TushareRequest {
/**
* API 接口名称
*/
private String apiName;
/**
* Tushare Token
*/
private String token;
/**
* 请求参数
*/
private Object params;
/**
* 返回字段
*/
private String fields;
}
@@ -0,0 +1,45 @@
package com.trade.marketdata.entity;
import lombok.Data;
import java.util.List;
/**
* @author mac
* @date 2024/7/16
* @description Tushare API 响应实体类
*/
@Data
public class TushareResponse {
/**
* 返回码,0 表示成功
*/
private Integer code;
/**
* 错误信息
*/
private String msg;
/**
* 响应数据
*/
private TushareData data;
@Data
public static class TushareData {
/**
* 字段列表
*/
private List<String> fields;
/**
* 数据列表
*/
private List<List<Object>> items;
/**
* 总行数
*/
private Integer has_more;
}
}
@@ -0,0 +1,4 @@
/**
* 实体类层
*/
package com.trade.marketdata.entity;
@@ -0,0 +1,14 @@
package com.trade.marketdata.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.trade.marketdata.entity.DailyMarketData;
import org.apache.ibatis.annotations.Mapper;
/**
* @author mac
* @date 2024/7/16
* @description 股票日线行情数据 Mapper 接口
*/
@Mapper
public interface DailyMarketDataMapper extends BaseMapper<DailyMarketData> {
}
@@ -0,0 +1,15 @@
package com.trade.marketdata.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.trade.marketdata.entity.RealtimeMarketData;
import org.apache.ibatis.annotations.Mapper;
/**
* 股票实时行情数据 Mapper 接口
*
* @author Trae
* @since 2024-07-26
*/
@Mapper
public interface RealtimeMarketDataMapper extends BaseMapper<RealtimeMarketData> {
}
@@ -0,0 +1,14 @@
package com.trade.marketdata.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.trade.marketdata.entity.StockBasic;
import org.apache.ibatis.annotations.Mapper;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息 Mapper 接口
*/
@Mapper
public interface StockBasicMapper extends BaseMapper<StockBasic> {
}
@@ -0,0 +1,4 @@
/**
* 数据访问层 (MyBatis Mapper)
*/
package com.trade.marketdata.mapper;
@@ -0,0 +1,4 @@
/**
* 行情服务模块主包。
*/
package com.trade.marketdata;
@@ -0,0 +1,33 @@
package com.trade.marketdata.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.trade.marketdata.entity.DailyMarketData;
import java.time.LocalDate;
import java.util.List;
/**
* @author mac
* @date 2024/7/16
* @description 股票日线行情数据服务接口
*/
public interface DailyMarketDataService extends IService<DailyMarketData> {
/**
* 从 Tushare 同步指定股票的日线行情数据
* @param tsCode 股票代码
* @param startDate 开始日期 (yyyyMMdd)
* @param endDate 结束日期 (yyyyMMdd)
* @return 同步的日线数据数量
*/
int syncDailyMarketDataFromTushare(String tsCode, String startDate, String endDate);
/**
* 查询指定股票在指定日期范围内的日线行情数据
* @param tsCode 股票代码
* @param startDate 开始日期
* @param endDate 结束日期
* @return 日线行情数据列表
*/
List<DailyMarketData> listDailyMarketData(String tsCode, LocalDate startDate, LocalDate endDate);
}
@@ -0,0 +1,29 @@
package com.trade.marketdata.service;
import com.trade.marketdata.entity.RealtimeMarketData;
import java.util.List;
/**
* 股票实时行情数据 Service 接口
*
* @author Trae
* @since 2024-07-26
*/
public interface RealtimeMarketDataService {
/**
* 获取指定股票代码列表的实时行情数据
*
* @param tsCodes 股票代码列表,逗号分隔
* @return 实时行情数据列表
*/
List<RealtimeMarketData> getRealtimeMarketData(String tsCodes);
/**
* 保存实时行情数据列表
*
* @param realtimeMarketDataList 实时行情数据列表
*/
void saveRealtimeMarketData(List<RealtimeMarketData> realtimeMarketDataList);
}
@@ -0,0 +1,26 @@
package com.trade.marketdata.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.trade.marketdata.entity.StockBasic;
import java.util.List;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息服务接口
*/
public interface StockBasicService extends IService<StockBasic> {
/**
* 从 Tushare 同步股票基本信息
* @return 同步的股票数量
*/
int syncStockBasicFromTushare();
/**
* 查询所有股票基本信息
* @return 股票基本信息列表
*/
List<StockBasic> listAllStockBasic();
}
@@ -0,0 +1,45 @@
package com.trade.marketdata.service;
import com.trade.marketdata.config.TushareConfig;
import com.trade.marketdata.entity.TushareRequest;
import com.trade.marketdata.entity.TushareResponse;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
/**
* @author mac
* @date 2024/7/16
* @description Tushare API 调用服务
*/
@Service
public class TushareApi {
private final WebClient tushareWebClient;
private final TushareConfig tushareConfig;
public TushareApi(WebClient tushareWebClient, TushareConfig tushareConfig) {
this.tushareWebClient = tushareWebClient;
this.tushareConfig = tushareConfig;
}
/**
* 调用 Tushare API 获取数据
* @param apiName API 名称,例如 "daily"
* @param params 请求参数,JSON 格式
* @param fields 返回字段,逗号分隔
* @return TushareResponse 响应对象
*/
public Mono<TushareResponse> post(String apiName, Object params, String fields) {
TushareRequest request = new TushareRequest();
request.setApiName(apiName);
request.setToken(tushareConfig.getToken());
request.setParams(params);
request.setFields(fields);
return tushareWebClient.post()
.bodyValue(request)
.retrieve()
.bodyToMono(TushareResponse.class);
}
}
@@ -0,0 +1,163 @@
package com.trade.marketdata.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.trade.marketdata.entity.DailyMarketData;
import com.trade.marketdata.entity.TushareResponse;
import com.trade.marketdata.mapper.DailyMarketDataMapper;
import com.trade.marketdata.service.DailyMarketDataService;
import com.trade.marketdata.service.TushareApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author mac
* @date 2024/7/16
* @description 股票日线行情数据服务实现类
*/
@Service
@Slf4j
public class DailyMarketDataServiceImpl extends ServiceImpl<DailyMarketDataMapper, DailyMarketData> implements DailyMarketDataService {
private final TushareApi tushareApi;
public DailyMarketDataServiceImpl(TushareApi tushareApi) {
this.tushareApi = tushareApi;
}
/**
* 从 Tushare 同步指定股票的日线行情数据
* @param tsCode 股票代码
* @param startDate 开始日期 (yyyyMMdd)
* @param endDate 结束日期 (yyyyMMdd)
* @return 同步的日线数据数量
*/
@Override
@Transactional
public int syncDailyMarketDataFromTushare(String tsCode, String startDate, String endDate) {
log.info("开始从 Tushare 同步股票 {} 的日线行情数据,日期范围:{} 至 {}...", tsCode, startDate, endDate);
Map<String, String> params = new HashMap<>();
params.put("ts_code", tsCode);
params.put("start_date", startDate);
params.put("end_date", endDate);
String fields = "ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount";
TushareResponse response = tushareApi.post("daily", params, fields).block();
if (response == null || response.getCode() != 0 || response.getData() == null) {
log.error("从 Tushare 获取股票 {} 日线行情数据失败: {}", tsCode, response != null ? response.getMsg() : "未知错误");
return 0;
}
List<List<Object>> items = response.getData().getItems();
List<String> fieldsList = response.getData().getFields();
if (items == null || items.isEmpty()) {
log.warn("从 Tushare 获取到股票 {} 的空日线行情数据列表。", tsCode);
return 0;
}
List<DailyMarketData> newDailyDataList = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
for (List<Object> item : items) {
DailyMarketData dailyData = new DailyMarketData();
for (int i = 0; i < fieldsList.size(); i++) {
String fieldName = fieldsList.get(i);
Object value = item.get(i);
if (value == null) continue;
switch (fieldName) {
case "ts_code": dailyData.setTsCode(value.toString()); break;
case "trade_date": dailyData.setTradeDate(LocalDate.parse(value.toString(), formatter)); break;
case "open": dailyData.setOpen(new BigDecimal(value.toString())); break;
case "high": dailyData.setHigh(new BigDecimal(value.toString())); break;
case "low": dailyData.setLow(new BigDecimal(value.toString())); break;
case "close": dailyData.setClose(new BigDecimal(value.toString())); break;
case "pre_close": dailyData.setPreClose(new BigDecimal(value.toString())); break;
case "change": dailyData.setChange(new BigDecimal(value.toString())); break;
case "pct_chg": dailyData.setPctChg(new BigDecimal(value.toString())); break;
case "vol": dailyData.setVol(new BigDecimal(value.toString())); break;
case "amount": dailyData.setAmount(new BigDecimal(value.toString())); break;
}
}
dailyData.setCreateTime(LocalDateTime.now());
dailyData.setUpdateTime(LocalDateTime.now());
newDailyDataList.add(dailyData);
}
// 批量插入或更新
// 获取当前数据库中指定股票在指定日期范围内的所有日线数据的 ts_code 和 trade_date 组合
List<DailyMarketData> existingData = baseMapper.selectList(new QueryWrapper<DailyMarketData>()
.eq("ts_code", tsCode)
.between("trade_date", LocalDate.parse(startDate, formatter), LocalDate.parse(endDate, formatter)));
Map<String, DailyMarketData> existingDataMap = existingData.stream()
.collect(Collectors.toMap(data -> data.getTsCode() + "_" + data.getTradeDate().format(formatter), data -> data));
List<DailyMarketData> toInsert = new ArrayList<>();
List<DailyMarketData> toUpdate = new ArrayList<>();
for (DailyMarketData newData : newDailyDataList) {
String key = newData.getTsCode() + "_" + newData.getTradeDate().format(formatter);
if (existingDataMap.containsKey(key)) {
DailyMarketData existing = existingDataMap.get(key);
newData.setId(existing.getId()); // 设置ID以便更新
toUpdate.add(newData);
} else {
toInsert.add(newData);
}
}
int insertedCount = 0;
if (!toInsert.isEmpty()) {
saveBatch(toInsert);
insertedCount = toInsert.size();
log.info("成功插入 {} 条新的股票 {} 日线行情数据。", insertedCount, tsCode);
}
int updatedCount = 0;
if (!toUpdate.isEmpty()) {
updateBatchById(toUpdate);
updatedCount = toUpdate.size();
log.info("成功更新 {} 条股票 {} 日线行情数据。", updatedCount, tsCode);
}
log.info("股票 {} 日线行情数据同步完成,总计插入 {} 条,更新 {} 条。", tsCode, insertedCount, updatedCount);
return insertedCount + updatedCount;
}
/**
* 查询指定股票在指定日期范围内的日线行情数据
* @param tsCode 股票代码
* @param startDate 开始日期
* @param endDate 结束日期
* @return 日线行情数据列表
*/
@Override
public List<DailyMarketData> listDailyMarketData(String tsCode, LocalDate startDate, LocalDate endDate) {
QueryWrapper<DailyMarketData> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("ts_code", tsCode);
if (startDate != null) {
queryWrapper.ge("trade_date", startDate);
}
if (endDate != null) {
queryWrapper.le("trade_date", endDate);
}
queryWrapper.orderByAsc("trade_date");
return list(queryWrapper);
}
}
@@ -0,0 +1,140 @@
package com.trade.marketdata.service.impl;
import com.trade.marketdata.entity.RealtimeMarketData;
import com.trade.marketdata.mapper.RealtimeMarketDataMapper;
import com.trade.marketdata.service.RealtimeMarketDataService;
import com.trade.marketdata.util.TushareApi;
import com.trade.marketdata.util.TushareRequest;
import com.trade.marketdata.util.TushareResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 股票实时行情数据 Service 实现类
*
* @author Trae
* @since 2024-07-26
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class RealtimeMarketDataServiceImpl implements RealtimeMarketDataService {
private final RealtimeMarketDataMapper realtimeMarketDataMapper;
private final TushareApi tushareApi;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
/**
* 获取指定股票代码列表的实时行情数据
*
* @param tsCodes 股票代码列表,逗号分隔
* @return 实时行情数据列表
*/
@Override
public List<RealtimeMarketData> getRealtimeMarketData(String tsCodes) {
TushareRequest<Map<String, String>> request = new TushareRequest<>();
request.setApiName("realtime_quotes"); // Tushare 实时行情接口名称,请根据实际情况调整
Map<String, String> params = new HashMap<>();
params.put("ts_code", tsCodes);
request.setParams(params);
TushareResponse<List<List<Object>>> response = tushareApi.call(request, List.class, List.class, Object.class);
List<RealtimeMarketData> resultList = new ArrayList<>();
if (response != null && response.getData() != null && response.getData().getItems() != null) {
List<String> fields = response.getData().getFields();
List<List<Object>> items = response.getData().getItems();
for (List<Object> item : items) {
RealtimeMarketData data = new RealtimeMarketData();
for (int i = 0; i < fields.size(); i++) {
String fieldName = fields.get(i);
Object value = item.get(i);
if (value == null) continue;
switch (fieldName) {
case "ts_code":
data.setTsCode(String.valueOf(value));
break;
case "name":
data.setName(String.valueOf(value));
break;
case "price":
data.setPrice(new BigDecimal(String.valueOf(value)));
break;
case "change":
data.setChange(new BigDecimal(String.valueOf(value)));
break;
case "pct_chg": // Tushare返回的字段名可能为 pct_chg
case "pct_change":
data.setPctChange(new BigDecimal(String.valueOf(value)));
break;
case "volume":
data.setVolume(Long.parseLong(String.valueOf(value)));
break;
case "amount":
data.setAmount(new BigDecimal(String.valueOf(value)));
break;
case "open":
data.setOpen(new BigDecimal(String.valueOf(value)));
break;
case "pre_close":
data.setPreClose(new BigDecimal(String.valueOf(value)));
break;
case "high":
data.setHigh(new BigDecimal(String.valueOf(value)));
break;
case "low":
data.setLow(new BigDecimal(String.valueOf(value)));
break;
case "time": // Tushare返回的时间字段名可能为 time
// 假设Tushare返回的时间格式是 yyyyMMddHHmmss
// 如果是其他格式,需要调整 DateTimeFormatter
// 如果Tushare直接返回的是 HH:mm:ss 格式,需要结合当前日期进行转换
// 这里假设返回的是包含日期的完整时间字符串
try {
data.setTimestamp(LocalDateTime.parse(String.valueOf(value), FORMATTER));
} catch (Exception e) {
log.warn("Failed to parse timestamp: {} for ts_code: {}. Error: {}", value, data.getTsCode(), e.getMessage());
// 可以设置一个默认值或者根据业务需求处理
data.setTimestamp(LocalDateTime.now());
}
break;
default:
break;
}
}
resultList.add(data);
}
}
return resultList;
}
/**
* 保存实时行情数据列表
*
* @param realtimeMarketDataList 实时行情数据列表
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void saveRealtimeMarketData(List<RealtimeMarketData> realtimeMarketDataList) {
if (realtimeMarketDataList == null || realtimeMarketDataList.isEmpty()) {
return;
}
// 实际应用中,可能需要根据 ts_code 和 timestamp 判断数据是否已存在,进行更新或插入操作
// 这里简化为直接批量插入
realtimeMarketDataList.forEach(realtimeMarketDataMapper::insert);
log.info("Successfully saved {} realtime market data records.", realtimeMarketDataList.size());
}
}
@@ -0,0 +1,150 @@
package com.trade.marketdata.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.trade.marketdata.entity.StockBasic;
import com.trade.marketdata.entity.TushareResponse;
import com.trade.marketdata.mapper.StockBasicMapper;
import com.trade.marketdata.service.StockBasicService;
import com.trade.marketdata.service.TushareApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author mac
* @date 2024/7/16
* @description 股票基本信息服务实现类
*/
@Service
@Slf4j
public class StockBasicServiceImpl extends ServiceImpl<StockBasicMapper, StockBasic> implements StockBasicService {
private final TushareApi tushareApi;
public StockBasicServiceImpl(TushareApi tushareApi) {
this.tushareApi = tushareApi;
}
/**
* 从 Tushare 同步股票基本信息
* @return 同步的股票数量
*/
@Override
@Transactional
public int syncStockBasicFromTushare() {
log.info("开始从 Tushare 同步股票基本信息...");
Map<String, String> params = new HashMap<>();
params.put("exchange", ""); // 交易所代码,可选,空表示获取所有
params.put("list_status", "L"); // 上市状态 L上市 D退市 P暂停上市
String fields = "ts_code,symbol,name,area,province,city,industry,fullname,enname,market,exchange,curr_type,list_status,list_date,delist_date,is_hs";
TushareResponse response = tushareApi.post("stock_basic", params, fields).block();
if (response == null || response.getCode() != 0 || response.getData() == null) {
log.error("从 Tushare 获取股票基本信息失败: {}", response != null ? response.getMsg() : "未知错误");
return 0;
}
List<List<Object>> items = response.getData().getItems();
List<String> fieldsList = response.getData().getFields();
if (items == null || items.isEmpty()) {
log.warn("从 Tushare 获取到空股票基本信息列表。");
return 0;
}
List<StockBasic> newStockBasics = new ArrayList<>();
for (List<Object> item : items) {
StockBasic stockBasic = new StockBasic();
for (int i = 0; i < fieldsList.size(); i++) {
String fieldName = fieldsList.get(i);
Object value = item.get(i);
if (value == null) continue;
switch (fieldName) {
case "ts_code": stockBasic.setTsCode(value.toString()); break;
case "symbol": stockBasic.setSymbol(value.toString()); break;
case "name": stockBasic.setName(value.toString()); break;
case "area": stockBasic.setArea(value.toString()); break;
case "province": stockBasic.setProvince(value.toString()); break;
case "city": stockBasic.setCity(value.toString()); break;
case "industry": stockBasic.setIndustry(value.toString()); break;
case "fullname": stockBasic.setFullname(value.toString()); break;
case "enname": stockBasic.setEnname(value.toString()); break;
case "market": stockBasic.setMarket(value.toString()); break;
case "exchange": stockBasic.setExchange(value.toString()); break;
case "curr_type": stockBasic.setCurrType(value.toString()); break;
case "list_status": stockBasic.setListStatus(value.toString()); break;
case "list_date": stockBasic.setListDate(LocalDate.parse(value.toString())); break;
case "delist_date": stockBasic.setDelistDate(value.toString().isEmpty() ? null : LocalDate.parse(value.toString())); break;
case "is_hs": stockBasic.setIsHs(value.toString()); break;
}
}
stockBasic.setCreateTime(LocalDateTime.now());
stockBasic.setUpdateTime(LocalDateTime.now());
newStockBasics.add(stockBasic);
}
// 批量插入或更新
// 考虑到数据量可能较大,且需要判断是否已存在,这里可以先查询现有数据,然后进行区分插入和更新
// 简化处理:先删除所有现有数据,再批量插入新数据 (适用于数据量不大,且更新频率不高的场景)
// 更优方案:根据 ts_code 判断是否存在,存在则更新,不存在则插入
// 获取当前数据库中所有股票的 ts_code 集合
List<String> existingTsCodes = baseMapper.selectList(null).stream()
.map(StockBasic::getTsCode)
.collect(Collectors.toList());
List<StockBasic> toInsert = new ArrayList<>();
List<StockBasic> toUpdate = new ArrayList<>();
for (StockBasic stock : newStockBasics) {
if (existingTsCodes.contains(stock.getTsCode())) {
// 查找现有记录的ID,用于更新
StockBasic existingStock = baseMapper.selectOne(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.<StockBasic>lambdaQuery().eq(StockBasic::getTsCode, stock.getTsCode()));
if (existingStock != null) {
stock.setId(existingStock.getId());
toUpdate.add(stock);
}
} else {
toInsert.add(stock);
}
}
int insertedCount = 0;
if (!toInsert.isEmpty()) {
saveBatch(toInsert);
insertedCount = toInsert.size();
log.info("成功插入 {} 条新的股票基本信息。".formatted(insertedCount));
}
int updatedCount = 0;
if (!toUpdate.isEmpty()) {
updateBatchById(toUpdate);
updatedCount = toUpdate.size();
log.info("成功更新 {} 条股票基本信息。".formatted(updatedCount));
}
log.info("股票基本信息同步完成,总计插入 {} 条,更新 {} 条。".formatted(insertedCount, updatedCount));
return insertedCount + updatedCount;
}
/**
* 查询所有股票基本信息
* @return 股票基本信息列表
*/
@Override
public List<StockBasic> listAllStockBasic() {
return list();
}
}
@@ -0,0 +1,4 @@
/**
* 服务实现层
*/
package com.trade.marketdata.service.impl;
@@ -0,0 +1,4 @@
/**
* 服务接口层
*/
package com.trade.marketdata.service;
@@ -0,0 +1,75 @@
package com.stock.marketdata.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.stock.common.pojo.CommonResult;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import com.stock.marketdata.service.MarketDataService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 行情数据 Controller
*
* @author Pure AI
*/
@Tag(name = "行情数据接口")
@RestController
@RequestMapping("/market-data")
public class MarketDataController {
@Resource
private MarketDataService marketDataService;
@PostMapping
@Operation(summary = "创建行情数据")
public CommonResult<Long> createMarketData(@RequestBody MarketDataDO marketData) {
return CommonResult.success(marketDataService.createMarketData(marketData));
}
@PutMapping
@Operation(summary = "更新行情数据")
public CommonResult<Boolean> updateMarketData(@RequestBody MarketDataDO marketData) {
marketDataService.updateMarketData(marketData);
return CommonResult.success(true);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除行情数据")
@Parameter(name = "id", description = "行情数据编号", required = true, example = "1024")
public CommonResult<Boolean> deleteMarketData(@PathVariable("id") Long id) {
marketDataService.deleteMarketData(id);
return CommonResult.success(true);
}
@GetMapping("/{id}")
@Operation(summary = "获取行情数据")
@Parameter(name = "id", description = "行情数据编号", required = true, example = "1024")
public CommonResult<MarketDataDO> getMarketData(@PathVariable("id") Long id) {
return CommonResult.success(marketDataService.getMarketData(id));
}
@GetMapping("/list")
@Operation(summary = "获取行情数据列表")
@Parameter(name = "stockCode", description = "股票代码", example = "000001")
public CommonResult<List<MarketDataDO>> getMarketDataList(@RequestParam(required = false) String stockCode) {
return CommonResult.success(marketDataService.getMarketDataList(stockCode));
}
@GetMapping("/page")
@Operation(summary = "分页查询行情数据")
public CommonResult<Page<MarketDataDO>> pageMarketData(
@Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") Integer pageNum,
@Parameter(description = "每页条数", example = "10") @RequestParam(defaultValue = "10") Integer pageSize,
@Parameter(description = "股票代码", example = "000001") @RequestParam(required = false) String stockCode) {
Page<MarketDataDO> page = new Page<>(pageNum, pageSize);
return CommonResult.success(marketDataService.pageMarketData(page, stockCode));
}
// TODO: 添加其他接口
}
@@ -0,0 +1,53 @@
package com.stock.marketdata.dal.dataobject;
import com.stock.common.dal.dataobject.BaseDO; // 引入公共BaseDO
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* 行情数据DO
*
* @author TraeAI
*/
@TableName("market_data") // TODO: 确认表名是否正确
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MarketDataDO extends BaseDO {
/**
* ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 股票代码
*/
private String stockCode;
/**
* 股票名称
*/
private String stockName;
/**
* 最新价格
*/
private Double latestPrice;
/**
* 更新时间
*/
private LocalDateTime updateTime;
// TODO: 根据实际需求添加更多字段
}
@@ -0,0 +1,17 @@
package com.stock.marketdata.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 行情数据 Mapper
*
* @author Pure AI
*/
@Mapper
public interface MarketDataMapper extends BaseMapper<MarketDataDO> {
// TODO: 定义自定义的 SQL 查询方法
}
@@ -0,0 +1,15 @@
package com.stock.marketdata.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 行情数据 Mapper
*
* @author TraeAI
*/
@Mapper
public interface MarketDataMapper extends BaseMapper<MarketDataDO> {
// TODO: 定义行情数据相关的数据库操作方法
}
@@ -0,0 +1,64 @@
package com.stock.marketdata.service;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
/**
* 行情数据服务接口
*
* @author Pure AI
*/
public interface MarketDataService {
/**
* 创建行情数据
*
* @param marketData 行情数据对象
* @return 创建的行情数据ID
*/
Long createMarketData(MarketDataDO marketData);
/**
* 更新行情数据
*
* @param marketData 行情数据对象
*/
void updateMarketData(MarketDataDO marketData);
/**
* 删除行情数据
*
* @param id 行情数据ID
*/
void deleteMarketData(Long id);
/**
* 获取行情数据
*
* @param id 行情数据ID
* @return 行情数据对象
*/
MarketDataDO getMarketData(Long id);
/**
* 获取行情数据列表
*
* @param stockCode 股票代码
* @return 行情数据列表
*/
List<MarketDataDO> getMarketDataList(String stockCode);
/**
* 分页查询行情数据
*
* @param page 分页参数
* @param stockCode 股票代码
* @return 分页结果
*/
Page<MarketDataDO> pageMarketData(Page<MarketDataDO> page, String stockCode);
// TODO: 添加其他业务方法
}
@@ -0,0 +1,105 @@
package com.stock.marketdata.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import com.stock.marketdata.dal.mapper.MarketDataMapper;
import com.stock.marketdata.service.MarketDataService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.List;
/**
* 行情数据服务实现类
*
* @author Pure AI
*/
@Service
public class MarketDataServiceImpl implements MarketDataService {
@Resource
private MarketDataMapper marketDataMapper;
/**
* 创建行情数据
*
* @param marketData 行情数据对象
* @return 创建的行情数据ID
*/
@Override
public Long createMarketData(MarketDataDO marketData) {
marketDataMapper.insert(marketData);
return marketData.getId();
}
/**
* 更新行情数据
*
* @param marketData 行情数据对象
*/
@Override
public void updateMarketData(MarketDataDO marketData) {
marketDataMapper.updateById(marketData);
}
/**
* 删除行情数据
*
* @param id 行情数据ID
*/
@Override
public void deleteMarketData(Long id) {
marketDataMapper.deleteById(id);
}
/**
* 获取行情数据
*
* @param id 行情数据ID
* @return 行情数据对象
*/
@Override
public MarketDataDO getMarketData(Long id) {
return marketDataMapper.selectById(id);
}
/**
* 获取行情数据列表
*
* @param stockCode 股票代码
* @return 行情数据列表
*/
@Override
public List<MarketDataDO> getMarketDataList(String stockCode) {
LambdaQueryWrapper<MarketDataDO> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(stockCode)) {
queryWrapper.eq(MarketDataDO::getStockCode, stockCode);
}
// TODO: 根据业务需求添加其他查询条件,例如时间范围等
queryWrapper.orderByDesc(MarketDataDO::getTradingDay); // 默认按交易日降序
return marketDataMapper.selectList(queryWrapper);
}
/**
* 分页查询行情数据
*
* @param page 分页参数
* @param stockCode 股票代码
* @return 分页结果
*/
@Override
public Page<MarketDataDO> pageMarketData(Page<MarketDataDO> page, String stockCode) {
LambdaQueryWrapper<MarketDataDO> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(stockCode)) {
queryWrapper.eq(MarketDataDO::getStockCode, stockCode);
}
// TODO: 根据业务需求添加其他查询条件,例如时间范围等
queryWrapper.orderByDesc(MarketDataDO::getTradingDay); // 默认按交易日降序
return marketDataMapper.selectPage(page, queryWrapper);
}
// TODO: 实现其他业务方法
}
@@ -0,0 +1,11 @@
server:
port: 8004
spring:
profiles:
active: dev
application:
name: trade-market-data
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8004
spring:
profiles:
active: prod
application:
name: trade-market-data
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,11 @@
server:
port: 8004
spring:
profiles:
active: test
application:
name: trade-market-data
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
@@ -0,0 +1,72 @@
server:
port: 8081 # 服务端口,可以根据实际情况修改
spring:
application:
name: trade-market-data # 应用名称
# datasource: # 数据库配置,可以从 yudao-cloud-mini 的公共配置中获取或在此处覆盖
# url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
# username: your_username
# password: your_password
# driver-class-name: com.mysql.cj.jdbc.Driver
# redis: # Redis 配置,可以从 yudao-cloud-mini 的公共配置中获取或在此处覆盖
# host: localhost
# port: 6379
# password:
# database: 0
# Tushare API 配置
tushare:
api-url: http://api.tushare.pro
token: "YOUR_TUSHARE_TOKEN" # 请替换为您的 Tushare Token
# Mybatis Plus 配置
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml # Mapper XML 文件路径
#type-aliases-package: com.trade.marketdata.entity # 实体类别名包路径,如果需要的话
global-config:
db-config:
id-type: auto # ID 生成策略
# table-prefix: t_ # 表前缀,如果需要的话
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名转换
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志,生产环境建议关闭或使用更完善的日志方案
# SpringDoc OpenAPI 配置 (Swagger)
springdoc:
api-docs:
path: /v3/api-docs # API 文档路径
swagger-ui:
path: /swagger-ui.html # Swagger UI 路径
enabled: true # 开启 Swagger UI
group-configs:
- group: default
paths-to-match: /**
packages-to-scan: com.trade.marketdata.controller # Controller 包路径
# Nacos 配置,用于服务注册与发现
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
# namespace: # Nacos 命名空间,如果需要的话
# group: # Nacos 分组,如果需要的话
# config:
# server-addr: ${spring.cloud.nacos.discovery.server-addr}
# file-extension: yaml # 配置文件格式
# namespace: ${spring.cloud.nacos.discovery.namespace}
# group: ${spring.cloud.nacos.discovery.group}
# shared-configs[0]: # 共享配置
# data-id: application-common.yaml
# group: DEFAULT_GROUP
# refresh: true
# 日志配置 (可选,Spring Boot 默认使用 Logback)
logging:
level:
com.trade.marketdata: DEBUG # 设置项目包的日志级别
# org.springframework: INFO
# org.apache.ibatis: DEBUG # 如果需要查看 MyBatis 执行的 SQL
# file:
# name: ./logs/trade-market-data.log # 日志文件路径
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="base-logback.xml"/>
<logger name="com.trade.marketdata" level="debug"/>
</configuration>
@@ -0,0 +1,134 @@
package com.stock.marketdata.controller;
import com.stock.marketdata.service.MarketDataService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.http.MediaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.stock.marketdata.dal.dataobject.MarketDataDO;
import com.stock.common.pojo.CommonResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(MarketDataController.class)
class MarketDataControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MarketDataService marketDataService;
@Autowired
private ObjectMapper objectMapper;
private MarketDataDO createMarketDataDO() {
MarketDataDO marketData = new MarketDataDO();
marketData.setId(1L);
marketData.setStockCode("000001");
marketData.setStockName("平安银行");
marketData.setOpenPrice(BigDecimal.valueOf(10.00));
marketData.setClosePrice(BigDecimal.valueOf(10.50));
marketData.setHighPrice(BigDecimal.valueOf(10.60));
marketData.setLowPrice(BigDecimal.valueOf(9.90));
marketData.setVolume(10000L);
marketData.setTurnover(BigDecimal.valueOf(105000.00));
marketData.setTradeTime(LocalDateTime.now());
return marketData;
}
@Test
void createMarketData() throws Exception {
MarketDataDO marketData = createMarketDataDO();
when(marketDataService.createMarketData(any(MarketDataDO.class))).thenReturn(1L);
mockMvc.perform(post("/market-data")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(marketData)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data").value(1L));
}
@Test
void updateMarketData() throws Exception {
MarketDataDO marketData = createMarketDataDO();
doNothing().when(marketDataService).updateMarketData(any(MarketDataDO.class));
mockMvc.perform(put("/market-data")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(marketData)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data").value(true));
}
@Test
void deleteMarketData() throws Exception {
doNothing().when(marketDataService).deleteMarketData(anyLong());
mockMvc.perform(delete("/market-data/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data").value(true));
}
@Test
void getMarketData() throws Exception {
MarketDataDO marketData = createMarketDataDO();
when(marketDataService.getMarketData(anyLong())).thenReturn(marketData);
mockMvc.perform(get("/market-data/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.id").value(1L))
.andExpect(jsonPath("$.data.stockCode").value("000001"));
}
@Test
void getMarketDataList() throws Exception {
MarketDataDO marketData = createMarketDataDO();
List<MarketDataDO> list = Collections.singletonList(marketData);
when(marketDataService.getMarketDataList(anyString())).thenReturn(list);
mockMvc.perform(get("/market-data/list").param("stockCode", "000001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data[0].id").value(1L))
.andExpect(jsonPath("$.data[0].stockCode").value("000001"));
}
@Test
void pageMarketData() throws Exception {
MarketDataDO marketData = createMarketDataDO();
Page<MarketDataDO> page = new Page<>(1, 10);
page.setRecords(Collections.singletonList(marketData));
page.setTotal(1L);
when(marketDataService.pageMarketData(any(Page.class), anyString())).thenReturn(page);
mockMvc.perform(get("/market-data/page")
.param("pageNum", "1")
.param("pageSize", "10")
.param("stockCode", "000001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.records[0].id").value(1L))
.andExpect(jsonPath("$.data.total").value(1L));
}
}