Zhangbw пре 3 месеци
родитељ
комит
0d42056e6e

+ 30 - 0
src/main/java/com/yingpai/gupiao/config/CorsConfig.java

@@ -0,0 +1,30 @@
+package com.yingpai.gupiao.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+/**
+ * CORS配置,允许跨域请求
+ */
+@Configuration
+public class CorsConfig {
+
+    @Bean
+    public CorsFilter corsFilter() {
+        CorsConfiguration config = new CorsConfiguration();
+        
+        config.addAllowedOriginPattern("*");
+        config.addAllowedHeader("*");
+        config.addAllowedMethod("*");
+        config.setAllowCredentials(true);
+        config.setMaxAge(3600L);
+
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", config);
+        
+        return new CorsFilter(source);
+    }
+}

+ 50 - 0
src/main/java/com/yingpai/gupiao/controller/StockSearchController.java

@@ -0,0 +1,50 @@
+package com.yingpai.gupiao.controller;
+
+import com.yingpai.gupiao.domain.dto.StockSearchRequest;
+import com.yingpai.gupiao.domain.dto.StockSuggestionDto;
+import com.yingpai.gupiao.domain.vo.ApiResponse;
+import com.yingpai.gupiao.domain.vo.PageResult;
+import com.yingpai.gupiao.domain.vo.StockDetailResponse;
+import com.yingpai.gupiao.domain.vo.StockSearchResponseItem;
+import com.yingpai.gupiao.service.StockSearchService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/v1/stock")
+public class StockSearchController {
+
+    private final StockSearchService stockSearchService;
+
+    public StockSearchController(StockSearchService stockSearchService) {
+        this.stockSearchService = stockSearchService;
+    }
+
+    /**
+     * 股票精确查询(详情)
+     *
+     * POST /v1/stock/search
+     */
+    @PostMapping("/search")
+    public ApiResponse<StockDetailResponse> search(@RequestBody StockSearchRequest request) {
+        StockDetailResponse result = stockSearchService.search(request);
+        if (result == null) {
+            return new ApiResponse<>(1001, "未查询到该股票评分数据", null);
+        }
+        return new ApiResponse<>(0, "success", result);
+    }
+
+    /**
+     * 股票搜索联想
+     *
+     * GET /v1/stock/suggestion
+     */
+    @GetMapping("/suggestion")
+    public ApiResponse<List<StockSuggestionDto>> suggestion(@RequestParam String keyword) {
+        List<StockSuggestionDto> list = stockSearchService.getSuggestion(keyword);
+        return new ApiResponse<>(0, "success", list);
+    }
+}
+
+

+ 48 - 0
src/main/java/com/yingpai/gupiao/domain/dto/StockSearchRequest.java

@@ -0,0 +1,48 @@
+package com.yingpai.gupiao.domain.dto;
+
+/**
+ * 前端搜索股票请求体
+ */
+public class StockSearchRequest {
+
+    /**
+     * 关键字,必填
+     */
+    private String keyword;
+
+    /**
+     * 页码(从 1 开始),可选,默认 1
+     */
+    private Integer page;
+
+    /**
+     * 每页大小,可选,默认 20,最大 50
+     */
+    private Integer pageSize;
+
+    public String getKeyword() {
+        return keyword;
+    }
+
+    public void setKeyword(String keyword) {
+        this.keyword = keyword;
+    }
+
+    public Integer getPage() {
+        return page;
+    }
+
+    public void setPage(Integer page) {
+        this.page = page;
+    }
+
+    public Integer getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(Integer pageSize) {
+        this.pageSize = pageSize;
+    }
+}
+
+

+ 32 - 0
src/main/java/com/yingpai/gupiao/domain/dto/StockSuggestionDto.java

@@ -0,0 +1,32 @@
+package com.yingpai.gupiao.domain.dto;
+
+public class StockSuggestionDto {
+
+    private String name;
+    private String code;
+    private String market;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public String getMarket() {
+        return market;
+    }
+
+    public void setMarket(String market) {
+        this.market = market;
+    }
+}

+ 241 - 0
src/main/java/com/yingpai/gupiao/domain/po/StockInfo.java

@@ -0,0 +1,241 @@
+package com.yingpai.gupiao.domain.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+
+/**
+ * 对应 MySQL 表 stock_info
+ */
+@TableName("stock_info")
+public class StockInfo {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String stockCode;
+
+    private String stockName;
+
+    private String market;
+
+    private Integer strengthScore;
+
+    private String shortName;
+
+    private String pinyin;
+
+    private String abbr;
+
+    private LocalDate tradeDate;
+
+    // 下面这些字段目前不在搜索接口返回中使用,但保留映射方便后续扩展
+
+    private BigDecimal changePct;
+
+    private BigDecimal closePrice;
+
+    private BigDecimal totalAmount;
+
+    private BigDecimal circulationValue;
+
+    private Integer mainCycle;
+
+    private BigDecimal recentTurnover;
+
+    private Integer recentLimitUp;
+
+    private BigDecimal highPrice;
+
+    private BigDecimal lowPrice;
+
+    private BigDecimal avgPrice;
+
+    private BigDecimal lastClosePrice;
+
+    private OffsetDateTime createdAt;
+
+    private OffsetDateTime updatedAt;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getStockCode() {
+        return stockCode;
+    }
+
+    public void setStockCode(String stockCode) {
+        this.stockCode = stockCode;
+    }
+
+    public String getStockName() {
+        return stockName;
+    }
+
+    public void setStockName(String stockName) {
+        this.stockName = stockName;
+    }
+
+    public String getMarket() {
+        return market;
+    }
+
+    public void setMarket(String market) {
+        this.market = market;
+    }
+
+    public Integer getStrengthScore() {
+        return strengthScore;
+    }
+
+    public void setStrengthScore(Integer strengthScore) {
+        this.strengthScore = strengthScore;
+    }
+
+    public String getShortName() {
+        return shortName;
+    }
+
+    public void setShortName(String shortName) {
+        this.shortName = shortName;
+    }
+
+    public String getPinyin() {
+        return pinyin;
+    }
+
+    public void setPinyin(String pinyin) {
+        this.pinyin = pinyin;
+    }
+
+    public String getAbbr() {
+        return abbr;
+    }
+
+    public void setAbbr(String abbr) {
+        this.abbr = abbr;
+    }
+
+    public LocalDate getTradeDate() {
+        return tradeDate;
+    }
+
+    public void setTradeDate(LocalDate tradeDate) {
+        this.tradeDate = tradeDate;
+    }
+
+    public BigDecimal getChangePct() {
+        return changePct;
+    }
+
+    public void setChangePct(BigDecimal changePct) {
+        this.changePct = changePct;
+    }
+
+    public BigDecimal getClosePrice() {
+        return closePrice;
+    }
+
+    public void setClosePrice(BigDecimal closePrice) {
+        this.closePrice = closePrice;
+    }
+
+    public BigDecimal getTotalAmount() {
+        return totalAmount;
+    }
+
+    public void setTotalAmount(BigDecimal totalAmount) {
+        this.totalAmount = totalAmount;
+    }
+
+    public BigDecimal getCirculationValue() {
+        return circulationValue;
+    }
+
+    public void setCirculationValue(BigDecimal circulationValue) {
+        this.circulationValue = circulationValue;
+    }
+
+    public Integer getMainCycle() {
+        return mainCycle;
+    }
+
+    public void setMainCycle(Integer mainCycle) {
+        this.mainCycle = mainCycle;
+    }
+
+    public BigDecimal getRecentTurnover() {
+        return recentTurnover;
+    }
+
+    public void setRecentTurnover(BigDecimal recentTurnover) {
+        this.recentTurnover = recentTurnover;
+    }
+
+    public Integer getRecentLimitUp() {
+        return recentLimitUp;
+    }
+
+    public void setRecentLimitUp(Integer recentLimitUp) {
+        this.recentLimitUp = recentLimitUp;
+    }
+
+    public BigDecimal getHighPrice() {
+        return highPrice;
+    }
+
+    public void setHighPrice(BigDecimal highPrice) {
+        this.highPrice = highPrice;
+    }
+
+    public BigDecimal getLowPrice() {
+        return lowPrice;
+    }
+
+    public void setLowPrice(BigDecimal lowPrice) {
+        this.lowPrice = lowPrice;
+    }
+
+    public BigDecimal getAvgPrice() {
+        return avgPrice;
+    }
+
+    public void setAvgPrice(BigDecimal avgPrice) {
+        this.avgPrice = avgPrice;
+    }
+
+    public BigDecimal getLastClosePrice() {
+        return lastClosePrice;
+    }
+
+    public void setLastClosePrice(BigDecimal lastClosePrice) {
+        this.lastClosePrice = lastClosePrice;
+    }
+
+    public OffsetDateTime getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(OffsetDateTime createdAt) {
+        this.createdAt = createdAt;
+    }
+
+    public OffsetDateTime getUpdatedAt() {
+        return updatedAt;
+    }
+
+    public void setUpdatedAt(OffsetDateTime updatedAt) {
+        this.updatedAt = updatedAt;
+    }
+}
+
+

+ 60 - 0
src/main/java/com/yingpai/gupiao/domain/vo/ApiResponse.java

@@ -0,0 +1,60 @@
+package com.yingpai.gupiao.domain.vo;
+
+/**
+ * 统一返回结构
+ *
+ * {
+ *   "code": 0,
+ *   "message": "ok",
+ *   "data": {...}
+ * }
+ */
+public class ApiResponse<T> {
+
+    private int code;
+    private String message;
+    private T data;
+
+    public ApiResponse() {
+    }
+
+    public ApiResponse(int code, String message, T data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+
+    public static <T> ApiResponse<T> success(T data) {
+        return new ApiResponse<>(0, "success", data);
+    }
+
+    public static <T> ApiResponse<T> error(int code, String message) {
+        return new ApiResponse<>(code, message, null);
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public void setData(T data) {
+        this.data = data;
+    }
+}
+
+

+ 58 - 0
src/main/java/com/yingpai/gupiao/domain/vo/PageResult.java

@@ -0,0 +1,58 @@
+package com.yingpai.gupiao.domain.vo;
+
+import java.util.List;
+
+/**
+ * data 字段中分页结构
+ */
+public class PageResult<T> {
+
+    private List<T> list;
+    private int page;
+    private int pageSize;
+    private long total;
+
+    public PageResult() {
+    }
+
+    public PageResult(List<T> list, int page, int pageSize, long total) {
+        this.list = list;
+        this.page = page;
+        this.pageSize = pageSize;
+        this.total = total;
+    }
+
+    public List<T> getList() {
+        return list;
+    }
+
+    public void setList(List<T> list) {
+        this.list = list;
+    }
+
+    public int getPage() {
+        return page;
+    }
+
+    public void setPage(int page) {
+        this.page = page;
+    }
+
+    public int getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(int pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    public long getTotal() {
+        return total;
+    }
+
+    public void setTotal(long total) {
+        this.total = total;
+    }
+}
+
+

+ 119 - 0
src/main/java/com/yingpai/gupiao/domain/vo/StockDetailResponse.java

@@ -0,0 +1,119 @@
+package com.yingpai.gupiao.domain.vo;
+
+import java.util.List;
+
+public class StockDetailResponse {
+
+    private String stockCode;
+    private String stockName;
+    private String market;
+    private Double score;
+    private List<HistoryScore> history;
+    private List<Factor> factors;
+
+    public String getStockCode() {
+        return stockCode;
+    }
+
+    public void setStockCode(String stockCode) {
+        this.stockCode = stockCode;
+    }
+
+    public String getStockName() {
+        return stockName;
+    }
+
+    public void setStockName(String stockName) {
+        this.stockName = stockName;
+    }
+
+    public String getMarket() {
+        return market;
+    }
+
+    public void setMarket(String market) {
+        this.market = market;
+    }
+
+    public Double getScore() {
+        return score;
+    }
+
+    public void setScore(Double score) {
+        this.score = score;
+    }
+
+    public List<HistoryScore> getHistory() {
+        return history;
+    }
+
+    public void setHistory(List<HistoryScore> history) {
+        this.history = history;
+    }
+
+    public List<Factor> getFactors() {
+        return factors;
+    }
+
+    public void setFactors(List<Factor> factors) {
+        this.factors = factors;
+    }
+
+    public static class HistoryScore {
+        private String date;
+        private Double score;
+
+        public HistoryScore() {
+        }
+
+        public HistoryScore(String date, Double score) {
+            this.date = date;
+            this.score = score;
+        }
+
+        public String getDate() {
+            return date;
+        }
+
+        public void setDate(String date) {
+            this.date = date;
+        }
+
+        public Double getScore() {
+            return score;
+        }
+
+        public void setScore(Double score) {
+            this.score = score;
+        }
+    }
+
+    public static class Factor {
+        private String name;
+        private Double value;
+
+        public Factor() {
+        }
+
+        public Factor(String name, Double value) {
+            this.name = name;
+            this.value = value;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public Double getValue() {
+            return value;
+        }
+
+        public void setValue(Double value) {
+            this.value = value;
+        }
+    }
+}

+ 73 - 0
src/main/java/com/yingpai/gupiao/domain/vo/StockSearchResponseItem.java

@@ -0,0 +1,73 @@
+package com.yingpai.gupiao.domain.vo;
+
+/**
+ * 搜索结果中单只股票的信息
+ */
+public class StockSearchResponseItem {
+
+    private String stockCode;
+    private String stockName;
+    private String market;
+    private String shortName;
+    private String pinyin;
+    private Integer score;
+    private String scoreDate;
+
+    public String getStockCode() {
+        return stockCode;
+    }
+
+    public void setStockCode(String stockCode) {
+        this.stockCode = stockCode;
+    }
+
+    public String getStockName() {
+        return stockName;
+    }
+
+    public void setStockName(String stockName) {
+        this.stockName = stockName;
+    }
+
+    public String getMarket() {
+        return market;
+    }
+
+    public void setMarket(String market) {
+        this.market = market;
+    }
+
+    public String getShortName() {
+        return shortName;
+    }
+
+    public void setShortName(String shortName) {
+        this.shortName = shortName;
+    }
+
+    public String getPinyin() {
+        return pinyin;
+    }
+
+    public void setPinyin(String pinyin) {
+        this.pinyin = pinyin;
+    }
+
+    public Integer getScore() {
+        return score;
+    }
+
+    public void setScore(Integer score) {
+        this.score = score;
+    }
+
+    public String getScoreDate() {
+        return scoreDate;
+    }
+
+    public void setScoreDate(String scoreDate) {
+        this.scoreDate = scoreDate;
+    }
+}
+
+

+ 38 - 0
src/main/java/com/yingpai/gupiao/handler/GlobalExceptionHandler.java

@@ -0,0 +1,38 @@
+package com.yingpai.gupiao.handler;
+
+import com.yingpai.gupiao.domain.vo.ApiResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 全局异常处理器
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+    /**
+     * 处理参数异常
+     */
+    @ExceptionHandler(IllegalArgumentException.class)
+    public ResponseEntity<ApiResponse<Void>> handleIllegalArgumentException(IllegalArgumentException ex) {
+        log.warn("参数错误: {}", ex.getMessage());
+        ApiResponse<Void> body = ApiResponse.error(1003, ex.getMessage() != null ? ex.getMessage() : "参数错误");
+        return new ResponseEntity<>(body, HttpStatus.OK);
+    }
+
+    /**
+     * 处理所有未捕获的异常
+     */
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
+        log.error("服务器内部错误", ex);
+        ApiResponse<Void> body = ApiResponse.error(5000, "服务器内部错误");
+        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
+    }
+}

+ 11 - 0
src/main/java/com/yingpai/gupiao/mapper/StockInfoMapper.java

@@ -0,0 +1,11 @@
+package com.yingpai.gupiao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yingpai.gupiao.domain.po.StockInfo;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface StockInfoMapper extends BaseMapper<StockInfo> {
+}
+
+

+ 16 - 0
src/main/java/com/yingpai/gupiao/service/StockSearchService.java

@@ -0,0 +1,16 @@
+package com.yingpai.gupiao.service;
+
+import com.yingpai.gupiao.domain.dto.StockSearchRequest;
+import com.yingpai.gupiao.domain.dto.StockSuggestionDto;
+import com.yingpai.gupiao.domain.vo.StockDetailResponse;
+
+import java.util.List;
+
+public interface StockSearchService {
+
+    StockDetailResponse search(StockSearchRequest request);
+
+    List<StockSuggestionDto> getSuggestion(String keyword);
+
+    StockDetailResponse getStockDetail(String stockCode);
+}

+ 111 - 0
src/main/java/com/yingpai/gupiao/service/impl/StockSearchServiceImpl.java

@@ -0,0 +1,111 @@
+package com.yingpai.gupiao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yingpai.gupiao.domain.dto.StockSearchRequest;
+import com.yingpai.gupiao.domain.dto.StockSuggestionDto;
+import com.yingpai.gupiao.domain.po.StockInfo;
+import com.yingpai.gupiao.domain.vo.StockDetailResponse;
+import com.yingpai.gupiao.mapper.StockInfoMapper;
+import com.yingpai.gupiao.service.StockSearchService;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@AllArgsConstructor
+public class StockSearchServiceImpl implements StockSearchService {
+
+    private final StockInfoMapper stockInfoMapper;
+
+    @Override
+    public StockDetailResponse search(StockSearchRequest request) {
+        String keyword = request.getKeyword();
+        if (!StringUtils.hasText(keyword)) {
+            throw new IllegalArgumentException("keyword 不能为空");
+        }
+        return getStockDetail(keyword);
+    }
+
+    @Override
+    public List<StockSuggestionDto> getSuggestion(String keyword) {
+        if (!StringUtils.hasText(keyword)) {
+            return List.of();
+        }
+
+        // 1. 获取最新日期
+        LambdaQueryWrapper<StockInfo> maxDateWrapper = new LambdaQueryWrapper<StockInfo>()
+                .select(StockInfo::getTradeDate)
+                .orderByDesc(StockInfo::getTradeDate)
+                .last("limit 1");
+        List<StockInfo> maxDateList = stockInfoMapper.selectList(maxDateWrapper);
+        if (maxDateList.isEmpty() || maxDateList.get(0).getTradeDate() == null) {
+            return List.of();
+        }
+        LocalDate latestDate = maxDateList.get(0).getTradeDate();
+
+        // 2. 查询
+        LambdaQueryWrapper<StockInfo> wrapper = new LambdaQueryWrapper<StockInfo>()
+                .eq(StockInfo::getTradeDate, latestDate)
+                .and(w -> w
+                        .likeRight(StockInfo::getStockCode, keyword) // 代码前缀匹配
+                        .or()
+                        .like(StockInfo::getStockName, keyword)      // 名称包含匹配
+                        .or()
+                        .like(StockInfo::getAbbr, keyword)           // 拼音缩写包含匹配
+                )
+                .orderByAsc(StockInfo::getStockCode) // 可选:按代码排序
+                .last("limit 10");
+
+        List<StockInfo> list = stockInfoMapper.selectList(wrapper);
+
+        return list.stream().map(info -> {
+            StockSuggestionDto dto = new StockSuggestionDto();
+            dto.setName(info.getStockName());
+            dto.setCode(info.getStockCode());
+            dto.setMarket(info.getMarket());
+            return dto;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public StockDetailResponse getStockDetail(String stockCode) {
+        if (!StringUtils.hasText(stockCode)) {
+            throw new IllegalArgumentException("参数错误:stockCode不能为空");
+        }
+        String finalStockCode = stockCode.trim();
+
+        // 修改点:直接查询该股票最新的一条记录,不再依赖全局最新日期
+        LambdaQueryWrapper<StockInfo> wrapper = new LambdaQueryWrapper<StockInfo>()
+                .eq(StockInfo::getStockCode, finalStockCode)
+                .orderByDesc(StockInfo::getTradeDate)
+                .last("limit 1");
+
+        StockInfo stockInfo = stockInfoMapper.selectOne(wrapper);
+
+        if (stockInfo == null) {
+            return null;
+        }
+
+        return buildStockDetailResponse(stockInfo);
+    }
+
+    private StockDetailResponse buildStockDetailResponse(StockInfo stockInfo) {
+        StockDetailResponse response = new StockDetailResponse();
+        response.setStockCode(stockInfo.getStockCode());
+        response.setStockName(stockInfo.getStockName());
+        response.setMarket(stockInfo.getMarket());
+        
+        // 最新量化评分也为null
+        response.setScore(null);
+
+        // 简化返回,暂时不返回历史数据和因子数据
+        response.setHistory(null);
+        response.setFactors(null);
+
+        return response;
+    }
+}