diff --git a/backend/src/main/java/com/yfd/platform/config/MyMetaObjectHandler.java b/backend/src/main/java/com/yfd/platform/config/MyMetaObjectHandler.java index 45ac60c..3e8a46e 100644 --- a/backend/src/main/java/com/yfd/platform/config/MyMetaObjectHandler.java +++ b/backend/src/main/java/com/yfd/platform/config/MyMetaObjectHandler.java @@ -1,6 +1,7 @@ package com.yfd.platform.config; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.yfd.platform.utils.SecurityUtils; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; @@ -13,6 +14,7 @@ import java.util.Date; @Component public class MyMetaObjectHandler implements MetaObjectHandler { + /** * 插入时自动填充 */ @@ -25,6 +27,11 @@ public class MyMetaObjectHandler implements MetaObjectHandler { // 自动填充更新时间 this.strictInsertFill(metaObject, "updatedAt", Date.class, now); + // 自动填充更新时间 + this.strictInsertFill(metaObject, "createdBy", String.class, SecurityUtils.getUserId()); + + // 自动填充更新时间 + this.strictInsertFill(metaObject, "updatedBy", String.class, SecurityUtils.getUserId()); } /** @@ -34,5 +41,7 @@ public class MyMetaObjectHandler implements MetaObjectHandler { public void updateFill(MetaObject metaObject) { // 自动填充更新时间 this.strictUpdateFill(metaObject, "updatedAt", Date.class, new Date()); + // 自动填充更新人 + this.strictInsertFill(metaObject, "updatedBy", String.class, SecurityUtils.getUserId()); } } diff --git a/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java b/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java index ea55c4a..4c08de0 100644 --- a/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java +++ b/backend/src/main/java/com/yfd/platform/data/controller/FishDraftDataController.java @@ -5,14 +5,19 @@ import com.yfd.platform.common.DataSourceLoadOptionsBase; import com.yfd.platform.common.DataSourceRequest; import com.yfd.platform.config.ResponseResult; import com.yfd.platform.data.domain.FishDraftData; +import com.yfd.platform.data.domain.FishImportRequest; +import com.yfd.platform.data.domain.FishImportResult; import com.yfd.platform.data.service.IFishDraftDataService; +import com.yfd.platform.data.service.IFishImportService; import com.yfd.platform.utils.KendoUtil; import com.yfd.platform.utils.QgcQueryWrapperUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.Date; import java.util.List; @@ -29,6 +34,9 @@ public class FishDraftDataController { @Resource private IFishDraftDataService fishDraftDataService; + @Resource + private IFishImportService fishImportService; + // @GetMapping("/page") // @Operation(summary = "分页查询过鱼数据") // public ResponseResult queryPageList( @@ -144,8 +152,6 @@ public class FishDraftDataController { @PostMapping("/add") @Operation(summary = "新增过鱼数据") public ResponseResult add(@RequestBody FishDraftData fishDraftData) { - fishDraftData.setCreatedAt(new Date()); - fishDraftData.setUpdatedAt(new Date()); boolean result = fishDraftDataService.save(fishDraftData); return result ? ResponseResult.success("新增成功") : ResponseResult.error("新增失败"); } @@ -153,7 +159,6 @@ public class FishDraftDataController { @PostMapping("/update") @Operation(summary = "修改过鱼数据") public ResponseResult update(@RequestBody FishDraftData fishDraftData) { - fishDraftData.setUpdatedAt(new Date()); boolean result = fishDraftDataService.updateById(fishDraftData); return result ? ResponseResult.success("修改成功") : ResponseResult.error("修改失败"); } @@ -164,4 +169,39 @@ public class FishDraftDataController { boolean result = fishDraftDataService.removeById(id); return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败"); } + + @PostMapping("/importExcel") + @Operation(summary = "导入Excel过鱼数据") + public ResponseResult importExcel(@RequestParam("file") MultipartFile file, + @RequestParam(required = false) String importNo, + @RequestParam(required = false) Long uploadUserId) { + if (file == null || file.isEmpty()) { + return ResponseResult.error("请上传文件"); + } + String fileName = file.getOriginalFilename(); + if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) { + return ResponseResult.error("请上传Excel文件(.xlsx或.xls)"); + } + try { + FishImportRequest request = new FishImportRequest(); + request.setFilePath(file.getOriginalFilename()); + request.setImportNo(importNo); + request.setUploadUserId(uploadUserId); + request.setBizType("FISH"); + FishImportResult result = fishImportService.parseAndMapExcel(request, file.getInputStream()); + return ResponseResult.successData(result); + } catch (IOException e) { + return ResponseResult.error("文件读取失败: " + e.getMessage()); + } + } + + @PostMapping("/importExcelFromPath") + @Operation(summary = "从文件路径导入Excel过鱼数据") + public ResponseResult importExcelFromPath(@RequestBody FishImportRequest request) { + if (request.getFilePath() == null || request.getFilePath().isEmpty()) { + return ResponseResult.error("请提供文件路径"); + } + FishImportResult result = fishImportService.parseAndMapExcelFromPath(request); + return ResponseResult.successData(result); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/domain/FishDraftData.java b/backend/src/main/java/com/yfd/platform/data/domain/FishDraftData.java index 383f3ad..7f56961 100644 --- a/backend/src/main/java/com/yfd/platform/data/domain/FishDraftData.java +++ b/backend/src/main/java/com/yfd/platform/data/domain/FishDraftData.java @@ -31,6 +31,11 @@ public class FishDraftData implements Serializable { */ private String stcd; + /** + * 所属基地编码 + */ + private String baseId; + /** * 填报时间 */ @@ -177,6 +182,7 @@ public class FishDraftData implements Serializable { /** * 创建人 */ + @TableField(fill = FieldFill.INSERT) private String createdBy; /** @@ -188,5 +194,6 @@ public class FishDraftData implements Serializable { /** * 更新人 */ + @TableField(fill = FieldFill.INSERT_UPDATE) private String updatedBy; } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/domain/FishImportRequest.java b/backend/src/main/java/com/yfd/platform/data/domain/FishImportRequest.java new file mode 100644 index 0000000..44c909d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/domain/FishImportRequest.java @@ -0,0 +1,20 @@ +package com.yfd.platform.data.domain; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Map; + +@Data +public class FishImportRequest implements Serializable { + + private String filePath; + + private String importNo; + + private String bizType = "FISH"; + + private Long uploadUserId; + + private Map columnMapping; +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/domain/FishImportResult.java b/backend/src/main/java/com/yfd/platform/data/domain/FishImportResult.java new file mode 100644 index 0000000..ec27ddd --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/domain/FishImportResult.java @@ -0,0 +1,38 @@ +package com.yfd.platform.data.domain; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class FishImportResult { + + private List successRows; + private List failedRows; + private List unrecognizedFields; + private int totalCount; + private int successCount; + private int failedCount; + private String summary; + + public FishImportResult() { + this.successRows = new ArrayList<>(); + this.failedRows = new ArrayList<>(); + this.unrecognizedFields = new ArrayList<>(); + } + + @Data + public static class FishImportRow { + private int rowIndex; + private FishDraftData data; + private List unrecognizedFields; + private List warnings; + + public FishImportRow(int rowIndex) { + this.rowIndex = rowIndex; + this.unrecognizedFields = new ArrayList<>(); + this.warnings = new ArrayList<>(); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/domain/ImportTask.java b/backend/src/main/java/com/yfd/platform/data/domain/ImportTask.java index 1e4e645..fe058ac 100644 --- a/backend/src/main/java/com/yfd/platform/data/domain/ImportTask.java +++ b/backend/src/main/java/com/yfd/platform/data/domain/ImportTask.java @@ -83,7 +83,7 @@ public class ImportTask implements Serializable { /** * 上传人ID */ - private Long uploadUserId; + private String uploadUserId; /** * 上传时间 diff --git a/backend/src/main/java/com/yfd/platform/data/service/IFishImportService.java b/backend/src/main/java/com/yfd/platform/data/service/IFishImportService.java new file mode 100644 index 0000000..f85b28d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/service/IFishImportService.java @@ -0,0 +1,31 @@ +package com.yfd.platform.data.service; + +import com.yfd.platform.data.domain.FishImportRequest; +import com.yfd.platform.data.domain.FishImportResult; + +import java.io.InputStream; + +/** + *

+ * 过鱼数据Excel导入服务接口 + *

+ */ +public interface IFishImportService { + + /** + * 解析过鱼数据Excel文件并映射字段 + * + * @param request 导入请求 + * @param inputStream Excel文件输入流 + * @return 解析结果 + */ + FishImportResult parseAndMapExcel(FishImportRequest request, InputStream inputStream); + + /** + * 解析过鱼数据Excel文件(从文件路径) + * + * @param request 导入请求 + * @return 解析结果 + */ + FishImportResult parseAndMapExcelFromPath(FishImportRequest request); +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/impl/FishImportServiceImpl.java b/backend/src/main/java/com/yfd/platform/data/service/impl/FishImportServiceImpl.java new file mode 100644 index 0000000..1030ec7 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/service/impl/FishImportServiceImpl.java @@ -0,0 +1,403 @@ +package com.yfd.platform.data.service.impl; + +import com.yfd.platform.data.domain.FishDraftData; +import com.yfd.platform.data.domain.FishImportRequest; +import com.yfd.platform.data.domain.FishImportResult; +import com.yfd.platform.data.service.IFishImportService; +import com.yfd.platform.env.domain.SdEngInfoBH; +import com.yfd.platform.env.domain.SdHydrobase; +import com.yfd.platform.env.mapper.SdEngInfoBHMapper; +import com.yfd.platform.env.mapper.SdHydrobaseMapper; +import jakarta.annotation.Resource; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class FishImportServiceImpl implements IFishImportService { + + @Resource + private SdEngInfoBHMapper engInfoBHMapper; + + @Resource + private SdHydrobaseMapper hydrobaseMapper; + + private static final Map EXCEL_COLUMN_MAPPING = new LinkedHashMap<>(); + + private static final Map STATION_NAME_CACHE = new HashMap<>(); + private static final Map STATION_CODE_CACHE = new HashMap<>(); + private static final Map BASE_NAME_CACHE = new HashMap<>(); + private static final Map BASE_CODE_CACHE = new HashMap<>(); + + static { + EXCEL_COLUMN_MAPPING.put("电站名称", "stcd"); + EXCEL_COLUMN_MAPPING.put("基地名称", "baseId"); + EXCEL_COLUMN_MAPPING.put("数据时间", "tm"); + EXCEL_COLUMN_MAPPING.put("鱼类", "ftp"); + EXCEL_COLUMN_MAPPING.put("鱼类全长", "fsz"); + EXCEL_COLUMN_MAPPING.put("过鱼数量", "fcnt"); + EXCEL_COLUMN_MAPPING.put("平均体重", "fwet"); + EXCEL_COLUMN_MAPPING.put("开始日期", "strdt"); + EXCEL_COLUMN_MAPPING.put("结束日期", "enddt"); + EXCEL_COLUMN_MAPPING.put("游向", "direction"); + EXCEL_COLUMN_MAPPING.put("年份", "yr"); + EXCEL_COLUMN_MAPPING.put("月份", "mouth"); + EXCEL_COLUMN_MAPPING.put("过鱼视频文件路径", "vdpth"); + EXCEL_COLUMN_MAPPING.put("图片文件路径", "picpth"); + EXCEL_COLUMN_MAPPING.put("是否鱼苗", "isfs"); + EXCEL_COLUMN_MAPPING.put("数据来源", "sourceType"); + } + + @Override + public FishImportResult parseAndMapExcel(FishImportRequest request, InputStream inputStream) { + FishImportResult result = new FishImportResult(); + + try (Workbook workbook = new XSSFWorkbook(inputStream)) { + Sheet sheet = workbook.getSheetAt(0); + return parseSheet(sheet, result); + } catch (IOException e) { + result.setSummary("Excel文件解析失败: " + e.getMessage()); + return result; + } + } + + @Override + public FishImportResult parseAndMapExcelFromPath(FishImportRequest request) { + FishImportResult result = new FishImportResult(); + + try (InputStream inputStream = request.getFilePath().startsWith("http") + ? new java.net.URL(request.getFilePath()).openStream() + : new java.io.FileInputStream(request.getFilePath())) { + return parseAndMapExcel(request, inputStream); + } catch (IOException e) { + result.setSummary("读取文件失败: " + e.getMessage()); + return result; + } + } + + private FishImportResult parseSheet(Sheet sheet, FishImportResult result) { + loadStationAndBaseCache(); + + Row headerRow = sheet.getRow(0); + if (headerRow == null) { + result.setSummary("Excel文件没有表头"); + return result; + } + + Map columnIndexMap = new HashMap<>(); + List unrecognizedHeaders = new ArrayList<>(); + + for (int i = 0; i < headerRow.getLastCellNum(); i++) { + Cell cell = headerRow.getCell(i); + if (cell != null) { + String headerName = getCellStringValue(cell).trim(); + String mappedField = EXCEL_COLUMN_MAPPING.get(headerName); + if (mappedField != null) { + columnIndexMap.put(i, mappedField); + } else { + unrecognizedHeaders.add(headerName); + } + } + } + + if (!unrecognizedHeaders.isEmpty()) { + result.setUnrecognizedFields(unrecognizedHeaders); + } + + int totalRows = sheet.getLastRowNum(); + result.setTotalCount(totalRows); + + for (int i = 1; i <= totalRows; i++) { + Row row = sheet.getRow(i); + if (row == null || isRowEmpty(row)) { + continue; + } + + FishImportResult.FishImportRow importRow = parseRow(row, columnIndexMap, i); + if (importRow.getData() != null && importRow.getUnrecognizedFields().isEmpty()) { + result.getSuccessRows().add(importRow); + result.setSuccessCount(result.getSuccessCount() + 1); + } else { + result.getFailedRows().add(importRow); + result.setFailedCount(result.getFailedCount() + 1); + } + } + + result.setSummary(String.format("共解析%d条数据,成功%d条,失败%d条,未识别字段:%s", + result.getTotalCount(), result.getSuccessCount(), result.getFailedCount(), + unrecognizedHeaders.isEmpty() ? "无" : String.join(", ", unrecognizedHeaders))); + + return result; + } + + private FishImportResult.FishImportRow parseRow(Row row, Map columnIndexMap, int rowIndex) { + FishImportResult.FishImportRow importRow = new FishImportResult.FishImportRow(rowIndex); + FishDraftData data = new FishDraftData(); + data.setId(UUID.randomUUID().toString()); + List unrecognizedFields = new ArrayList<>(); + + for (Map.Entry entry : columnIndexMap.entrySet()) { + Integer columnIndex = entry.getKey(); + String fieldName = entry.getValue(); + Cell cell = row.getCell(columnIndex); + String cellValue = getCellStringValue(cell); + + if (!StringUtils.hasText(cellValue)) { + continue; + } + + try { + switch (fieldName) { + case "stcd": + String stcd = resolveStationCode(cellValue.trim()); + if (stcd == null) { + importRow.getWarnings().add("无法识别的电站: " + cellValue); + } + data.setStcd(stcd); + break; + case "baseId": + String baseId = resolveBaseCode(cellValue.trim()); + if (baseId == null) { + importRow.getWarnings().add("无法识别的基地: " + cellValue); + } + data.setBaseId(baseId); + break; + case "tm": + data.setTm(parseDate(cellValue)); + break; + case "ftp": + data.setFtp(cellValue.trim()); + break; + case "fsz": + data.setFsz(cellValue.trim()); + break; + case "fcnt": + data.setFcnt(parseInteger(cellValue)); + break; + case "fwet": + data.setFwet(cellValue.trim()); + break; + case "strdt": + data.setStrdt(parseDate(cellValue)); + break; + case "enddt": + data.setEnddt(parseDate(cellValue)); + break; + case "direction": + data.setDirection(parseDirection(cellValue.trim())); + break; + case "yr": + data.setYr(parseInteger(cellValue)); + break; + case "mouth": + data.setMouth(parseInteger(cellValue)); + break; + case "vdpth": + data.setVdpth(cellValue.trim()); + break; + case "picpth": + data.setPicpth(cellValue.trim()); + break; + case "isfs": + data.setIsfs(parseIsfs(cellValue)); + break; + case "sourceType": + data.setSourceType(parseSourceType(cellValue.trim())); + break; + default: + break; + } + } catch (Exception e) { + importRow.getWarnings().add("字段[" + fieldName + "]解析异常: " + e.getMessage()); + } + } + + importRow.setData(data); + importRow.setUnrecognizedFields(unrecognizedFields); + return importRow; + } + + private void loadStationAndBaseCache() { + if (STATION_NAME_CACHE.isEmpty()) { + List stationList = engInfoBHMapper.selectList(null); + for (SdEngInfoBH station : stationList) { + if (StringUtils.hasText(station.getEnnm())) { + STATION_NAME_CACHE.put(station.getEnnm().trim().toLowerCase(), station.getStcd()); + } + if (StringUtils.hasText(station.getStcd())) { + STATION_CODE_CACHE.put(station.getStcd().trim().toLowerCase(), station.getStcd()); + } + } + } + + if (BASE_NAME_CACHE.isEmpty()) { + List baseList = hydrobaseMapper.selectList(null); + for (SdHydrobase base : baseList) { + if (StringUtils.hasText(base.getBasename())) { + BASE_NAME_CACHE.put(base.getBasename().trim().toLowerCase(), base.getBaseid()); + } + if (StringUtils.hasText(base.getBaseid())) { + BASE_CODE_CACHE.put(base.getBaseid().trim().toLowerCase(), base.getBaseid()); + } + } + } + } + + private String resolveStationCode(String stationName) { + if (stationName == null) { + return null; + } + String lowerName = stationName.toLowerCase().trim(); + if (STATION_NAME_CACHE.containsKey(lowerName)) { + return STATION_NAME_CACHE.get(lowerName); + } + for (Map.Entry entry : STATION_NAME_CACHE.entrySet()) { + if (entry.getKey().contains(lowerName) || lowerName.contains(entry.getKey())) { + return entry.getValue(); + } + } + return null; + } + + private String resolveBaseCode(String baseName) { + if (baseName == null) { + return null; + } + String lowerName = baseName.toLowerCase().trim(); + if (BASE_NAME_CACHE.containsKey(lowerName)) { + return BASE_NAME_CACHE.get(lowerName); + } + for (Map.Entry entry : BASE_NAME_CACHE.entrySet()) { + if (entry.getKey().contains(lowerName) || lowerName.contains(entry.getKey())) { + return entry.getValue(); + } + } + return null; + } + + private String getCellStringValue(Cell cell) { + if (cell == null) { + return ""; + } + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getLocalDateTimeCellValue().toString(); + } + return String.valueOf((long) cell.getNumericCellValue()); + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + try { + return cell.getStringCellValue(); + } catch (Exception e) { + return String.valueOf(cell.getNumericCellValue()); + } + default: + return ""; + } + } + + private boolean isRowEmpty(Row row) { + for (int i = 0; i < row.getLastCellNum(); i++) { + Cell cell = row.getCell(i); + if (cell != null && StringUtils.hasText(getCellStringValue(cell))) { + return false; + } + } + return true; + } + + private Date parseDate(String dateStr) { + if (!StringUtils.hasText(dateStr)) { + return null; + } + String[] patterns = { + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd", + "yyyy/MM/dd", + "yyyy.MM.dd", + "yyyyMMdd" + }; + for (String pattern : patterns) { + try { + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + sdf.setLenient(false); + return sdf.parse(dateStr); + } catch (ParseException ignored) { + } + } + return null; + } + + private Integer parseInteger(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + try { + return (int) Double.parseDouble(value.trim()); + } catch (NumberFormatException ex) { + return null; + } + } + } + + private String parseDirection(String direction) { + if (!StringUtils.hasText(direction)) { + return null; + } + String d = direction.toLowerCase(); + if (d.contains("上行") || d.contains("up")) { + return "上行"; + } else if (d.contains("下行") || d.contains("down")) { + return "下行"; + } else if (d.contains("折返")) { + if (d.contains("上行")) { + return "上行折返"; + } else if (d.contains("下行")) { + return "下行折返"; + } + return "折返"; + } + return direction; + } + + private Integer parseIsfs(String value) { + if (!StringUtils.hasText(value)) { + return 0; + } + String v = value.toLowerCase().trim(); + if (v.equals("1") || v.equals("是") || v.equals("yes") || v.equals("鱼苗")) { + return 1; + } + return 0; + } + + private String parseSourceType(String value) { + if (!StringUtils.hasText(value)) { + return "IMPORT"; + } + String v = value.toLowerCase(); + if (v.contains("手工") || v.contains("manual") || v.contains("录入")) { + return "MANUAL"; + } else if (v.contains("自动") || v.contains("auto")) { + return "AUTO"; + } else if (v.contains("导入") || v.contains("import")) { + return "IMPORT"; + } + return "IMPORT"; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/utils/SecurityUtils.java b/backend/src/main/java/com/yfd/platform/utils/SecurityUtils.java index 9df23cd..4e8f518 100644 --- a/backend/src/main/java/com/yfd/platform/utils/SecurityUtils.java +++ b/backend/src/main/java/com/yfd/platform/utils/SecurityUtils.java @@ -71,6 +71,16 @@ public class SecurityUtils { return new JSONObject(new JSONObject(userDetails).get("user")).get("id", Long.class); } + /** + * 获取系统用户ID + * @return 系统用户ID + */ + public static String getUserId() { + UserDetails userDetails = getCurrentUser(); + return new JSONObject(new JSONObject(userDetails).get("user")).get("id", String.class); + } + + /** * 获取当前用户的数据权限 * @return /