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