解决导入时报错的问题
This commit is contained in:
@@ -250,9 +250,11 @@ public class LbDailyUserTradeController {
|
||||
@Parameter(description = "Excel文件", required = true)
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "默认报表日期,格式:yyyy-MM-dd;当Excel行里无报表日期时生效")
|
||||
@RequestParam(required = false) String defaultReportDate) {
|
||||
@RequestParam(required = false) String defaultReportDate,
|
||||
@Parameter(description = "租户ID(必填,导入时写入每行记录)", required = true)
|
||||
@RequestParam String tenantId) {
|
||||
try {
|
||||
Map<String, Object> result = lbDailyUserTradeService.importFromExcel(file, defaultReportDate);
|
||||
Map<String, Object> result = lbDailyUserTradeService.importFromExcel(file, defaultReportDate, tenantId);
|
||||
Boolean success = (Boolean) result.get("success");
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
@@ -13,5 +13,5 @@ import java.util.Map;
|
||||
*/
|
||||
public interface ILbDailyUserTradeService extends IService<LbDailyUserTrade> {
|
||||
|
||||
Map<String, Object> importFromExcel(MultipartFile file, String defaultReportDate);
|
||||
Map<String, Object> importFromExcel(MultipartFile file, String defaultReportDate, String tenantId);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.rj.entity.LbDailyUserTrade;
|
||||
import com.rj.mapper.LbDailyUserTradeMapper;
|
||||
import com.rj.service.ILbDailyUserTradeService;
|
||||
import org.apache.poi.poifs.filesystem.FileMagic;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellType;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
@@ -12,12 +13,19 @@ import org.apache.poi.ss.usermodel.DateUtil;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -35,19 +43,26 @@ import java.util.UUID;
|
||||
*/
|
||||
@Service
|
||||
public class LbDailyUserTradeServiceImpl extends ServiceImpl<LbDailyUserTradeMapper, LbDailyUserTrade> implements ILbDailyUserTradeService {
|
||||
private static final Logger log = LoggerFactory.getLogger(LbDailyUserTradeServiceImpl.class);
|
||||
|
||||
@Override
|
||||
public Map<String, Object> importFromExcel(MultipartFile file, String defaultReportDate) {
|
||||
public Map<String, Object> importFromExcel(MultipartFile file, String defaultReportDate, String tenantId) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
if (file == null || file.isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "上传文件不能为空");
|
||||
return result;
|
||||
}
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".xlsx")) {
|
||||
if (StringUtils.isBlank(tenantId)) {
|
||||
result.put("success", false);
|
||||
result.put("message", "仅支持 .xlsx 文件");
|
||||
result.put("message", "tenantId不能为空");
|
||||
return result;
|
||||
}
|
||||
String normalizedTenantId = tenantId.trim();
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null || !isSupportedExcelSuffix(originalFilename)) {
|
||||
result.put("success", false);
|
||||
result.put("message", "仅支持 .xlsx 或 .xls 文件");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -65,7 +80,9 @@ public class LbDailyUserTradeServiceImpl extends ServiceImpl<LbDailyUserTradeMap
|
||||
List<LbDailyUserTrade> importList = new ArrayList<>();
|
||||
List<String> skippedRows = new ArrayList<>();
|
||||
DataFormatter dataFormatter = new DataFormatter();
|
||||
try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {
|
||||
log.info("开始导入Excel,fileName={}, size={} bytes, defaultReportDate={}",
|
||||
originalFilename, file.getSize(), defaultReportDate);
|
||||
try (Workbook workbook = openWorkbook(file)) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
if (sheet == null) {
|
||||
result.put("success", false);
|
||||
@@ -76,7 +93,7 @@ public class LbDailyUserTradeServiceImpl extends ServiceImpl<LbDailyUserTradeMap
|
||||
int headerRowNum = findHeaderRow(sheet, dataFormatter);
|
||||
if (headerRowNum < 0) {
|
||||
result.put("success", false);
|
||||
result.put("message", "未识别到表头,请确认模板列包含:租户ID、用户ID、单日卖出金额、当日买入金额等字段");
|
||||
result.put("message", "未识别到表头,请确认模板至少包含:用户ID、单日卖出金额、当日买入金额等字段");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -87,20 +104,38 @@ public class LbDailyUserTradeServiceImpl extends ServiceImpl<LbDailyUserTradeMap
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
LbDailyUserTrade trade = parseTradeRow(row, headerIndexMap, dataFormatter, fallbackReportDate);
|
||||
LbDailyUserTrade trade = parseTradeRow(row, headerIndexMap, dataFormatter, fallbackReportDate, normalizedTenantId);
|
||||
if (trade == null) {
|
||||
skippedRows.add(String.valueOf(i + 1));
|
||||
continue;
|
||||
}
|
||||
importList.add(trade);
|
||||
} catch (Exception ex) {
|
||||
log.warn("解析Excel行失败,rowNum={}, message={}", i + 1, ex.getMessage(), ex);
|
||||
skippedRows.add((i + 1) + "(" + ex.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
log.info("Excel解析完成,fileName={}, parsedCount={}, skippedCount={}",
|
||||
originalFilename, importList.size(), skippedRows.size());
|
||||
if (importList.isEmpty()) {
|
||||
log.warn("Excel解析后无有效数据,fileName={}, tenantId={}, skippedRows={}",
|
||||
originalFilename, normalizedTenantId, skippedRows);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("读取Excel失败,fileName={}, message={}", originalFilename, e.getMessage(), e);
|
||||
result.put("success", false);
|
||||
result.put("message", "读取Excel失败:" + e.getMessage());
|
||||
return result;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Excel格式校验失败,fileName={}, message={}", originalFilename, e.getMessage(), e);
|
||||
result.put("success", false);
|
||||
result.put("message", e.getMessage());
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("解析Excel异常,fileName={}, message={}", originalFilename, e.getMessage(), e);
|
||||
result.put("success", false);
|
||||
result.put("message", "解析Excel异常:" + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
|
||||
if (importList.isEmpty()) {
|
||||
@@ -112,11 +147,14 @@ public class LbDailyUserTradeServiceImpl extends ServiceImpl<LbDailyUserTradeMap
|
||||
|
||||
boolean saved = this.saveBatch(importList);
|
||||
if (!saved) {
|
||||
log.error("导入数据保存失败,fileName={}, importCount={}", originalFilename, importList.size());
|
||||
result.put("success", false);
|
||||
result.put("message", "导入失败,数据库保存异常");
|
||||
return result;
|
||||
}
|
||||
|
||||
log.info("导入成功,fileName={}, importCount={}, skipCount={}",
|
||||
originalFilename, importList.size(), skippedRows.size());
|
||||
result.put("success", true);
|
||||
result.put("message", "导入成功");
|
||||
result.put("importCount", importList.size());
|
||||
@@ -133,7 +171,7 @@ public class LbDailyUserTradeServiceImpl extends ServiceImpl<LbDailyUserTradeMap
|
||||
continue;
|
||||
}
|
||||
Map<String, Integer> headerMap = buildHeaderIndexMap(row, formatter);
|
||||
if (headerMap.containsKey("tenantId") && headerMap.containsKey("userId")) {
|
||||
if (headerMap.containsKey("userId")) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -179,10 +217,21 @@ public class LbDailyUserTradeServiceImpl extends ServiceImpl<LbDailyUserTradeMap
|
||||
return null;
|
||||
}
|
||||
|
||||
private LbDailyUserTrade parseTradeRow(Row row, Map<String, Integer> headerMap, DataFormatter formatter, LocalDate fallbackReportDate) {
|
||||
String tenantId = readString(row, headerMap.get("tenantId"), formatter);
|
||||
private LbDailyUserTrade parseTradeRow(Row row,
|
||||
Map<String, Integer> headerMap,
|
||||
DataFormatter formatter,
|
||||
LocalDate fallbackReportDate,
|
||||
String defaultTenantId) {
|
||||
String tenantId = defaultTenantId == null ? "" : defaultTenantId;
|
||||
String userId = readString(row, headerMap.get("userId"), formatter);
|
||||
if (tenantId.isEmpty() || userId.isEmpty()) {
|
||||
String nickname = readString(row, headerMap.get("nickname"), formatter);
|
||||
if (userId.isEmpty()) {
|
||||
userId = nickname;
|
||||
}
|
||||
if (userId.isEmpty()) {
|
||||
userId = "AUTO_" + UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
if (tenantId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -190,7 +239,7 @@ public class LbDailyUserTradeServiceImpl extends ServiceImpl<LbDailyUserTradeMap
|
||||
trade.setId(UUID.randomUUID().toString());
|
||||
trade.setTenantId(tenantId);
|
||||
trade.setUserId(userId);
|
||||
trade.setNickname(readString(row, headerMap.get("nickname"), formatter));
|
||||
trade.setNickname(nickname);
|
||||
trade.setDailySellAmt(readDecimal(row, headerMap.get("dailySellAmt"), formatter));
|
||||
trade.setDailyBuyAmt(readDecimal(row, headerMap.get("dailyBuyAmt"), formatter));
|
||||
trade.setPromoterId(readString(row, headerMap.get("promoterId"), formatter));
|
||||
@@ -277,4 +326,166 @@ public class LbDailyUserTradeServiceImpl extends ServiceImpl<LbDailyUserTradeMap
|
||||
private String normalizeHeader(String header) {
|
||||
return header == null ? "" : header.toLowerCase().replace(" ", "").replace(" ", "").trim();
|
||||
}
|
||||
|
||||
private boolean isSupportedExcelSuffix(String fileName) {
|
||||
String lowerCaseName = fileName.toLowerCase();
|
||||
return lowerCaseName.endsWith(".xlsx") || lowerCaseName.endsWith(".xls");
|
||||
}
|
||||
|
||||
private Workbook openWorkbook(MultipartFile file) throws IOException {
|
||||
byte[] fileBytes = file.getBytes();
|
||||
FileMagic fileMagic = detectFileMagic(fileBytes);
|
||||
try {
|
||||
return WorkbookFactory.create(new ByteArrayInputStream(fileBytes));
|
||||
} catch (Exception e) {
|
||||
log.warn("标准Excel解析失败,尝试文本格式兜底,fileName={}, fileMagic={}, contentType={}, headHex={}",
|
||||
file.getOriginalFilename(), fileMagic, file.getContentType(), toHexPreview(fileBytes, 16));
|
||||
Workbook textWorkbook = tryCreateWorkbookFromText(fileBytes, file.getOriginalFilename());
|
||||
if (textWorkbook != null) {
|
||||
log.info("文本格式兜底解析成功,fileName={}", file.getOriginalFilename());
|
||||
return textWorkbook;
|
||||
}
|
||||
log.error("文本格式兜底也失败,fileName={}, fileMagic={}, contentType={}",
|
||||
file.getOriginalFilename(), fileMagic, file.getContentType(), e);
|
||||
throw new IllegalArgumentException(
|
||||
"上传文件内容不是有效的Excel文件,请确认文件未损坏,且不要仅修改后缀名。检测到文件签名:" + fileMagic, e);
|
||||
}
|
||||
}
|
||||
|
||||
private FileMagic detectFileMagic(byte[] fileBytes) throws IOException {
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(fileBytes)) {
|
||||
return FileMagic.valueOf(FileMagic.prepareToCheckMagic(inputStream));
|
||||
}
|
||||
}
|
||||
|
||||
private Workbook tryCreateWorkbookFromText(byte[] fileBytes, String fileName) {
|
||||
for (Charset charset : List.of(StandardCharsets.UTF_8, Charset.forName("GBK"))) {
|
||||
try {
|
||||
Workbook workbook = createWorkbookFromDelimitedText(fileBytes, charset);
|
||||
if (workbook != null) {
|
||||
log.info("文本文件按分隔格式解析成功,fileName={}, charset={}", fileName, charset.name());
|
||||
return workbook;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.debug("文本格式兜底解析失败,fileName={}, charset={}, message={}",
|
||||
fileName, charset.name(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Workbook createWorkbookFromDelimitedText(byte[] fileBytes, Charset charset) throws IOException {
|
||||
List<String> lines = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(fileBytes), charset))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (!line.trim().isEmpty()) {
|
||||
lines.add(removeUtf8Bom(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lines.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
char delimiter = detectDelimiter(lines.get(0));
|
||||
if (delimiter == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<List<String>> parsedRows = new ArrayList<>();
|
||||
int maxColumns = 0;
|
||||
for (String line : lines) {
|
||||
List<String> cells = parseDelimitedLine(line, delimiter);
|
||||
if (cells.size() > 1) {
|
||||
parsedRows.add(cells);
|
||||
maxColumns = Math.max(maxColumns, cells.size());
|
||||
}
|
||||
}
|
||||
if (parsedRows.isEmpty() || maxColumns < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Workbook workbook = WorkbookFactory.create(true);
|
||||
Sheet sheet = workbook.createSheet("Sheet1");
|
||||
for (int rowIndex = 0; rowIndex < parsedRows.size(); rowIndex++) {
|
||||
Row row = sheet.createRow(rowIndex);
|
||||
List<String> cells = parsedRows.get(rowIndex);
|
||||
for (int colIndex = 0; colIndex < cells.size(); colIndex++) {
|
||||
row.createCell(colIndex).setCellValue(cells.get(colIndex));
|
||||
}
|
||||
}
|
||||
return workbook;
|
||||
}
|
||||
|
||||
private char detectDelimiter(String line) {
|
||||
int tabCount = countChar(line, '\t');
|
||||
int commaCount = countChar(line, ',');
|
||||
int semicolonCount = countChar(line, ';');
|
||||
if (tabCount > 0 && tabCount >= commaCount && tabCount >= semicolonCount) {
|
||||
return '\t';
|
||||
}
|
||||
if (commaCount > 0 && commaCount >= semicolonCount) {
|
||||
return ',';
|
||||
}
|
||||
if (semicolonCount > 0) {
|
||||
return ';';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int countChar(String text, char ch) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
if (text.charAt(i) == ch) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private List<String> parseDelimitedLine(String line, char delimiter) {
|
||||
List<String> cells = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
boolean inQuotes = false;
|
||||
for (int i = 0; i < line.length(); i++) {
|
||||
char c = line.charAt(i);
|
||||
if (c == '"') {
|
||||
if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') {
|
||||
current.append('"');
|
||||
i++;
|
||||
} else {
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == delimiter && !inQuotes) {
|
||||
cells.add(current.toString().trim());
|
||||
current.setLength(0);
|
||||
continue;
|
||||
}
|
||||
current.append(c);
|
||||
}
|
||||
cells.add(current.toString().trim());
|
||||
return cells;
|
||||
}
|
||||
|
||||
private String removeUtf8Bom(String line) {
|
||||
if (!line.isEmpty() && line.charAt(0) == '\uFEFF') {
|
||||
return line.substring(1);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private String toHexPreview(byte[] bytes, int len) {
|
||||
int size = Math.min(bytes.length, len);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < size; i++) {
|
||||
sb.append(String.format("%02X", bytes[i]));
|
||||
if (i < size - 1) {
|
||||
sb.append(" ");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user