Parcourir la source

历史信息管理

Zhangbw il y a 3 mois
Parent
commit
9c701b243c
17 fichiers modifiés avec 1027 ajouts et 58 suppressions
  1. 1 0
      ruoyi-admin/src/main/resources/application.yml
  2. 1 6
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/MiniappUser.java
  3. 1 7
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/bo/MiniappUserBo.java
  4. 1 6
      ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/vo/MiniappUserVo.java
  5. 99 0
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/controller/StockPoolHistoryController.java
  6. 1 8
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/StockInfo.java
  7. 1 6
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/StockPool.java
  8. 103 0
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/StockPoolHistory.java
  9. 1 7
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/bo/StockInfoBo.java
  10. 1 6
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/bo/StockPoolBo.java
  11. 103 0
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/bo/StockPoolHistoryBo.java
  12. 1 6
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/vo/StockInfoVo.java
  13. 135 0
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/vo/StockPoolHistoryVo.java
  14. 1 6
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/vo/StockPoolVo.java
  15. 11 0
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/mapper/StockPoolHistoryMapper.java
  16. 54 0
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/service/IStockPoolHistoryService.java
  17. 512 0
      ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/service/impl/StockPoolHistoryServiceImpl.java

+ 1 - 0
ruoyi-admin/src/main/resources/application.yml

@@ -141,6 +141,7 @@ tenant:
     - stock_pool
     - user_subscription
     - payment_order
+    - stock_pool_history
 
 # MyBatisPlus配置
 # https://baomidou.com/config/

+ 1 - 6
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/MiniappUser.java

@@ -5,8 +5,6 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 
-import java.io.Serial;
-import java.io.Serializable;
 import java.time.LocalDateTime;
 
 /**
@@ -15,10 +13,7 @@ import java.time.LocalDateTime;
  */
 @Data
 @TableName("user")
-public class MiniappUser implements Serializable {
-
-    @Serial
-    private static final long serialVersionUID = 1L;
+public class MiniappUser {
 
     /**
      * 用户ID,主键自增

+ 1 - 7
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/bo/MiniappUserBo.java

@@ -2,17 +2,11 @@ package com.yingpai.miniapp.domain.bo;
 
 import lombok.Data;
 
-import java.io.Serial;
-import java.io.Serializable;
-
 /**
  * 小程序用户查询对象
  */
 @Data
-public class MiniappUserBo implements Serializable {
-
-    @Serial
-    private static final long serialVersionUID = 1L;
+public class MiniappUserBo {
 
     /**
      * 用户ID

+ 1 - 6
ruoyi-modules/yp-miniapp/src/main/java/com/yingpai/miniapp/domain/vo/MiniappUserVo.java

@@ -7,8 +7,6 @@ import com.yingpai.miniapp.domain.MiniappUser;
 import io.github.linpeilie.annotations.AutoMapper;
 import lombok.Data;
 
-import java.io.Serial;
-import java.io.Serializable;
 import java.time.LocalDateTime;
 
 /**
@@ -16,10 +14,7 @@ import java.time.LocalDateTime;
  */
 @Data
 @AutoMapper(target = MiniappUser.class)
-public class MiniappUserVo implements Serializable {
-
-    @Serial
-    private static final long serialVersionUID = 1L;
+public class MiniappUserVo {
 
     /**
      * 用户ID

+ 99 - 0
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/controller/StockPoolHistoryController.java

@@ -0,0 +1,99 @@
+package com.yingpai.stock.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.yingpai.stock.domain.bo.StockPoolHistoryBo;
+import com.yingpai.stock.domain.vo.StockPoolHistoryVo;
+import com.yingpai.stock.service.IStockPoolHistoryService;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+/**
+ * 股票池历史数据管理
+ */
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/stock/history")
+public class StockPoolHistoryController extends BaseController {
+
+    private final IStockPoolHistoryService stockPoolHistoryService;
+
+    /**
+     * 查询历史数据列表
+     */
+    @SaCheckPermission("stock:history:list")
+    @GetMapping("/list")
+    public TableDataInfo<StockPoolHistoryVo> list(StockPoolHistoryBo bo, PageQuery pageQuery) {
+        return stockPoolHistoryService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 获取历史数据详细信息
+     */
+    @SaCheckPermission("stock:history:query")
+    @GetMapping("/{id}")
+    public R<StockPoolHistoryVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return R.ok(stockPoolHistoryService.queryById(id));
+    }
+
+    /**
+     * 新增历史数据
+     */
+    @SaCheckPermission("stock:history:add")
+    @Log(title = "历史数据", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public R<Void> add(@Validated @RequestBody StockPoolHistoryBo bo) {
+        return toAjax(stockPoolHistoryService.insertByBo(bo));
+    }
+
+    /**
+     * 修改历史数据
+     */
+    @SaCheckPermission("stock:history:edit")
+    @Log(title = "历史数据", businessType = BusinessType.UPDATE)
+    @PutMapping("/edit")
+    public R<Void> edit(@Validated @RequestBody StockPoolHistoryBo bo) {
+        return toAjax(stockPoolHistoryService.updateByBo(bo));
+    }
+
+    /**
+     * 删除历史数据
+     */
+    @SaCheckPermission("stock:history:remove")
+    @Log(title = "历史数据", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
+        return toAjax(stockPoolHistoryService.deleteWithValidByIds(List.of(ids)));
+    }
+
+    /**
+     * 导入历史数据(Excel)
+     * @param file Excel文件
+     * @param recordDate 管理员指定的记录日期
+     * @param updateSupport 是否更新已存在的数据
+     */
+    @SaCheckPermission("stock:history:import")
+    @Log(title = "历史数据", businessType = BusinessType.IMPORT)
+    @PostMapping("/importData")
+    public R<Void> importData(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam("recordDate") String recordDate,
+            @RequestParam(value = "updateSupport", defaultValue = "true") boolean updateSupport) throws Exception {
+        String message = stockPoolHistoryService.importData(file, recordDate, updateSupport);
+        return R.ok(message);
+    }
+}

+ 1 - 8
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/StockInfo.java

@@ -1,14 +1,10 @@
 package com.yingpai.stock.domain;
 
-import com.baomidou.mybatisplus.annotation.FieldFill;
 import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 
-import java.io.Serial;
-import java.io.Serializable;
 import java.time.LocalDateTime;
 
 /**
@@ -17,10 +13,7 @@ import java.time.LocalDateTime;
  */
 @Data
 @TableName(value = "stock_info", excludeProperty = {"tenantId", "createDept"})
-public class StockInfo implements Serializable {
-
-    @Serial
-    private static final long serialVersionUID = 1L;
+public class StockInfo {
 
     /**
      * 自增主键

+ 1 - 6
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/StockPool.java

@@ -5,8 +5,6 @@ import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 
-import java.io.Serial;
-import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -17,10 +15,7 @@ import java.time.LocalDateTime;
  */
 @Data
 @TableName(value = "stock_pool", excludeProperty = {"tenantId", "createDept"})
-public class StockPool implements Serializable {
-
-    @Serial
-    private static final long serialVersionUID = 1L;
+public class StockPool {
 
     /**
      * 主键ID

+ 103 - 0
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/StockPoolHistory.java

@@ -0,0 +1,103 @@
+package com.yingpai.stock.domain;
+
+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;
+
+/**
+ * 股票池历史数据实体类
+ * 对应数据库表 stock_pool_history
+ */
+@Data
+@TableName(value = "stock_pool_history", excludeProperty = {"tenantId", "createDept"})
+public class StockPoolHistory {
+    /**
+     * 自增主键
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 管理员选择的记录日期
+     */
+    private LocalDate recordDate;
+
+    /**
+     * 股票代码
+     */
+    private String stockCode;
+
+    /**
+     * 股票名称
+     */
+    private String stockName;
+
+    /**
+     * 涨幅%
+     */
+    private BigDecimal changePercent;
+
+    /**
+     * 收盘价
+     */
+    private BigDecimal closePrice;
+
+    /**
+     * 总金额
+     */
+    private BigDecimal totalAmount;
+
+    /**
+     * 强度评分
+     */
+    private BigDecimal strengthScore;
+
+    /**
+     * 流通市值
+     */
+    private BigDecimal circulationMarketValue;
+
+    /**
+     * 主升周期
+     */
+    private String mainRisePeriod;
+
+    /**
+     * 近期涨手
+     */
+    private String recentRiseHand;
+
+    /**
+     * 近期涨停
+     */
+    private String recentLimitUp;
+
+    /**
+     * 当天最高价
+     */
+    private BigDecimal dayHighestPrice;
+
+    /**
+     * 当天最低价
+     */
+    private BigDecimal dayLowestPrice;
+
+    /**
+     * 当天均价
+     */
+    private BigDecimal dayAvgPrice;
+
+    /**
+     * 当天收盘价
+     */
+    private BigDecimal dayClosePrice;
+
+    /**
+     * 操作管理员ID
+     */
+    private Long adminId;
+}

+ 1 - 7
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/bo/StockInfoBo.java

@@ -2,17 +2,11 @@ package com.yingpai.stock.domain.bo;
 
 import lombok.Data;
 
-import java.io.Serial;
-import java.io.Serializable;
-
 /**
  * 股票信息查询对象
  */
 @Data
-public class StockInfoBo implements Serializable {
-
-    @Serial
-    private static final long serialVersionUID = 1L;
+public class StockInfoBo {
 
     /**
      * 股票ID

+ 1 - 6
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/bo/StockPoolBo.java

@@ -2,8 +2,6 @@ package com.yingpai.stock.domain.bo;
 
 import lombok.Data;
 
-import java.io.Serial;
-import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 
@@ -11,10 +9,7 @@ import java.time.LocalDate;
  * 股票池查询/新增对象
  */
 @Data
-public class StockPoolBo implements Serializable {
-
-    @Serial
-    private static final long serialVersionUID = 1L;
+public class StockPoolBo {
 
     /**
      * 主键ID

+ 103 - 0
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/bo/StockPoolHistoryBo.java

@@ -0,0 +1,103 @@
+package com.yingpai.stock.domain.bo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * 股票池历史查询/新增对象
+ */
+@Data
+public class StockPoolHistoryBo {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 记录日期
+     */
+    private LocalDate recordDate;
+
+    /**
+     * 股票代码
+     */
+    private String stockCode;
+
+    /**
+     * 股票名称
+     */
+    private String stockName;
+
+    /**
+     * 涨幅%
+     */
+    private BigDecimal changePercent;
+
+    /**
+     * 收盘价
+     */
+    private BigDecimal closePrice;
+
+    /**
+     * 总金额
+     */
+    private BigDecimal totalAmount;
+
+    /**
+     * 强度评分
+     */
+    private BigDecimal strengthScore;
+
+    /**
+     * 流通市值
+     */
+    private BigDecimal circulationMarketValue;
+
+    /**
+     * 主升周期
+     */
+    private String mainRisePeriod;
+
+    /**
+     * 近期涨手
+     */
+    private String recentRiseHand;
+
+    /**
+     * 近期涨停
+     */
+    private String recentLimitUp;
+
+    /**
+     * 当天最高价
+     */
+    private BigDecimal dayHighestPrice;
+
+    /**
+     * 当天最低价
+     */
+    private BigDecimal dayLowestPrice;
+
+    /**
+     * 当天均价
+     */
+    private BigDecimal dayAvgPrice;
+
+    /**
+     * 当天收盘价
+     */
+    private BigDecimal dayClosePrice;
+
+    /**
+     * 查询开始日期
+     */
+    private LocalDate startDate;
+
+    /**
+     * 查询结束日期
+     */
+    private LocalDate endDate;
+}

+ 1 - 6
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/vo/StockInfoVo.java

@@ -7,8 +7,6 @@ import com.yingpai.stock.domain.StockInfo;
 import io.github.linpeilie.annotations.AutoMapper;
 import lombok.Data;
 
-import java.io.Serial;
-import java.io.Serializable;
 import java.time.LocalDateTime;
 
 /**
@@ -16,10 +14,7 @@ import java.time.LocalDateTime;
  */
 @Data
 @AutoMapper(target = StockInfo.class)
-public class StockInfoVo implements Serializable {
-
-    @Serial
-    private static final long serialVersionUID = 1L;
+public class StockInfoVo {
 
     /**
      * 股票ID

+ 135 - 0
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/vo/StockPoolHistoryVo.java

@@ -0,0 +1,135 @@
+package com.yingpai.stock.domain.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.idev.excel.annotation.write.style.ColumnWidth;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.yingpai.stock.domain.StockPoolHistory;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 股票池历史视图对象
+ */
+@Data
+@AutoMapper(target = StockPoolHistory.class)
+public class StockPoolHistoryVo {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 记录日期
+     */
+    @ColumnWidth(15)
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private LocalDate recordDate;
+
+    /**
+     * 股票代码
+     */
+    @ExcelProperty(index = 0)
+    private String stockCode;
+
+    /**
+     * 股票名称
+     */
+    @ExcelProperty(index = 1)
+    private String stockName;
+
+    /**
+     * 涨幅%
+     */
+    @ExcelProperty(index = 2)
+    private BigDecimal changePercent;
+
+    /**
+     * 收盘价
+     */
+    @ExcelProperty(index = 3)
+    private BigDecimal closePrice;
+
+    /**
+     * 总金额
+     */
+    @ExcelProperty(index = 4)
+    private BigDecimal totalAmount;
+
+    /**
+     * 强度评分
+     */
+    @ExcelProperty(index = 5)
+    private BigDecimal strengthScore;
+
+    /**
+     * 流通市值
+     */
+    @ExcelProperty(index = 6)
+    private BigDecimal circulationMarketValue;
+
+    /**
+     * 主升周期
+     */
+    @ExcelProperty(index = 7)
+    private String mainRisePeriod;
+
+    /**
+     * 近期涨手
+     */
+    @ExcelProperty(index = 8)
+    private String recentRiseHand;
+
+    /**
+     * 近期涨停
+     */
+    @ExcelProperty(index = 9)
+    private String recentLimitUp;
+
+    /**
+     * 当天最高价
+     */
+    @ExcelProperty(index = 10)
+    private BigDecimal dayHighestPrice;
+
+    /**
+     * 当天最低价
+     */
+    @ExcelProperty(index = 11)
+    private BigDecimal dayLowestPrice;
+
+    /**
+     * 当天均价
+     */
+    @ExcelProperty(index = 12)
+    private BigDecimal dayAvgPrice;
+
+    /**
+     * 当天收盘价
+     */
+    @ExcelProperty(index = 13)
+    private BigDecimal dayClosePrice;
+
+    /**
+     * 操作管理员ID
+     */
+    private Long adminId;
+
+    /**
+     * 创建时间
+     */
+    @ColumnWidth(20)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @ColumnWidth(20)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}

+ 1 - 6
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/domain/vo/StockPoolVo.java

@@ -7,8 +7,6 @@ import com.yingpai.stock.domain.StockPool;
 import io.github.linpeilie.annotations.AutoMapper;
 import lombok.Data;
 
-import java.io.Serial;
-import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -18,10 +16,7 @@ import java.time.LocalDateTime;
  */
 @Data
 @AutoMapper(target = StockPool.class)
-public class StockPoolVo implements Serializable {
-
-    @Serial
-    private static final long serialVersionUID = 1L;
+public class StockPoolVo {
 
     /**
      * 主键ID

+ 11 - 0
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/mapper/StockPoolHistoryMapper.java

@@ -0,0 +1,11 @@
+package com.yingpai.stock.mapper;
+
+import com.yingpai.stock.domain.StockPoolHistory;
+import com.yingpai.stock.domain.vo.StockPoolHistoryVo;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+
+/**
+ * 股票池历史Mapper接口
+ */
+public interface StockPoolHistoryMapper extends BaseMapperPlus<StockPoolHistory, StockPoolHistoryVo> {
+}

+ 54 - 0
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/service/IStockPoolHistoryService.java

@@ -0,0 +1,54 @@
+package com.yingpai.stock.service;
+
+import com.yingpai.stock.domain.bo.StockPoolHistoryBo;
+import com.yingpai.stock.domain.vo.StockPoolHistoryVo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 股票池历史服务接口
+ */
+public interface IStockPoolHistoryService {
+
+    /**
+     * 查询历史数据列表(分页)
+     */
+    TableDataInfo<StockPoolHistoryVo> queryPageList(StockPoolHistoryBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询历史数据列表
+     */
+    List<StockPoolHistoryVo> queryList(StockPoolHistoryBo bo);
+
+    /**
+     * 根据ID查询历史数据详情
+     */
+    StockPoolHistoryVo queryById(Long id);
+
+    /**
+     * 新增历史数据
+     */
+    Boolean insertByBo(StockPoolHistoryBo bo);
+
+    /**
+     * 修改历史数据
+     */
+    Boolean updateByBo(StockPoolHistoryBo bo);
+
+    /**
+     * 批量删除历史数据
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids);
+
+    /**
+     * 导入历史数据(Excel)
+     * @param file Excel文件
+     * @param recordDate 管理员指定的记录日期(格式:yyyy-MM-dd)
+     * @param updateSupport 是否更新已存在的数据
+     */
+    String importData(MultipartFile file, String recordDate, Boolean updateSupport) throws Exception;
+}

+ 512 - 0
ruoyi-modules/yp-stock/src/main/java/com/yingpai/stock/service/impl/StockPoolHistoryServiceImpl.java

@@ -0,0 +1,512 @@
+package com.yingpai.stock.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.idev.excel.EasyExcel;
+import cn.idev.excel.context.AnalysisContext;
+import cn.idev.excel.read.listener.ReadListener;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.yingpai.stock.domain.StockPoolHistory;
+import com.yingpai.stock.domain.bo.StockPoolHistoryBo;
+import com.yingpai.stock.domain.vo.StockPoolHistoryVo;
+import com.yingpai.stock.mapper.StockPoolHistoryMapper;
+import com.yingpai.stock.service.IStockPoolHistoryService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.nio.charset.Charset;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 股票池历史服务实现类
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class StockPoolHistoryServiceImpl implements IStockPoolHistoryService {
+
+    private final StockPoolHistoryMapper baseMapper;
+
+    @Override
+    public TableDataInfo<StockPoolHistoryVo> queryPageList(StockPoolHistoryBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<StockPoolHistory> lqw = buildQueryWrapper(bo);
+        Page<StockPoolHistoryVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    @Override
+    public List<StockPoolHistoryVo> queryList(StockPoolHistoryBo bo) {
+        LambdaQueryWrapper<StockPoolHistory> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    @Override
+    public StockPoolHistoryVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+    @Override
+    public Boolean insertByBo(StockPoolHistoryBo bo) {
+        StockPoolHistory entity = BeanUtil.toBean(bo, StockPoolHistory.class);
+        entity.setAdminId(LoginHelper.getUserId());
+        boolean flag = baseMapper.insert(entity) > 0;
+        if (flag) {
+            bo.setId(entity.getId());
+        }
+        return flag;
+    }
+
+    @Override
+    public Boolean updateByBo(StockPoolHistoryBo bo) {
+        StockPoolHistory entity = BeanUtil.toBean(bo, StockPoolHistory.class);
+        return baseMapper.updateById(entity) > 0;
+    }
+
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids) {
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String importData(MultipartFile file, String recordDate, Boolean updateSupport) throws Exception {
+        // 解析记录日期
+        LocalDate parsedRecordDate;
+        try {
+            parsedRecordDate = LocalDate.parse(recordDate);
+        } catch (Exception e) {
+            throw new RuntimeException("记录日期格式错误,请使用 yyyy-MM-dd 格式");
+        }
+
+        // 读取 Excel 数据
+        String originalFilename = file.getOriginalFilename();
+        log.info("导入文件名: {}, 文件大小: {} bytes", originalFilename, file.getSize());
+        
+        // 判断文件类型
+        boolean isXls = originalFilename != null && originalFilename.toLowerCase().endsWith(".xls") 
+            && !originalFilename.toLowerCase().endsWith(".xlsx");
+        log.info("文件类型: {}", isXls ? "xls (Excel 97-2003)" : "xlsx (Excel 2007+)");
+        
+        // 使用 Map 方式读取原始数据,避免类型转换问题
+        List<Map<Integer, String>> rawDataList = new ArrayList<>();
+        
+        // 使用 BufferedInputStream 支持 mark/reset
+        InputStream inputStream = new BufferedInputStream(file.getInputStream());
+        
+        try {
+            if (isXls) {
+                // .xls 文件使用 GBK 编码读取(解决中文乱码问题)
+                log.info("使用 GBK 编码读取 .xls 文件");
+                EasyExcel.read(inputStream, new ReadListener<Map<Integer, String>>() {
+                    @Override
+                    public void invoke(Map<Integer, String> data, AnalysisContext context) {
+                        rawDataList.add(data);
+                    }
+                    @Override
+                    public void doAfterAllAnalysed(AnalysisContext context) {
+                        log.info("Excel 读取完成,共 {} 行", rawDataList.size());
+                    }
+                }).charset(Charset.forName("GBK")).sheet().headRowNumber(1).doRead();
+            } else {
+                // .xlsx 文件让 EasyExcel 自动处理
+                log.info("读取 .xlsx 文件");
+                EasyExcel.read(inputStream, new ReadListener<Map<Integer, String>>() {
+                    @Override
+                    public void invoke(Map<Integer, String> data, AnalysisContext context) {
+                        rawDataList.add(data);
+                    }
+                    @Override
+                    public void doAfterAllAnalysed(AnalysisContext context) {
+                        log.info("Excel 读取完成,共 {} 行", rawDataList.size());
+                    }
+                }).sheet().headRowNumber(1).doRead();
+            }
+        } catch (Exception e) {
+            log.error("Excel 读取失败: {}", e.getMessage());
+            throw new RuntimeException("无法读取Excel文件,请确保文件格式正确(支持 .xls 和 .xlsx 格式)。错误: " + e.getMessage());
+        }
+
+        if (rawDataList.isEmpty()) {
+            return "导入数据为空";
+        }
+
+        log.info("读取到 {} 条原始数据", rawDataList.size());
+        
+        // 输出前5条原始数据用于调试
+        for (int i = 0; i < Math.min(5, rawDataList.size()); i++) {
+            Map<Integer, String> row = rawDataList.get(i);
+            log.info("=== 调试 - 第{}条原始数据(共{}列)===", i + 1, row.size());
+            for (Map.Entry<Integer, String> entry : row.entrySet()) {
+                String value = entry.getValue();
+                log.info("  列{}: [{}] (长度:{})", entry.getKey(), 
+                    value != null && value.length() > 50 ? value.substring(0, 50) + "..." : value,
+                    value != null ? value.length() : 0);
+            }
+        }
+        
+        // 检查是否所有数据都在一列中(可能是制表符分隔的文本)
+        if (!rawDataList.isEmpty()) {
+            Map<Integer, String> firstRow = rawDataList.get(0);
+            if (firstRow.size() == 1) {
+                // 只有一列数据,尝试按制表符分割
+                log.warn("检测到只有1列数据,尝试按制表符分割...");
+                List<Map<Integer, String>> splitDataList = new ArrayList<>();
+                for (Map<Integer, String> row : rawDataList) {
+                    String singleValue = row.get(0);
+                    if (singleValue != null) {
+                        String[] parts = singleValue.split("\t");
+                        Map<Integer, String> newRow = new java.util.HashMap<>();
+                        for (int j = 0; j < parts.length; j++) {
+                            newRow.put(j, parts[j].trim());
+                        }
+                        splitDataList.add(newRow);
+                    }
+                }
+                if (!splitDataList.isEmpty() && splitDataList.get(0).size() > 1) {
+                    log.info("制表符分割成功,每行 {} 列", splitDataList.get(0).size());
+                    rawDataList.clear();
+                    rawDataList.addAll(splitDataList);
+                    
+                    // 再次输出调试信息
+                    for (int i = 0; i < Math.min(3, rawDataList.size()); i++) {
+                        Map<Integer, String> row = rawDataList.get(i);
+                        log.info("分割后第{}条: 列0=[{}], 列1=[{}], 列2=[{}]", 
+                            i + 1, row.get(0), row.get(1), row.get(2));
+                    }
+                }
+            }
+        }
+        
+        // 将原始数据转换为 StockPoolHistoryVo
+        List<StockPoolHistoryVo> dataList = new ArrayList<>();
+        for (Map<Integer, String> row : rawDataList) {
+            StockPoolHistoryVo vo = new StockPoolHistoryVo();
+            vo.setStockCode(getStringValue(row, 0));
+            vo.setStockName(getStringValue(row, 1));
+            vo.setChangePercent(getBigDecimalValue(row, 2));
+            vo.setClosePrice(getBigDecimalValue(row, 3));
+            vo.setTotalAmount(getBigDecimalValue(row, 4));
+            vo.setStrengthScore(getBigDecimalValue(row, 5));
+            vo.setCirculationMarketValue(getBigDecimalValue(row, 6));
+            vo.setMainRisePeriod(getStringValue(row, 7));
+            vo.setRecentRiseHand(getStringValue(row, 8));
+            vo.setRecentLimitUp(getStringValue(row, 9));
+            vo.setDayHighestPrice(getBigDecimalValue(row, 10));
+            vo.setDayLowestPrice(getBigDecimalValue(row, 11));
+            vo.setDayAvgPrice(getBigDecimalValue(row, 12));
+            vo.setDayClosePrice(getBigDecimalValue(row, 13));
+            dataList.add(vo);
+        }
+
+        log.info("转换后数据 {} 条", dataList.size());
+        
+        // 输出前5条转换后的数据用于调试
+        for (int i = 0; i < Math.min(5, dataList.size()); i++) {
+            StockPoolHistoryVo debugVo = dataList.get(i);
+            log.info("转换后第{}条: 代码=[{}], 名称=[{}], 涨幅=[{}], 收盘=[{}]", 
+                i + 1, debugVo.getStockCode(), debugVo.getStockName(), 
+                debugVo.getChangePercent(), debugVo.getClosePrice());
+        }
+
+        int successNum = 0;
+        int updateNum = 0;
+        int failureNum = 0;
+        StringBuilder successMsg = new StringBuilder();
+        StringBuilder failureMsg = new StringBuilder();
+
+        for (int i = 0; i < dataList.size(); i++) {
+            StockPoolHistoryVo vo = dataList.get(i);
+            try {
+                // 处理股票代码(去除可能的引号和公式格式)
+                String stockCode = vo.getStockCode();
+                String stockName = vo.getStockName();
+                
+                if (stockCode != null) {
+                    // 去除前后空格
+                    stockCode = stockCode.trim();
+                    
+                    // 调试日志:记录前几条数据的原始值
+                    if (i < 5) {
+                        log.info("第{}条数据 - 处理前代码: [{}], 长度: {}, 名称: [{}]", 
+                            i + 1, stockCode, stockCode.length(), stockName);
+                    }
+                    
+                    // 处理公式格式 ="000547"
+                    if (stockCode.startsWith("=")) {
+                        stockCode = stockCode.substring(1);
+                    }
+                    
+                    // 去除所有类型的引号(使用 Unicode 转义避免编码问题)
+                    stockCode = stockCode.replace("\"", "");  // 双引号
+                    stockCode = stockCode.replace("'", "");   // 单引号
+                    stockCode = stockCode.replace("\u201C", "");  // 中文左双引号
+                    stockCode = stockCode.replace("\u201D", "");  // 中文右双引号
+                    stockCode = stockCode.replace("\u2018", "");  // 中文左单引号
+                    stockCode = stockCode.replace("\u2019", "");  // 中文右单引号
+                    
+                    // 去除所有空白字符(包括空格、制表符、换行符等)
+                    stockCode = stockCode.replaceAll("\\s+", "");
+                    
+                    // 只保留数字和字母(股票代码只应该包含这些字符)
+                    stockCode = stockCode.replaceAll("[^0-9A-Za-z]", "");
+                    
+                    // 确保长度不超过20个字符(数据库限制)
+                    if (stockCode.length() > 20) {
+                        log.warn("第{}条数据 - 股票代码过长,截取前20位: [{}] -> [{}]", 
+                            i + 1, stockCode, stockCode.substring(0, 20));
+                        stockCode = stockCode.substring(0, 20);
+                    }
+                    
+                    // 调试日志:记录处理后的值
+                    if (i < 5) {
+                        log.info("第{}条数据 - 处理后代码: [{}], 长度: {}", i + 1, stockCode, stockCode.length());
+                    }
+                    
+                    vo.setStockCode(stockCode);
+                }
+                
+                // 处理股票名称(去除可能的特殊字符)
+                if (stockName != null) {
+                    stockName = stockName.trim();
+                    // 确保长度不超过50个字符(数据库限制)
+                    if (stockName.length() > 50) {
+                        log.warn("第{}条数据 - 股票名称过长,截取前50位: [{}]", i + 1, stockName);
+                        stockName = stockName.substring(0, 50);
+                    }
+                    vo.setStockName(stockName);
+                }
+
+                // 检查必填字段 - 股票代码
+                if (StringUtils.isBlank(vo.getStockCode())) {
+                    failureNum++;
+                    String displayName = vo.getStockName() != null ? vo.getStockName() : "未知";
+                    String rowInfo = "第" + (i + 1) + "行";
+                    if (StringUtils.isNotBlank(displayName) && !"未知".equals(displayName)) {
+                        failureMsg.append("<br/>").append(failureNum).append("、").append(rowInfo)
+                            .append(",股票名称: ").append(displayName).append(",代码为空");
+                    } else {
+                        failureMsg.append("<br/>").append(failureNum).append("、").append(rowInfo)
+                            .append(",股票代码为空(可能是Excel列名不匹配,请检查Excel第一行是否包含'代码'列)");
+                    }
+                    continue;
+                }
+                
+                // 验证股票代码格式(正常股票代码应该是6位数字)
+                String code = vo.getStockCode();
+                
+                // 如果代码超过6位,可能是多列数据被拼接了,尝试提取前6位
+                if (code.length() > 6) {
+                    String originalCode = code;
+                    // 尝试提取前6位作为股票代码
+                    code = code.substring(0, 6);
+                    log.info("第{}条数据 - 股票代码过长,尝试提取前6位: [{}] -> [{}]", i + 1, originalCode, code);
+                    vo.setStockCode(code);
+                }
+                
+                // 验证股票代码是否为纯6位数字
+                if (!code.matches("^\\d{6}$")) {
+                    log.warn("第{}条数据 - 股票代码不是6位数字,跳过: [{}]", i + 1, code);
+                    failureNum++;
+                    failureMsg.append("<br/>").append(failureNum).append("、第").append(i + 1)
+                        .append("行,股票代码格式异常: ").append(code)
+                        .append("(股票代码应为6位数字)");
+                    continue;
+                }
+                
+                // 检查必填字段 - 股票名称
+                if (StringUtils.isBlank(vo.getStockName())) {
+                    failureNum++;
+                    failureMsg.append("<br/>").append(failureNum).append("、第").append(i + 1)
+                        .append("行,股票代码: ").append(vo.getStockCode())
+                        .append(",股票名称为空(请检查Excel数据)");
+                    continue;
+                }
+
+                // 使用管理员指定的记录日期,而不是Excel中的日期
+                vo.setRecordDate(parsedRecordDate);
+
+                // 检查是否已存在(根据股票代码和记录日期)
+                LambdaQueryWrapper<StockPoolHistory> lqw = Wrappers.lambdaQuery();
+                lqw.eq(StockPoolHistory::getStockCode, vo.getStockCode())
+                    .eq(StockPoolHistory::getRecordDate, parsedRecordDate);
+                StockPoolHistory existing = baseMapper.selectOne(lqw);
+
+                if (existing != null) {
+                    if (updateSupport) {
+                        // 更新所有字段
+                        existing.setStockName(vo.getStockName());
+                        existing.setChangePercent(vo.getChangePercent());
+                        existing.setClosePrice(vo.getClosePrice());
+                        existing.setTotalAmount(vo.getTotalAmount());
+                        existing.setStrengthScore(vo.getStrengthScore());
+                        existing.setCirculationMarketValue(vo.getCirculationMarketValue());
+                        existing.setMainRisePeriod(vo.getMainRisePeriod());
+                        existing.setRecentRiseHand(vo.getRecentRiseHand());
+                        existing.setRecentLimitUp(vo.getRecentLimitUp());
+                        existing.setDayHighestPrice(vo.getDayHighestPrice());
+                        existing.setDayLowestPrice(vo.getDayLowestPrice());
+                        existing.setDayAvgPrice(vo.getDayAvgPrice());
+                        existing.setDayClosePrice(vo.getDayClosePrice());
+                        baseMapper.updateById(existing);
+                        updateNum++;
+                        successNum++;
+                    } else {
+                        failureNum++;
+                        failureMsg.append("<br/>").append(failureNum).append("、股票 ")
+                            .append(vo.getStockCode()).append(" 在 ")
+                            .append(parsedRecordDate).append(" 的记录已存在");
+                    }
+                } else {
+                    // 新增
+                    StockPoolHistory entity = BeanUtil.toBean(vo, StockPoolHistory.class);
+                    entity.setRecordDate(parsedRecordDate);
+                    entity.setAdminId(LoginHelper.getUserId());
+                    baseMapper.insert(entity);
+                    successNum++;
+                }
+            } catch (Exception e) {
+                failureNum++;
+                String code = vo.getStockCode() != null ? vo.getStockCode() : "未知";
+                String msg = "<br/>" + failureNum + "、股票 " + code + " 导入失败:" + e.getMessage();
+                failureMsg.append(msg);
+                log.error(msg, e);
+            }
+        }
+
+        if (failureNum > 0) {
+            String message = String.format("导入完成!成功 %d 条(其中更新 %d 条),失败 %d 条。详细错误信息请查看服务器日志。", 
+                successNum, updateNum, failureNum);
+            log.error("导入失败详情:{}", failureMsg.toString());
+            return message;
+        } else {
+            successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条(其中更新 " + updateNum + " 条)");
+            return successMsg.toString();
+        }
+    }
+
+    /**
+     * 从 Map 中获取字符串值
+     */
+    private String getStringValue(Map<Integer, String> row, int index) {
+        String value = row.get(index);
+        if (value == null) {
+            return null;
+        }
+        // 去除前后空格
+        value = value.trim();
+        // 去除可能的引号
+        value = value.replace("\"", "").replace("'", "");
+        // 去除公式前缀
+        if (value.startsWith("=")) {
+            value = value.substring(1);
+            value = value.replace("\"", "").replace("'", "");
+        }
+        
+        // 尝试修复编码问题(如果检测到乱码)
+        if (containsGarbledText(value)) {
+            value = tryFixEncoding(value);
+        }
+        
+        return value.isEmpty() ? null : value;
+    }
+    
+    /**
+     * 检测是否包含乱码(简单检测)
+     */
+    private boolean containsGarbledText(String text) {
+        if (text == null || text.isEmpty()) {
+            return false;
+        }
+        // 检测是否包含常见的乱码特征
+        // 乱码通常包含大量的特殊字符或不可打印字符
+        int specialCharCount = 0;
+        for (char c : text.toCharArray()) {
+            // 检测是否是常见的乱码字符范围
+            if (c > 0x7F && c < 0x4E00) {
+                // 在 ASCII 扩展区但不在中文区
+                specialCharCount++;
+            }
+        }
+        // 如果超过30%的字符是特殊字符,可能是乱码
+        return specialCharCount > text.length() * 0.3;
+    }
+    
+    /**
+     * 尝试修复编码问题
+     */
+    private String tryFixEncoding(String text) {
+        if (text == null) {
+            return null;
+        }
+        try {
+            // 尝试将 ISO-8859-1 编码的字符串转换为 GBK
+            byte[] bytes = text.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
+            String gbkText = new String(bytes, java.nio.charset.Charset.forName("GBK"));
+            if (!containsGarbledText(gbkText)) {
+                log.debug("编码修复成功: [{}] -> [{}]", text, gbkText);
+                return gbkText;
+            }
+            
+            // 尝试将 UTF-8 编码的字符串转换为 GBK
+            bytes = text.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+            gbkText = new String(bytes, java.nio.charset.Charset.forName("GBK"));
+            if (!containsGarbledText(gbkText)) {
+                log.debug("编码修复成功(UTF-8->GBK): [{}] -> [{}]", text, gbkText);
+                return gbkText;
+            }
+        } catch (Exception e) {
+            log.warn("编码转换失败: {}", e.getMessage());
+        }
+        return text;
+    }
+    
+    /**
+     * 从 Map 中获取 BigDecimal 值
+     */
+    private BigDecimal getBigDecimalValue(Map<Integer, String> row, int index) {
+        String value = getStringValue(row, index);
+        if (value == null || value.isEmpty()) {
+            return null;
+        }
+        try {
+            // 处理科学计数法
+            return new BigDecimal(value);
+        } catch (NumberFormatException e) {
+            log.warn("无法解析数字: [{}]", value);
+            return null;
+        }
+    }
+
+    /**
+     * 构建查询条件
+     */
+    private LambdaQueryWrapper<StockPoolHistory> buildQueryWrapper(StockPoolHistoryBo bo) {
+        LambdaQueryWrapper<StockPoolHistory> lqw = Wrappers.lambdaQuery();
+        lqw.eq(ObjectUtil.isNotNull(bo.getId()), StockPoolHistory::getId, bo.getId());
+        lqw.like(StringUtils.isNotBlank(bo.getStockCode()), StockPoolHistory::getStockCode, bo.getStockCode());
+        lqw.like(StringUtils.isNotBlank(bo.getStockName()), StockPoolHistory::getStockName, bo.getStockName());
+        lqw.eq(ObjectUtil.isNotNull(bo.getRecordDate()), StockPoolHistory::getRecordDate, bo.getRecordDate());
+        lqw.ge(ObjectUtil.isNotNull(bo.getStartDate()), StockPoolHistory::getRecordDate, bo.getStartDate());
+        lqw.le(ObjectUtil.isNotNull(bo.getEndDate()), StockPoolHistory::getRecordDate, bo.getEndDate());
+        // 先按日期降序,同一天内按ID升序(保持与Excel导入顺序一致)
+        lqw.orderByDesc(StockPoolHistory::getRecordDate);
+        lqw.orderByAsc(StockPoolHistory::getId);
+        return lqw;
+    }
+}