diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdWtBHController.java b/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdWtBHController.java index f0eed18b..790bf783 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdWtBHController.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdWtBHController.java @@ -13,8 +13,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.Map; @RestController @RequestMapping("/base/wt") @@ -62,4 +64,12 @@ public class SdWtBHController { Page page = service.queryPageList(request); 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 result = service.importData(file, source); + return ResponseResult.successData(result); + } } diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdWtBHService.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdWtBHService.java index d09ce970..4d014feb 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdWtBHService.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdWtBHService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import com.yfd.platform.common.DataSourceRequest; import com.yfd.platform.qgc_base.domain.SdWtBH; +import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.Map; @@ -14,4 +15,12 @@ public interface ISdWtBHService extends IService { boolean update(SdWtBH entity, String source); boolean update(Map engInfoPatch, String source) throws Exception; boolean delete(List stcds, String source); + /** + * 从 Excel 文件批量导入水温站数据(逐条走 add(),含站码唯一校验与操作日志) + * + * @param file 上传的 Excel 文件 + * @param source 数据来源 + * @return 导入结果汇总:total/success/fail/errors + */ + Map importData(MultipartFile file, String source) throws Exception; } diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdWtBHServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdWtBHServiceImpl.java index c29ff7e6..5a5d8673 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdWtBHServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdWtBHServiceImpl.java @@ -15,6 +15,7 @@ import com.yfd.platform.utils.*; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import java.util.*; @@ -130,4 +131,34 @@ public class SdWtBHServiceImpl extends ServiceImpl impleme } return count > 0; } + + @Override + @Transactional(rollbackFor = Exception.class) + public Map importData(MultipartFile file, String source) throws Exception { + List list = new ExcelUtil<>(SdWtBH.class).importExcel(file); + Map result = new HashMap<>(); + result.put("total", list.size()); + int batchSize = 500; + // 过滤无站码的行(stcd 为主键,不能为空) + List validList = new ArrayList<>(); + List 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 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; + } } diff --git a/backend/src/main/java/com/yfd/platform/utils/ExcelUtil.java b/backend/src/main/java/com/yfd/platform/utils/ExcelUtil.java index e2b10b19..c8b124d1 100644 --- a/backend/src/main/java/com/yfd/platform/utils/ExcelUtil.java +++ b/backend/src/main/java/com/yfd/platform/utils/ExcelUtil.java @@ -6,6 +6,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.BorderStyle; import org.apache.poi.ss.usermodel.Cell; 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.Font; 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.Sheet; 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.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFCellStyle; import org.apache.poi.xssf.usermodel.XSSFColor; +import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Field; +import java.math.BigDecimal; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -105,6 +116,67 @@ public class ExcelUtil { } } + /** + * 从上传的 Excel(xlsx)导入数据:读取首个工作表,按表头({@link Excel#name()})匹配列, + * 逐行转换为实体对象。表头列顺序无要求,只按列名匹配。 + * + * @param file 上传的 Excel 文件 + * @return 解析出的实体列表 + */ + public List importExcel(MultipartFile file) throws Exception { + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("导入文件不能为空"); + } + List 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 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 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 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 list) { @@ -285,4 +357,124 @@ public class ExcelUtil { String safe = name == null || name.isBlank() ? "Sheet1" : name.replaceAll("[\\[\\]:*?/\\\\]", "_"); 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()); + } + } }