fix: 水温站导入功能
This commit is contained in:
parent
8e5a76e644
commit
e62af4f460
@ -13,8 +13,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/base/wt")
|
@RequestMapping("/base/wt")
|
||||||
@ -62,4 +64,12 @@ public class SdWtBHController {
|
|||||||
Page<SdWtBH> page = service.queryPageList(request);
|
Page<SdWtBH> page = service.queryPageList(request);
|
||||||
new ExcelUtil<>(SdWtBH.class).exportExcel(response, page.getRecords(), "水温站数据", "水温站导出");
|
new ExcelUtil<>(SdWtBH.class).exportExcel(response, page.getRecords(), "水温站数据", "水温站导出");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/import")
|
||||||
|
@Operation(summary = "水温站导入")
|
||||||
|
public ResponseResult importData(@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam(value = "source", required = false) String source) throws Exception {
|
||||||
|
Map<String, Object> result = service.importData(file, source);
|
||||||
|
return ResponseResult.successData(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.yfd.platform.common.DataSourceRequest;
|
import com.yfd.platform.common.DataSourceRequest;
|
||||||
import com.yfd.platform.qgc_base.domain.SdWtBH;
|
import com.yfd.platform.qgc_base.domain.SdWtBH;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -14,4 +15,12 @@ public interface ISdWtBHService extends IService<SdWtBH> {
|
|||||||
boolean update(SdWtBH entity, String source);
|
boolean update(SdWtBH entity, String source);
|
||||||
boolean update(Map<String, Object> engInfoPatch, String source) throws Exception;
|
boolean update(Map<String, Object> engInfoPatch, String source) throws Exception;
|
||||||
boolean delete(List<String> stcds, String source);
|
boolean delete(List<String> stcds, String source);
|
||||||
|
/**
|
||||||
|
* 从 Excel 文件批量导入水温站数据(逐条走 add(),含站码唯一校验与操作日志)
|
||||||
|
*
|
||||||
|
* @param file 上传的 Excel 文件
|
||||||
|
* @param source 数据来源
|
||||||
|
* @return 导入结果汇总:total/success/fail/errors
|
||||||
|
*/
|
||||||
|
Map<String, Object> importData(MultipartFile file, String source) throws Exception;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import com.yfd.platform.utils.*;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -130,4 +131,34 @@ public class SdWtBHServiceImpl extends ServiceImpl<SdWtBHMapper, SdWtBH> impleme
|
|||||||
}
|
}
|
||||||
return count > 0;
|
return count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Map<String, Object> importData(MultipartFile file, String source) throws Exception {
|
||||||
|
List<SdWtBH> list = new ExcelUtil<>(SdWtBH.class).importExcel(file);
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("total", list.size());
|
||||||
|
int batchSize = 500;
|
||||||
|
// 过滤无站码的行(stcd 为主键,不能为空)
|
||||||
|
List<SdWtBH> validList = new ArrayList<>();
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
for (SdWtBH entity : list) {
|
||||||
|
if (StrUtil.isBlank(entity.getStcd())) {
|
||||||
|
errors.add("存在缺少站码的行,已跳过");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validList.add(entity);
|
||||||
|
}
|
||||||
|
int success = 0;
|
||||||
|
for (int i = 0; i < validList.size(); i += batchSize) {
|
||||||
|
List<SdWtBH> sub = validList.subList(i, Math.min(i + batchSize, validList.size()));
|
||||||
|
if (this.saveBatch(sub, batchSize)) {
|
||||||
|
success += sub.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.put("success", success);
|
||||||
|
result.put("fail", list.size() - success);
|
||||||
|
result.put("errors", errors);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.poi.ss.usermodel.BorderStyle;
|
import org.apache.poi.ss.usermodel.BorderStyle;
|
||||||
import org.apache.poi.ss.usermodel.Cell;
|
import org.apache.poi.ss.usermodel.Cell;
|
||||||
import org.apache.poi.ss.usermodel.CellStyle;
|
import org.apache.poi.ss.usermodel.CellStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.CellType;
|
||||||
|
import org.apache.poi.ss.usermodel.DateUtil;
|
||||||
import org.apache.poi.ss.usermodel.FillPatternType;
|
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||||
import org.apache.poi.ss.usermodel.Font;
|
import org.apache.poi.ss.usermodel.Font;
|
||||||
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||||
@ -13,24 +15,33 @@ import org.apache.poi.ss.usermodel.IndexedColors;
|
|||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
import org.apache.poi.ss.usermodel.Sheet;
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
import org.apache.poi.ss.util.CellRangeAddress;
|
import org.apache.poi.ss.util.CellRangeAddress;
|
||||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFColor;
|
import org.apache.poi.xssf.usermodel.XSSFColor;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,6 +116,67 @@ public class ExcelUtil<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从上传的 Excel(xlsx)导入数据:读取首个工作表,按表头({@link Excel#name()})匹配列,
|
||||||
|
* 逐行转换为实体对象。表头列顺序无要求,只按列名匹配。
|
||||||
|
*
|
||||||
|
* @param file 上传的 Excel 文件
|
||||||
|
* @return 解析出的实体列表
|
||||||
|
*/
|
||||||
|
public List<T> importExcel(MultipartFile file) throws Exception {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("导入文件不能为空");
|
||||||
|
}
|
||||||
|
List<T> list = new ArrayList<>();
|
||||||
|
try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) {
|
||||||
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
|
Row headerRow = sheet.getRow(0);
|
||||||
|
if (headerRow == null) {
|
||||||
|
throw new IllegalArgumentException("Excel文件没有表头");
|
||||||
|
}
|
||||||
|
// 表头名 -> 列号
|
||||||
|
Map<String, Integer> headerColMap = new HashMap<>();
|
||||||
|
for (int c = 0; c < headerRow.getLastCellNum(); c++) {
|
||||||
|
String name = getStringCellValue(headerRow.getCell(c));
|
||||||
|
if (name != null && !name.isBlank()) {
|
||||||
|
headerColMap.put(name.trim(), c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 列号 -> 实体字段(仅匹配参与导出的 @Excel 字段)
|
||||||
|
Map<Integer, Field> colFieldMap = new HashMap<>();
|
||||||
|
for (Field field : exportFields) {
|
||||||
|
Integer col = headerColMap.get(field.getAnnotation(Excel.class).name());
|
||||||
|
if (col != null) {
|
||||||
|
colFieldMap.put(col, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int r = 1; r <= sheet.getLastRowNum(); r++) {
|
||||||
|
Row row = sheet.getRow(r);
|
||||||
|
if (isRowEmpty(row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
T entity = clazz.getDeclaredConstructor().newInstance();
|
||||||
|
boolean hasValue = false;
|
||||||
|
for (Map.Entry<Integer, Field> entry : colFieldMap.entrySet()) {
|
||||||
|
Cell cell = row.getCell(entry.getKey());
|
||||||
|
if (cell == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Excel excel = entry.getValue().getAnnotation(Excel.class);
|
||||||
|
Object value = convertFromCell(excel, entry.getValue(), cell);
|
||||||
|
if (value != null) {
|
||||||
|
setFieldValue(entry.getValue(), entity, value);
|
||||||
|
hasValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasValue) {
|
||||||
|
list.add(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 内部实现 ====================
|
// ==================== 内部实现 ====================
|
||||||
|
|
||||||
private void writeSheet(SXSSFWorkbook workbook, String sheetName, List<T> list) {
|
private void writeSheet(SXSSFWorkbook workbook, String sheetName, List<T> list) {
|
||||||
@ -285,4 +357,124 @@ public class ExcelUtil<T> {
|
|||||||
String safe = name == null || name.isBlank() ? "Sheet1" : name.replaceAll("[\\[\\]:*?/\\\\]", "_");
|
String safe = name == null || name.isBlank() ? "Sheet1" : name.replaceAll("[\\[\\]:*?/\\\\]", "_");
|
||||||
return safe.length() > 31 ? safe.substring(0, 31) : safe;
|
return safe.length() > 31 ? safe.substring(0, 31) : safe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取单元格文本值(空白单元格返回 null)
|
||||||
|
*/
|
||||||
|
private String getStringCellValue(Cell cell) {
|
||||||
|
if (cell == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return switch (cell.getCellType()) {
|
||||||
|
case STRING -> {
|
||||||
|
String v = cell.getStringCellValue();
|
||||||
|
yield v == null || v.isBlank() ? null : v.trim();
|
||||||
|
}
|
||||||
|
case NUMERIC -> {
|
||||||
|
if (DateUtil.isCellDateFormatted(cell)) {
|
||||||
|
yield new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(cell.getDateCellValue());
|
||||||
|
}
|
||||||
|
double d = cell.getNumericCellValue();
|
||||||
|
yield d == Math.floor(d) ? String.valueOf((long) d) : String.valueOf(d);
|
||||||
|
}
|
||||||
|
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
|
||||||
|
case FORMULA -> {
|
||||||
|
try {
|
||||||
|
yield cell.getStringCellValue();
|
||||||
|
} catch (Exception e) {
|
||||||
|
yield String.valueOf(cell.getNumericCellValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断整行是否无有效数据
|
||||||
|
*/
|
||||||
|
private boolean isRowEmpty(Row row) {
|
||||||
|
if (row == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < row.getLastCellNum(); i++) {
|
||||||
|
if (getStringCellValue(row.getCell(i)) != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将单元格值转换为目标实体字段类型(String/数值/日期/Boolean)
|
||||||
|
*/
|
||||||
|
private Object convertFromCell(Excel excel, Field field, Cell cell) {
|
||||||
|
if (cell == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Class<?> type = field.getType();
|
||||||
|
if (type == String.class) {
|
||||||
|
return getStringCellValue(cell);
|
||||||
|
}
|
||||||
|
if (type == BigDecimal.class || type == Integer.class || type == Long.class
|
||||||
|
|| type == Double.class || type == Float.class || type == Short.class) {
|
||||||
|
String s = getStringCellValue(cell);
|
||||||
|
if (s == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (type == BigDecimal.class) return new BigDecimal(s);
|
||||||
|
if (type == Integer.class) return Integer.valueOf(s);
|
||||||
|
if (type == Long.class) return Long.valueOf(s);
|
||||||
|
if (type == Double.class) return Double.valueOf(s);
|
||||||
|
if (type == Float.class) return Float.valueOf(s);
|
||||||
|
if (type == Short.class) return Short.valueOf(s);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("Excel 导入数值转换失败: col={}, value={}", excel.name(), s);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == Date.class || type == LocalDate.class || type == LocalDateTime.class) {
|
||||||
|
if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
|
||||||
|
Date date = cell.getDateCellValue();
|
||||||
|
if (type == Date.class) return date;
|
||||||
|
if (type == LocalDate.class) return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||||
|
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||||
|
}
|
||||||
|
String s = getStringCellValue(cell);
|
||||||
|
if (s == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat(excel.dateFormat());
|
||||||
|
sdf.setLenient(false);
|
||||||
|
Date date = sdf.parse(s);
|
||||||
|
if (type == Date.class) return date;
|
||||||
|
if (type == LocalDate.class) return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||||
|
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
log.warn("Excel 导入日期转换失败: col={}, value={}, format={}", excel.name(), s, excel.dateFormat());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == Boolean.class) {
|
||||||
|
if (cell.getCellType() == CellType.BOOLEAN) {
|
||||||
|
return cell.getBooleanCellValue();
|
||||||
|
}
|
||||||
|
String s = getStringCellValue(cell);
|
||||||
|
if (s == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return "1".equals(s) || "true".equalsIgnoreCase(s) || "是".equals(s);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFieldValue(Field field, Object entity, Object value) {
|
||||||
|
try {
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(entity, value);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
log.warn("Excel 导入设置字段值失败: field={}, err={}", field.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user