diff --git a/backend/src/main/java/com/yfd/platform/config/JwtAuthenticationTokenFilter.java b/backend/src/main/java/com/yfd/platform/config/JwtAuthenticationTokenFilter.java index bcb79c0..2f3c035 100644 --- a/backend/src/main/java/com/yfd/platform/config/JwtAuthenticationTokenFilter.java +++ b/backend/src/main/java/com/yfd/platform/config/JwtAuthenticationTokenFilter.java @@ -11,6 +11,7 @@ import com.yfd.platform.constant.Constant; import com.yfd.platform.system.domain.LoginUser; import com.yfd.platform.system.domain.Message; import com.yfd.platform.system.service.IMessageService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @@ -25,6 +26,7 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @Component +@Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired @@ -36,6 +38,9 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { FilterChain filterChain) throws ServletException, IOException { //获取token String uri = httpServletRequest.getRequestURI(); + if(uri.contains("/data/fishDraft/importZip")){ + log.info("请求地址:{}", uri); + } String token = httpServletRequest.getHeader("token"); if (StrUtil.isEmpty(token) || "/user/login".equals(uri)) { filterChain.doFilter(httpServletRequest, httpServletResponse); diff --git a/backend/src/main/java/com/yfd/platform/config/ProdApiPrefixFilter.java b/backend/src/main/java/com/yfd/platform/config/ProdApiPrefixFilter.java deleted file mode 100644 index 4c1680a..0000000 --- a/backend/src/main/java/com/yfd/platform/config/ProdApiPrefixFilter.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.yfd.platform.config; - -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.RequestDispatcher; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.annotation.WebFilter; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * 将以 /prod-api/ 开头的请求转发到去掉前缀的真实后端接口路径。 - * 例如:/prod-api/user/code -> /user/code - * 这样可以兼容前端生产环境仍使用 /prod-api 作为网关前缀的情况。 - */ -@WebFilter(urlPatterns = "/prod-api/*", filterName = "prodApiPrefixFilter") -public class ProdApiPrefixFilter implements Filter { - - private static final String PREFIX = "/prod-api"; - - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - if (!(req instanceof HttpServletRequest) || !(res instanceof HttpServletResponse)) { - chain.doFilter(req, res); - return; - } - - HttpServletRequest request = (HttpServletRequest) req; - String uri = request.getRequestURI(); - - // 仅拦截 /prod-api/* 的接口请求并进行内部 forward - if (uri.startsWith(PREFIX + "/")) { - String forwardUri = uri.substring(PREFIX.length()); - RequestDispatcher dispatcher = request.getRequestDispatcher(forwardUri); - dispatcher.forward(req, res); - return; - } - - chain.doFilter(req, res); - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java b/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java index 212db9b..42bd358 100644 --- a/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java +++ b/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java @@ -1,4 +1,5 @@ package com.yfd.platform.config; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springdoc.core.models.GroupedOpenApi; @@ -6,9 +7,6 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Contact; -/** - * Springdoc OpenAPI 配置 - */ @Configuration public class SwaggerConfig { @@ -23,37 +21,51 @@ public class SwaggerConfig { ); } - @Bean - public GroupedOpenApi groupWebsiteApi() { - return GroupedOpenApi.builder() - .group("1. 平台模块") - .packagesToScan("com.yfd.platform.modules.platformdb.controller") - .build(); - } +// @Bean +// public GroupedOpenApi groupPlatformApi() { +// return GroupedOpenApi.builder() +// .group("1. 平台模块") +// .packagesToScan("com.yfd.platform.modules.platformdb.controller") +// .build(); +// } - @Bean - public GroupedOpenApi groupQuartzApi() { - return GroupedOpenApi.builder() - .group("2. 定时任务") - .packagesToScan("com.yfd.platform.modules.quartz.controller") - .build(); - } +// @Bean +// public GroupedOpenApi groupQuartzApi() { +// return GroupedOpenApi.builder() +// .group("2. 定时任务") +// .packagesToScan("com.yfd.platform.modules.quartz.controller") +// .build(); +// } @Bean public GroupedOpenApi groupSystemApi() { return GroupedOpenApi.builder() - .group("3. 系统管理") + .group("1. 系统管理") .packagesToScan("com.yfd.platform.system.controller") .build(); } + @Bean + public GroupedOpenApi groupDataEnvApi() { + return GroupedOpenApi.builder() + .group("2. 过鱼数据模块") + .packagesToScan("com.yfd.platform.data.controller") + .build(); + } + @Bean public GroupedOpenApi groupEnvApi() { return GroupedOpenApi.builder() - .group("4. 环境/基地/流域管理") + .group("3. 全过程-生态环保数据服务") .packagesToScan("com.yfd.platform.env.controller") .build(); } - -} + @Bean + public GroupedOpenApi groupEngApi() { + return GroupedOpenApi.builder() + .group("4. 全过程-常规水电数据服务") + .packagesToScan("com.yfd.platform.eng.controller") + .build(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/controller/ApprovalMainController.java b/backend/src/main/java/com/yfd/platform/data/controller/ApprovalMainController.java index 53d927d..c0db986 100644 --- a/backend/src/main/java/com/yfd/platform/data/controller/ApprovalMainController.java +++ b/backend/src/main/java/com/yfd/platform/data/controller/ApprovalMainController.java @@ -13,6 +13,7 @@ import jakarta.annotation.Resource; import org.springframework.web.bind.annotation.*; import java.util.Date; +import java.util.List; /** *

@@ -116,4 +117,11 @@ public class ApprovalMainController { boolean result = approvalMainService.removeById(id); return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败"); } + + @PostMapping("/batchDelete") + @Operation(summary = "删除审批") + public ResponseResult delete(@RequestBody List ids) { + boolean result = approvalMainService.removeBatchByIds(ids); + return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败"); + } } \ No newline at end of file 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 1e1a0d5..5a84a7c 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 @@ -3,7 +3,9 @@ package com.yfd.platform.data.controller; import java.io.File; import java.io.FileInputStream; import java.io.OutputStream; +import java.net.URLDecoder; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -11,8 +13,12 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.databind.ObjectMapper; @@ -20,12 +26,15 @@ import com.yfd.platform.common.DataSourceRequest; import com.yfd.platform.config.ResponseResult; import com.yfd.platform.data.domain.*; import com.yfd.platform.data.domain.vo.FishDraftDataVO; +import com.yfd.platform.data.domain.vo.FishStatisticsVO; import com.yfd.platform.data.service.AttachmentUploadService; import com.yfd.platform.data.service.IFishDraftDataService; import com.yfd.platform.data.service.IFishImportService; +import com.yfd.platform.data.service.IFishStatisticsService; import com.yfd.platform.data.service.IImportTaskService; import com.yfd.platform.data.utils.ZipFileUtil; import com.yfd.platform.utils.KendoUtil; +import com.yfd.platform.utils.MultipartStreamParser; import com.yfd.platform.utils.SecurityUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -33,12 +42,12 @@ import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.*; @@ -69,6 +78,18 @@ public class FishDraftDataController { @Resource private AttachmentUploadService attachmentUploadService; + @Resource + private IFishStatisticsService fishStatisticsService; + + @Autowired + private ThreadPoolTaskExecutor taskExecutor; + + private final ExecutorService attachmentDeleteExecutor = Executors.newFixedThreadPool(4, r -> { + Thread t = new Thread(r, "attachment-delete"); + t.setDaemon(true); + return t; + }); + @PostMapping("/page") @Operation(summary = "分页查询过鱼数据(关联电站和设施)") public ResponseResult queryPageList(@RequestBody DataSourceRequest dataSourceRequest) { @@ -83,6 +104,13 @@ public class FishDraftDataController { return ResponseResult.successData(list); } + @PostMapping("/statistics") + @Operation(summary = "过鱼到数统计(按用户月度汇总,支持流域/电站多选过滤)") + public ResponseResult statistics(@RequestBody DataSourceRequest dataSourceRequest) { + Page result = fishStatisticsService.queryPage(dataSourceRequest); + return ResponseResult.successData(result); + } + @GetMapping("/getById") @Operation(summary = "根据ID查询") public ResponseResult getById(@RequestParam String id) { @@ -133,23 +161,10 @@ public class FishDraftDataController { @PostMapping("/batchSaveDraft") @Operation(summary = "批量保存草稿") - @Transactional(rollbackFor = Exception.class) public ResponseResult saveDraft(@RequestBody FishImportRowRequest request) { String taskId = request.getTaskId(); ImportTask importTask = importTaskService.getById(taskId); - String resultJson = importTask.getResultJson(); - FishImportResult importResult = null; - Map imageFiles = null; - Map videoFiles = null; - if (resultJson != null && !resultJson.isEmpty()) { - try { - importResult = objectMapper.readValue(resultJson, FishImportResult.class); - imageFiles = importResult.getImageFiles(); - videoFiles = importResult.getVideoFiles(); - } catch (Exception e) { - e.printStackTrace(); - } - } + FishImportResult importResult = importTaskService.buildImportResult(taskId); if (importResult == null || importResult.getRows() == null) { return ResponseResult.error("导入数据不存在"); @@ -164,10 +179,12 @@ public class FishDraftDataController { data.setDeletedFlag(0); data.setLockFlag(0); data.setTm(date); + data.setVdpthList(row.getVdpthList()); + data.setPicpthList(row.getPicpthList()); fishDraftDataList.add(data); } - boolean result = fishDraftDataService.saveBatch(fishDraftDataList); - fishImportService.processAttachmentsAsync(fishDraftDataList, imageFiles, videoFiles,importTask.getTempDir()); + boolean result = fishDraftDataService.batchSaveDraft(fishDraftDataList); + fishImportService.processAttachmentsAsync(fishDraftDataList, importTask.getTempDir()); importTaskService.markSuccess(taskId); return result ? ResponseResult.success("保存成功") : ResponseResult.error("保存失败"); } @@ -180,11 +197,10 @@ public class FishDraftDataController { if (importTask == null) { return ResponseResult.error("导入任务不存在"); } - String resultJson = importTask.getResultJson(); + FishImportResult importResult = importTaskService.buildImportResult(taskId); List fishDraftDataList = batchSaveDraftRequest.getFishDraftDataList(); - if (resultJson != null && !resultJson.isEmpty()) { + if (importResult != null) { try { - FishImportResult importResult = objectMapper.readValue(resultJson, FishImportResult.class); ZipFileUtil.ZipContent content = new ZipFileUtil.ZipContent(); content.images = importResult.getImageFiles(); content.videos = importResult.getVideoFiles(); @@ -203,7 +219,6 @@ public class FishDraftDataController { }); } catch (Exception e) { e.printStackTrace(); - // ignore parse error } } @@ -226,10 +241,90 @@ public class FishDraftDataController { return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败"); } + + +// @PostMapping("/approvalIdRemoveDraft") +// @Operation(summary = "根据批次号批量删除草稿(软删除)") +// public ResponseResult approvalIdRemoveDraft(@RequestBody BatchApproveRequest request) { +// List draft = fishDraftDataService.list(new LambdaQueryWrapper().eq(FishDraftData::getDeletedFlag, 0).in(FishDraftData::getApprovalId, request.getApprovalIds()).eq(FishDraftData::getStatus, "REJECTED").select(FishDraftData::getId)); +// List ids = draft.stream().map(FishDraftData::getId).toList(); +// boolean result = fishDraftDataService.batchRemoveDraft(ids); +// if(result){ +// List list = fishDraftDataService.list(new LambdaQueryWrapper().in(FishDraftData::getId, ids).select(FishDraftData::getPicpth, FishDraftData::getVdpth, FishDraftData::getId)); +// // 异步删除附件 +// CompletableFuture.runAsync(() -> { +// for (FishDraftData fishDraftData : list) { +// String picpth = fishDraftData.getPicpth(); +// String vdpth = fishDraftData.getVdpth(); +// +// try { +// if (StrUtil.isNotBlank(picpth)) { +// // 假设 picpth 是分号或逗号分隔的文件ID/路径 +// List split = StrUtil.split(picpth, StrUtil.C_COMMA); +// for (String fileId : split) { +// if (StrUtil.isNotBlank(fileId)) { +// attachmentUploadService.deleteFile(fileId.trim()); +// } +// } +// } +// if (StrUtil.isNotBlank(vdpth)) { +// List split = StrUtil.split(vdpth, StrUtil.C_COMMA); +// for (String fileId : split) { +// if (StrUtil.isNotBlank(fileId)) { +// attachmentUploadService.deleteFile(fileId.trim()); +// } +// } +// } +// } catch (Exception e) { +// log.error("异步删除附件失败, dataId: {}", fishDraftData.getId(), e); +// } +// } +// }, attachmentDeleteExecutor).exceptionally(ex -> { +// log.error("异步删除任务执行异常", ex); +// return null; +// }); +// } +// return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败"); +// } @PostMapping("/batchRemoveDraft") @Operation(summary = "批量删除草稿(软删除)") public ResponseResult batchRemoveDraft(@RequestBody List ids) { boolean result = fishDraftDataService.batchRemoveDraft(ids); + if(result){ + List list = fishDraftDataService.list(new LambdaQueryWrapper().in(FishDraftData::getId, ids).select(FishDraftData::getPicpth, FishDraftData::getVdpth, FishDraftData::getId)); + // 异步删除附件 + CompletableFuture.runAsync(() -> { + for (FishDraftData fishDraftData : list) { + String picpth = fishDraftData.getPicpth(); + String vdpth = fishDraftData.getVdpth(); + + try { + if (StrUtil.isNotBlank(picpth)) { + // 假设 picpth 是分号或逗号分隔的文件ID/路径 + List split = StrUtil.split(picpth, StrUtil.C_COMMA); + for (String fileId : split) { + if (StrUtil.isNotBlank(fileId)) { + attachmentUploadService.deleteFile(fileId.trim()); + } + } + } + if (StrUtil.isNotBlank(vdpth)) { + List split = StrUtil.split(vdpth, StrUtil.C_COMMA); + for (String fileId : split) { + if (StrUtil.isNotBlank(fileId)) { + attachmentUploadService.deleteFile(fileId.trim()); + } + } + } + } catch (Exception e) { + log.error("异步删除附件失败, dataId: {}", fishDraftData.getId(), e); + } + } + }, attachmentDeleteExecutor).exceptionally(ex -> { + log.error("异步删除任务执行异常", ex); + return null; + }); + } return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败"); } @@ -353,16 +448,11 @@ public class FishDraftDataController { @PostMapping("/importZip") @Operation(summary = "导入ZIP过鱼数据(每个用户同时只能进行一次导入)") - public ResponseResult importZip(@RequestParam("file") MultipartFile file) { + public ResponseResult importZip(HttpServletRequest request) { log.info("开始导入ZIP文件"); - if (file == null || file.isEmpty()) { - return ResponseResult.error("请上传文件"); - } - String fileName = file.getOriginalFilename(); - if (fileName == null || (!fileName.endsWith(".zip"))) { - return ResponseResult.error("请上传ZIP文件(.zip)"); - } + String uploadUserId = SecurityUtils.getUserId(); + if (importTaskService.hasImportingTask(uploadUserId)) { return ResponseResult.error("您有正在进行的导入任务,请等待完成后重试"); } @@ -370,52 +460,72 @@ public class FishDraftDataController { String importNo = "IMP" + System.currentTimeMillis(); String taskId = UUID.randomUUID().toString(); + ImportTask task = null; + Path tempDirPath = null; + try { String baseTempDir = ZipFileUtil.getDefaultTempDir(); String taskDirName = "zip_" + UUID.randomUUID().toString().substring(0, 8); - Path tempDirPath = Paths.get(baseTempDir, taskDirName); - Files.createDirectories(tempDirPath); - File savedZipFile = new File(tempDirPath.toFile(), "upload.zip"); - file.transferTo(savedZipFile); - log.info("ZIP文件已保存到: {}", savedZipFile.getAbsolutePath()); + tempDirPath = Paths.get(baseTempDir, taskDirName); - ImportTask task = new ImportTask(); + task = new ImportTask(); task.setId(taskId); task.setImportNo(importNo); task.setBizType("FISH"); - task.setFileName(fileName); - task.setFileSize(file.getSize()); - task.setStatus("UPLOADED"); + task.setStatus("UPLOADING"); task.setUploadUserId(uploadUserId); task.setUploadTime(new Date()); task.setTempDir(tempDirPath.toString()); importTaskService.save(task); - log.info("导入任务已创建: {}", taskId); + log.info("导入任务已预创建: {}, 状态: UPLOADING", taskId); + + MultipartStreamParser.StreamedFile streamedFile = + MultipartStreamParser.streamToFile(request, tempDirPath); + + String fileName = streamedFile.getOriginalFileName(); + if (fileName == null || !fileName.toLowerCase().endsWith(".zip")) { + importTaskService.markFailed(taskId, "文件格式错误: 请上传ZIP文件"); + return ResponseResult.error("请上传ZIP文件(.zip)"); + } + + File savedZipFile = streamedFile.getTargetFile(); + long fileSize = streamedFile.getFileSize(); + log.info("ZIP文件已保存到: {} ({} bytes)", savedZipFile.getAbsolutePath(), fileSize); + + task.setFileName(fileName); + task.setFileSize(fileSize); + task.setStatus("UPLOADED"); + importTaskService.updateById(task); + log.info("导入任务已更新: {}, 状态: UPLOADED, 文件名: {}", taskId, fileName); + SecurityContext securityContext = SecurityContextHolder.getContext(); + Path finalTempDirPath = tempDirPath; CompletableFuture.runAsync(() -> { try { SecurityContextHolder.setContext(securityContext); log.info("异步开始解析ZIP文件, taskId: {}", taskId); + FishImportResult result = fishImportService.parseAndMapZipFromFile( - savedZipFile, tempDirPath.toString(), uploadUserId); + savedZipFile, finalTempDirPath.toString(), uploadUserId); result.setTaskId(taskId); - String status = "VALIDATED"; - if ("1".equals(result.getCode())) { - status = "FAILED"; - } + + String status = "1".equals(result.getCode()) ? "FAILED" : "VALIDATED"; + importTaskService.updateStatus(taskId, status, result.getTempDir(), null); importTaskService.updateProgress(taskId, result.getTotalCount(), result.getSuccessCount(), result.getFailedCount()); - String resultJson = objectMapper.writeValueAsString(result); - importTaskService.saveResultJson(taskId, resultJson); - log.info("异步解析完成, taskId: {}, 状态: {}", taskId, status); + importTaskService.saveResultJson(taskId, result); + + log.info("异步解析完成, taskId: {}, 状态: {}, 成功: {}, 失败: {}", + taskId, status, result.getSuccessCount(), result.getFailedCount()); } catch (Exception e) { log.error("异步解析ZIP失败, taskId: {}", taskId, e); importTaskService.markFailed(taskId, "导入失败: " + e.getMessage()); } finally { SecurityContextHolder.clearContext(); if (savedZipFile.exists()) { - savedZipFile.delete(); + boolean deleted = savedZipFile.delete(); + log.debug("临时ZIP文件删除: {}, 结果: {}", savedZipFile.getAbsolutePath(), deleted); } } }); @@ -425,9 +535,23 @@ public class FishDraftDataController { response.put("importNo", importNo); response.put("status", "UPLOADED"); return ResponseResult.successData(response); + } catch (Exception e) { - log.error("创建导入任务失败", e); - importTaskService.markFailed(taskId, "导入失败: " + e.getMessage()); + log.error("导入ZIP文件失败, taskId: {}", taskId, e); + + if (task != null) { + importTaskService.markFailed(taskId, "导入失败: " + e.getMessage()); + } + + if (tempDirPath != null) { + try { + cn.hutool.core.io.FileUtil.del(tempDirPath.toFile()); + log.debug("清理临时目录: {}", tempDirPath); + } catch (Exception cleanupEx) { + log.warn("清理临时目录失败: {}", tempDirPath, cleanupEx); + } + } + return ResponseResult.error("导入失败: " + e.getMessage()); } } @@ -445,13 +569,11 @@ public class FishDraftDataController { return ResponseResult.error("任务不存在"); } - String resultJson = task.getResultJson(); - if (resultJson == null || resultJson.isEmpty()) { + FishImportResult importResult = importTaskService.buildImportResult(taskId); + if (importResult == null) { return ResponseResult.error("任务结果为空"); } - FishImportResult importResult = objectMapper.readValue(resultJson, FishImportResult.class); - Map previewData = new HashMap<>(); previewData.put("tempDir", importResult.getTempDir()); previewData.put("excelFileName", importResult.getExcelFileName()); @@ -495,20 +617,14 @@ public class FishDraftDataController { @GetMapping("/previewFile") @Operation(summary = "预览临时文件内容") public void previewFile(@RequestParam String taskId, @RequestParam String filename, @RequestParam String type, HttpServletRequest request, HttpServletResponse response) { + String decodedFilename = URLDecoder.decode(filename, StandardCharsets.UTF_8); + log.debug("原始文件名: {}, 解码后: {}", filename, decodedFilename); ImportTask importTask = importTaskService.getById(taskId); - String resultJson = importTask.getResultJson(); String filePath = null; String dir = "1".equals(type) ? "images" : "videos"; - if (resultJson != null && !resultJson.isEmpty()) { - try { - FishImportResult importResult = objectMapper.readValue(resultJson, FishImportResult.class); - String tempDir = importResult.getTempDir(); - filePath = tempDir + File.separator + dir + File.separator + filename; - } catch (Exception e) { - e.printStackTrace(); - // ignore parse error - } + if (importTask != null && importTask.getTempDir() != null) { + filePath = importTask.getTempDir() + File.separator + dir + File.separator + decodedFilename; } if (filePath == null) { writeErrorResponse(response, "文件路径不能为空"); @@ -568,13 +684,12 @@ public class FishDraftDataController { return ResponseResult.error("任务不存在"); } - String resultJson = importTask.getResultJson(); - if (resultJson == null || resultJson.isEmpty()) { + FishImportResult importResult = importTaskService.buildImportResult(taskId); + if (importResult == null) { return ResponseResult.error("任务结果为空"); } try { - FishImportResult importResult = objectMapper.readValue(resultJson, FishImportResult.class); boolean found = false; for (FishImportResult.FishImportRow row : importResult.getRows()) { @@ -632,8 +747,7 @@ public class FishDraftDataController { return ResponseResult.error("未找到对应的数据行"); } - String updatedJson = objectMapper.writeValueAsString(importResult); - importTaskService.saveResultJson(taskId, updatedJson); + importTaskService.saveResultJson(taskId, importResult); return ResponseResult.success("删除成功"); @@ -723,7 +837,6 @@ public class FishDraftDataController { result.put("currentTask", null); return ResponseResult.successData(result); } - String statusText = getStatusText(currentTask.getStatus()); boolean canImport = isTaskComplete(currentTask.getStatus()); @@ -752,7 +865,8 @@ public class FishDraftDataController { } private boolean isTaskComplete(String status) { - return "CONFIRMED".equals(status) || "FAILED".equals(status) || "CANCELLED".equals(status); + return "CONFIRMED".equals(status) || "CANCELLED".equals(status); +// return "CONFIRMED".equals(status) || "FAILED".equals(status) || "CANCELLED".equals(status); } @GetMapping("/getLastImportResult") @@ -768,15 +882,7 @@ public class FishDraftDataController { )); } - FishImportResult importResult = null; - if (lastTask.getResultJson() != null && !lastTask.getResultJson().isEmpty()) { - try { - importResult = objectMapper.readValue(lastTask.getResultJson(), FishImportResult.class); - } catch (Exception e) { - e.printStackTrace(); - // ignore parse error - } - } + FishImportResult importResult = importTaskService.buildImportResult(lastTask.getId()); int totalCount = lastTask.getTotalCount() != null ? lastTask.getTotalCount() : 0; int successCount = lastTask.getSuccessCount() != null ? lastTask.getSuccessCount() : 0; @@ -871,13 +977,11 @@ public class FishDraftDataController { return ResponseResult.error("任务不存在"); } - String resultJson = task.getResultJson(); - if (resultJson == null || resultJson.isEmpty()) { + FishImportResult importResult = importTaskService.buildImportResult(taskId); + if (importResult == null) { return ResponseResult.error("任务结果为空"); } - FishImportResult importResult = objectMapper.readValue(resultJson, FishImportResult.class); - FishImportResult.FishImportRow targetRow = null; int targetIndex = -1; for (int i = 0; i < importResult.getRows().size(); i++) { @@ -925,8 +1029,8 @@ public class FishDraftDataController { importResult.setSuccessCount(successCount); importResult.setFailedCount(failedCount); - String updatedJson = objectMapper.writeValueAsString(importResult); - importTaskService.saveResultJson(taskId, updatedJson); +// String updatedJson = objectMapper.writeValueAsString(importResult); + importTaskService.saveResultJson(taskId, importResult); Map map = new HashMap<>(); map.put("success", true); @@ -961,13 +1065,11 @@ public class FishDraftDataController { return ResponseResult.error("任务不存在"); } - String resultJson = task.getResultJson(); - if (resultJson == null || resultJson.isEmpty()) { + FishImportResult importResult = importTaskService.buildImportResult(taskId); + if (importResult == null) { return ResponseResult.error("任务结果为空"); } - FishImportResult importResult = objectMapper.readValue(resultJson, FishImportResult.class); - FishImportResult.FishImportRow targetRow = null; int targetIndex = -1; for (int i = 0; i < importResult.getRows().size(); i++) { @@ -983,8 +1085,8 @@ public class FishDraftDataController { return ResponseResult.error("未找到对应的数据行"); } importResult.getRows().remove(targetIndex); - String updatedJson = objectMapper.writeValueAsString(importResult); - importTaskService.saveResultJson(taskId, updatedJson); +// String updatedJson = objectMapper.writeValueAsString(importResult); + importTaskService.saveResultJson(taskId, importResult); return ResponseResult.success(); } catch (Exception e) { @@ -1122,13 +1224,11 @@ public class FishDraftDataController { return ResponseResult.error("任务不存在"); } - String resultJson = task.getResultJson(); - if (resultJson == null || resultJson.isEmpty()) { + FishImportResult importResult = importTaskService.buildImportResult(taskId); + if (importResult == null) { return ResponseResult.error("任务结果为空"); } - FishImportResult importResult = objectMapper.readValue(resultJson, FishImportResult.class); - FishImportResult.FishImportRow matchedRow = findMatchingRow(importResult, data.getId()); FishImportResult.FishImportRow resultRow = new FishImportResult.FishImportRow(); @@ -1214,4 +1314,17 @@ public class FishDraftDataController { return errors; } + + @jakarta.annotation.PreDestroy + public void shutdown() { + attachmentDeleteExecutor.shutdown(); + try { + if (!attachmentDeleteExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + attachmentDeleteExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + attachmentDeleteExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } } \ 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 4273248..da33082 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 @@ -8,6 +8,8 @@ import lombok.EqualsAndHashCode; import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; +import java.util.List; +import java.util.Map; /** *

@@ -65,11 +67,23 @@ public class FishDraftData implements Serializable { */ private Date strdt; + /** + * 开始日期 + */ + @TableField(exist = false) + private String strdtStr; + /** * 结束日期 */ private Date enddt; + /** + * 结束日期 + */ + @TableField(exist = false) + private String enddtStr; + /** * 游向(上行/下行/上行折返/下行折返) */ @@ -254,4 +268,10 @@ public class FishDraftData implements Serializable { @TableField(exist = false) private String ftpName; + @TableField(exist = false) + private List> vdpthList; + + @TableField(exist = false) + private List> picpthList; + } \ 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 index 99eed19..64641db 100644 --- a/backend/src/main/java/com/yfd/platform/data/domain/FishImportResult.java +++ b/backend/src/main/java/com/yfd/platform/data/domain/FishImportResult.java @@ -21,8 +21,6 @@ public class FishImportResult { private List unrecognizedFields; private Map imageFiles; private Map videoFiles; -// public Map images; -// public Map videos; private String tempDir; private String excelFileName; private String excelFilePath; 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 53b6311..a132556 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 @@ -96,9 +96,34 @@ public class ImportTask implements Serializable { private Date expireTime; /** - * 导入结果JSON(存储校验后的数据) + * 解析结果摘要 */ - private String resultJson; + private String summary; + + /** + * 结果代码(0成功 1失败) + */ + private String code; + + /** + * 结果消息 + */ + private String message; + + /** + * 未识别字段(逗号分隔) + */ + private String unrecognizedFields; + +// /** +// * 图片文件映射JSON +// */ +// private String imageFilesJson; +// +// /** +// * 视频文件映射JSON +// */ +// private String videoFilesJson; /** * 创建时间 diff --git a/backend/src/main/java/com/yfd/platform/data/domain/ImportTaskRow.java b/backend/src/main/java/com/yfd/platform/data/domain/ImportTaskRow.java new file mode 100644 index 0000000..a43098a --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/domain/ImportTaskRow.java @@ -0,0 +1,104 @@ +package com.yfd.platform.data.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + *

+ * 导入任务行数据表(DATA_JSON 拆为独立字段,彻底消除 CLOB) + *

+ */ +@Data +@EqualsAndHashCode(callSuper = false) +@TableName("IMPORT_TASK_ROW") +public class ImportTaskRow implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + private String taskId; + + private Integer rowIndex; + + private String status; + + // ========== FishDraftData 字段(原 DATA_JSON) ========== + + private String dataId; + + private String stcd; + + private String stnm; + + private String rstcd; + + private String ennm; + + private String hbrvcd; + + private String hbrvnm; + + private String rvcd; + + private String baseId; + + private String baseName; + + private Date strdt; + + private String strdtStr; + + private Date enddt; + + private String ftp; + + private String ftpName; + + private Integer isfs; + + private String direction; + + private Integer fcnt; + + private String fsz; + + private String fwet; + + private BigDecimal wt; + + private String picpth; + + private String vdpth; + + private Date tm; + + private String sourceType; + + private Integer mouth; + + private Integer yr; + + // ========== 校验相关 ========== + + private String warnings; + + private String unrecognizedFields; + + private String vdpthListJson; + + private String picpthListJson; + + private String vdpthWarnings; + + private String picpthWarnings; + + @TableField(fill = FieldFill.INSERT) + private Date createdAt; +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/domain/SysUserDataScope.java b/backend/src/main/java/com/yfd/platform/data/domain/SysUserDataScope.java index b3513dd..beca8a9 100644 --- a/backend/src/main/java/com/yfd/platform/data/domain/SysUserDataScope.java +++ b/backend/src/main/java/com/yfd/platform/data/domain/SysUserDataScope.java @@ -49,6 +49,12 @@ public class SysUserDataScope implements Serializable { */ private String orgId; + /** + * 资源名称(根据orgType关联查询得出) + */ + @TableField(exist = false) + private String orgName; + /** * 上级资源编码(可选,用于层级追溯) */ diff --git a/backend/src/main/java/com/yfd/platform/data/domain/vo/FishStatisticsVO.java b/backend/src/main/java/com/yfd/platform/data/domain/vo/FishStatisticsVO.java new file mode 100644 index 0000000..369cbf9 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/domain/vo/FishStatisticsVO.java @@ -0,0 +1,38 @@ +package com.yfd.platform.data.domain.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +public class FishStatisticsVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private String userId; + + private String realName; + + private String phone; + + private String contact; + + private String basinNames; + + private String stationNames; + + private String basinCode; + + private String stationCode; + + private String reportMonth; + + private Date minStrdt; + + private Date maxEnddt; + + private Integer totalFcnt; + + private Integer hasData; +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/mapper/FishStatisticsMapper.java b/backend/src/main/java/com/yfd/platform/data/mapper/FishStatisticsMapper.java new file mode 100644 index 0000000..47a44ec --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/mapper/FishStatisticsMapper.java @@ -0,0 +1,17 @@ +package com.yfd.platform.data.mapper; + +import com.yfd.platform.data.domain.vo.FishStatisticsVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FishStatisticsMapper { + + List queryStatistics(@Param("basinCode") String basinCode, + @Param("stationCode") String stationCode, + @Param("startRow") int startRow, + @Param("endRow") int endRow); + + int countStatistics(@Param("basinCode") String basinCode, + @Param("stationCode") String stationCode); +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskMapper.java b/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskMapper.java index 0937aab..a7d4edd 100644 --- a/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskMapper.java +++ b/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskMapper.java @@ -40,11 +40,10 @@ public interface ImportTaskMapper extends BaseMapper { @Select("SELECT * FROM (" + "SELECT ID, IMPORT_NO, BIZ_TYPE, FILE_NAME, FILE_SIZE, FILE_PATH, TEMP_DIR, " + - "TOTAL_COUNT, SUCCESS_COUNT, FAIL_COUNT, STATUS, ERROR_MSG, " + - "UPLOAD_USER_ID, UPLOAD_TIME, EXPIRE_TIME, CREATED_AT, UPDATED_AT " + + "TOTAL_COUNT, SUCCESS_COUNT, FAIL_COUNT, STATUS, ERROR_MSG " + "FROM IMPORT_TASK " + "WHERE UPLOAD_USER_ID = #{uploadUserId} " + - "AND STATUS IN ('UPLOADED', 'PARSING', 'VALIDATED') " + + "AND STATUS IN ('UPLOADED', 'PARSING', 'VALIDATED','FAILED') " + "ORDER BY CREATED_AT DESC" + ") WHERE ROWNUM = 1") List selectByUserIdAndStatuses(@Param("uploadUserId") String uploadUserId); diff --git a/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskRowMapper.java b/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskRowMapper.java new file mode 100644 index 0000000..a1e56db --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/mapper/ImportTaskRowMapper.java @@ -0,0 +1,21 @@ +package com.yfd.platform.data.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.data.domain.ImportTaskRow; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + *

+ * 导入任务行数据表 Mapper 接口 + *

+ */ +public interface ImportTaskRowMapper extends BaseMapper { + + List selectByTaskId(@Param("taskId") String taskId); + + List selectByTaskIdAndStatus(@Param("taskId") String taskId, @Param("status") String status); + + int deleteByTaskId(@Param("taskId") String taskId); +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/mapper/SysUserDataScopeMapper.java b/backend/src/main/java/com/yfd/platform/data/mapper/SysUserDataScopeMapper.java index 8a592c2..e1e10f1 100644 --- a/backend/src/main/java/com/yfd/platform/data/mapper/SysUserDataScopeMapper.java +++ b/backend/src/main/java/com/yfd/platform/data/mapper/SysUserDataScopeMapper.java @@ -43,4 +43,18 @@ public interface SysUserDataScopeMapper extends BaseMapper { * 查询有效权限(状态=1且在有效期内的) */ List selectValidPermissions(@Param("userId") String userId); + + /** + * 根据用户ID查询权限列表(含关联资源名称) + * orgType=STATION时关联SD_ENGINFO_B_H获取ennm(工程名称) + * orgType=HBRVCD时关联SD_HBRV_DIC获取hbrvnm(基地流域名称) + * orgType=BASE时关联SD_HYDROBASE获取basename(基地名称) + * orgType=RVCD时关联SD_RVCD_DIC获取rvnm(流域名称) + */ + List selectValidPermissionsWithName(@Param("userId") String userId); + + /** + * 批量根据用户ID查询权限列表(含关联资源名称) + */ + List selectValidPermissionsWithNameByUserIds(@Param("userIds") List userIds); } diff --git a/backend/src/main/java/com/yfd/platform/data/service/IFishDraftDataService.java b/backend/src/main/java/com/yfd/platform/data/service/IFishDraftDataService.java index d3b2486..2504172 100644 --- a/backend/src/main/java/com/yfd/platform/data/service/IFishDraftDataService.java +++ b/backend/src/main/java/com/yfd/platform/data/service/IFishDraftDataService.java @@ -96,4 +96,9 @@ public interface IFishDraftDataService extends IService { * 审批人修改数据并记录变更日志 */ boolean updateByIdWithLog(FishDraftData fishDraftData); + + /** + * 批量保存草稿(带事务,仅包裹 INSERT) + */ + boolean batchSaveDraft(List fishDraftDataList); } \ No newline at end of file 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 index c78bdc5..4862f01 100644 --- a/backend/src/main/java/com/yfd/platform/data/service/IFishImportService.java +++ b/backend/src/main/java/com/yfd/platform/data/service/IFishImportService.java @@ -1,5 +1,6 @@ package com.yfd.platform.data.service; +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.utils.ZipFileUtil; @@ -7,6 +8,8 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.InputStream; +import java.util.List; +import java.util.Map; /** *

@@ -52,8 +55,6 @@ public interface IFishImportService { void processAttachments(FishImportResult result, ZipFileUtil.ZipContent zipContent); - void processAttachmentsAsync(java.util.List dataList, - java.util.Map imageFiles, - java.util.Map videoFiles, + void processAttachmentsAsync(List dataList, String tempDir); } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/IFishStatisticsService.java b/backend/src/main/java/com/yfd/platform/data/service/IFishStatisticsService.java new file mode 100644 index 0000000..514d485 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/service/IFishStatisticsService.java @@ -0,0 +1,10 @@ +package com.yfd.platform.data.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.common.DataSourceRequest; +import com.yfd.platform.data.domain.vo.FishStatisticsVO; + +public interface IFishStatisticsService { + + Page queryPage(DataSourceRequest dataSourceRequest); +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/IImportTaskService.java b/backend/src/main/java/com/yfd/platform/data/service/IImportTaskService.java index 9726983..d0ca0b7 100644 --- a/backend/src/main/java/com/yfd/platform/data/service/IImportTaskService.java +++ b/backend/src/main/java/com/yfd/platform/data/service/IImportTaskService.java @@ -2,7 +2,9 @@ package com.yfd.platform.data.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.data.domain.FishImportResult; import com.yfd.platform.data.domain.ImportTask; +import com.yfd.platform.data.domain.ImportTaskRow; import java.util.List; @@ -79,12 +81,27 @@ public interface IImportTaskService extends IService { ImportTask getCurrentTaskByUserId(String uploadUserId); /** - * 保存导入结果JSON + * 保存导入结果(同时写入 IMPORT_TASK 汇总字段和 IMPORT_TASK_ROW 行数据) */ - boolean saveResultJson(String taskId, String resultJson); + boolean saveResultJson(String taskId, FishImportResult result); /** * 获取用户最后一次导入结果(用于断点续传或查看历史) */ ImportTask getLastImportResult(String uploadUserId); + + /** + * 根据任务ID查询行数据列表 + */ + List getRowsByTaskId(String taskId); + + /** + * 根据任务ID和状态查询行数据列表 + */ + List getRowsByTaskIdAndStatus(String taskId, String status); + + /** + * 根据任务ID重建 FishImportResult 对象(从行表组装) + */ + FishImportResult buildImportResult(String taskId); } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/impl/FishDraftDataServiceImpl.java b/backend/src/main/java/com/yfd/platform/data/service/impl/FishDraftDataServiceImpl.java index c1d83a4..4fd0c39 100644 --- a/backend/src/main/java/com/yfd/platform/data/service/impl/FishDraftDataServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/data/service/impl/FishDraftDataServiceImpl.java @@ -198,6 +198,7 @@ public class FishDraftDataServiceImpl extends ServiceImpl fishDraftDataList) { + if (fishDraftDataList == null || fishDraftDataList.isEmpty()) { + return false; + } + long start = System.currentTimeMillis(); + boolean result = this.saveBatch(fishDraftDataList, 500); + log.info("批量保存草稿完成, 共{}条, 耗时{}ms", fishDraftDataList.size(), System.currentTimeMillis() - start); + return result; + } } \ 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 index 0228d07..5d49a60 100644 --- 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 @@ -3,6 +3,7 @@ package com.yfd.platform.data.service.impl; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.yfd.platform.data.domain.FishDraftData; import com.yfd.platform.data.domain.FishImportRequest; import com.yfd.platform.data.domain.FishImportResult; @@ -19,6 +20,7 @@ import com.yfd.platform.system.service.ISysDictionaryService; import com.yfd.platform.utils.SecurityUtils; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.apache.poi.EncryptedDocumentException; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.security.core.context.SecurityContext; @@ -33,9 +35,13 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.text.ParseException; +import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; @Service @Slf4j @@ -74,6 +80,12 @@ public class FishImportServiceImpl implements IFishImportService { @Resource private SysUserDataScopeMapper userDataScopeMapper; + private final ExecutorService attachmentExecutor = Executors.newFixedThreadPool(4, r -> { + Thread t = new Thread(r, "attachment-upload"); + t.setDaemon(true); + return t; + }); + private static final Map EXCEL_COLUMN_MAPPING = new LinkedHashMap<>(); private static final Map EXCEL_COLUMN_INDEX_MAPPING = new LinkedHashMap<>(); @@ -176,38 +188,14 @@ public class FishImportServiceImpl implements IFishImportService { Map columnIndexMap = new HashMap<>(EXCEL_COLUMN_INDEX_MAPPING); - int totalRows = sheet.getLastRowNum(); - for (int i = 1; i <= totalRows; i++) { - Row row = sheet.getRow(i); - if (row == null || isRowEmpty(row)) { - continue; - } - - FishImportResult.FishImportRow importRow = parseRow(result, row, columnIndexMap, i, uploadUserId); - result.setTotalCount(result.getTotalCount() + 1); - if (importRow.getData() != null && importRow.getWarnings().isEmpty()) { - result.addSuccessRow(importRow); - result.setSuccessCount(result.getSuccessCount() + 1); - } else { - result.addFailedRow(importRow); - result.setFailedCount(result.getFailedCount() + 1); - } - } - result.setSummary(String.format("共解析%d条数据,失败%d条", - result.getSuccessCount(), result.getFailedCount())); - - return result; - } - - private FishImportResult.FishImportRow parseRow(FishImportResult result, Row row, Map columnIndexMap, int rowIndex, String userId) { - FishImportResult.FishImportRow importRow = new FishImportResult.FishImportRow(rowIndex); - FishDraftData data = new FishDraftData(); - data.setId(UUID.randomUUID().toString()); Set allowedHbrvcdSet = new HashSet<>(); Set directStcdSet = new HashSet<>(); - Set directBHSet = new HashSet<>(); - if (userId != null) { - List permissions = userDataScopeMapper.selectValidPermissions(userId); + + List sdFpssList=new ArrayList<>(); + List allowedHbrvcdList = new ArrayList<>(); + List directStcdList = new ArrayList<>(); + if (uploadUserId != null) { + List permissions = userDataScopeMapper.selectValidPermissions(uploadUserId); if (permissions != null && !permissions.isEmpty()) { for (SysUserDataScope permission : permissions) { String orgType = permission.getOrgType(); @@ -226,7 +214,6 @@ public class FishImportServiceImpl implements IFishImportService { } } -// Set allStcdSet = new HashSet<>(); if (!allowedHbrvcdSet.isEmpty() || !directStcdSet.isEmpty()) { if (!allowedHbrvcdSet.isEmpty()) { @@ -250,14 +237,52 @@ public class FishImportServiceImpl implements IFishImportService { } } } + + if(!allowedHbrvcdSet.isEmpty()){ + List sdHbrvDics = sdHbrvDicMapper.selectList(new LambdaQueryWrapper().in(SdHbrvDic::getHbrvcd, allowedHbrvcdSet).select(SdHbrvDic::getHbrvcd, SdHbrvDic::getHbrvnm)); + allowedHbrvcdList.addAll(sdHbrvDics); + } + if (!directStcdSet.isEmpty()) { - List sdFpssBHS = fpssBHMapper.selectList(new LambdaQueryWrapper().in(SdFpssBH::getRstcd, directStcdSet).select(SdFpssBH::getStcd)); - for (SdFpssBH sdFpssBH : sdFpssBHS) { - if (sdFpssBH.getStcd() != null) { - directBHSet.add(sdFpssBH.getStcd()); - } + List sdFpssBHS = fpssBHMapper.selectList(new LambdaQueryWrapper().in(SdFpssBH::getRstcd, directStcdSet).select(SdFpssBH::getStcd, SdFpssBH::getStnm)); + //设施 + sdFpssList.addAll(sdFpssBHS.stream().filter(sdFpssBH -> sdFpssBH.getStcd() != null).toList()); + + //电站 + directStcdList.addAll(engInfoBHMapper.selectList(new LambdaQueryWrapper().in(SdEngInfoBH::getStcd, directStcdSet).select(SdEngInfoBH::getStcd, SdEngInfoBH::getEnnm))); + + } + + int totalRows = sheet.getLastRowNum(); + for (int i = 1; i <= totalRows; i++) { + Row row = sheet.getRow(i); + if (row == null || isRowEmpty(row)) { + continue; + } + + FishImportResult.FishImportRow importRow = parseRow(result, row, columnIndexMap, i, uploadUserId,allowedHbrvcdList,directStcdList,sdFpssList); + result.setTotalCount(result.getTotalCount() + 1); + if (importRow.getData() != null && importRow.getWarnings().isEmpty()) { + result.addSuccessRow(importRow); + result.setSuccessCount(result.getSuccessCount() + 1); + } else { + result.addFailedRow(importRow); + result.setFailedCount(result.getFailedCount() + 1); } } + result.setSummary(String.format("共解析%d条数据,失败%d条", + result.getSuccessCount(), result.getFailedCount())); + + return result; + } + + private FishImportResult.FishImportRow parseRow(FishImportResult result, Row row, Map columnIndexMap, int rowIndex, String userId,List allowedHbrvcdList,List directStcdList,List sdFpssList) { + + + FishImportResult.FishImportRow importRow = new FishImportResult.FishImportRow(rowIndex); + FishDraftData data = new FishDraftData(); + data.setId(UUID.randomUUID().toString()); + for (Map.Entry entry : columnIndexMap.entrySet()) { Integer columnIndex = entry.getKey(); String fieldName = entry.getValue(); @@ -271,20 +296,14 @@ public class FishImportServiceImpl implements IFishImportService { data.setEnnm(cellValue.trim()); data.setRstcd(cellValue); } else { - String stcd = resolveStationCode(cellValue.trim()); - if (stcd == null) { + String stcd = directStcdList.stream().filter(sdEngInfoBH -> sdEngInfoBH.getEnnm().equals(cellValue.trim())).map(SdEngInfoBH::getStcd).findFirst().orElse( null); + if (StrUtil.isBlank(stcd)) { importRow.getWarnings().add("rstcd"); data.setEnnm(cellValue.trim()); + data.setRstcd(cellValue.trim()); } else { - if (directStcdSet.contains(stcd)) { - data.setEnnm(cellValue.trim()); - data.setRstcd(stcd); - } else { - importRow.getWarnings().add("rstcd"); - data.setEnnm(cellValue.trim()); - data.setRstcd(cellValue); - } - + data.setEnnm(cellValue.trim()); + data.setRstcd(stcd); } } break; @@ -322,33 +341,17 @@ public class FishImportServiceImpl implements IFishImportService { data.setHbrvcd(cellValue); data.setHbrvnm(cellValue); } else { - String hbrvcdCode = resolveHbrvcdCode(cellValue.trim()); - if (hbrvcdCode == null) { + String hbrvcdCode = allowedHbrvcdList.stream().filter(sdHbrvDic -> sdHbrvDic.getHbrvnm().equals(cellValue.trim())).map(SdHbrvDic::getHbrvcd).findFirst().orElse( null); + if (StrUtil.isBlank(hbrvcdCode)) { importRow.getWarnings().add("hbrvcd"); data.setHbrvcd(cellValue.trim()); data.setHbrvnm(cellValue.trim()); } else { - if (allowedHbrvcdSet.contains(hbrvcdCode)) { - data.setHbrvcd(hbrvcdCode); - data.setHbrvnm(cellValue.trim()); - } else { - importRow.getWarnings().add("hbrvcd"); - data.setHbrvcd(cellValue.trim()); - data.setHbrvnm(cellValue.trim()); - } - + data.setHbrvcd(hbrvcdCode); + data.setHbrvnm(cellValue.trim()); } } -// if (StringUtils.hasText(cellValue)) { -// String rvcd = resolveHbrvcdCode(cellValue.trim()); -// if (rvcd == null) { -// importRow.getWarnings().add(fieldName); -// data.setRvcd(cellValue); -// } else { -// data.setHbrvcd(rvcd); -// data.setHbrvcd(rvcd); -// } -// } +// break; case "tm": if (!StringUtils.hasText(cellValue)) { @@ -379,7 +382,10 @@ public class FishImportServiceImpl implements IFishImportService { } break; case "fsz": - data.setFsz(cellValue.trim()); + if (StringUtils.hasText(cellValue)) { + String parsedFsz = parseFishSizeRange(cellValue.trim()); + data.setFsz(parsedFsz); + } break; case "fcnt": if (!StringUtils.hasText(cellValue)) { @@ -412,13 +418,20 @@ public class FishImportServiceImpl implements IFishImportService { break; case "strdt": if (!StringUtils.hasText(cellValue)) { - importRow.getWarnings().add(fieldName); + importRow.getWarnings().add("strdtStr"); + data.setStrdtStr(cellValue); } else { Date strdt = parseDate(cellValue); if (strdt == null) { - importRow.getWarnings().add(fieldName); + importRow.getWarnings().add("strdtStr"); + data.setStrdt(null); + data.setStrdtStr(cellValue.replaceAll("T", " ")); + }else{ + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String dateString = sdf.format(strdt); + data.setStrdtStr(dateString); + data.setStrdt(strdt); } - data.setStrdt(strdt); } break; case "enddt": @@ -445,6 +458,9 @@ public class FishImportServiceImpl implements IFishImportService { Map videoFiles = result.getVideoFiles(); for (String fileName : vdpth.split(";")) { + if(StrUtil.isBlank(fileName)){ + continue; + } for (String entryName : videoFiles.keySet()) { if (entryName.equals(fileName) || entryName.endsWith("/" + fileName) || entryName.endsWith("\\" + fileName)) { Map objectObjectHashMap = new HashMap<>(); @@ -452,6 +468,13 @@ public class FishImportServiceImpl implements IFishImportService { objectObjectHashMap.put("value", fileName); importRow.getVdpthList().add(objectObjectHashMap); vdpthList.add(fileName); + } else if (!com.yfd.platform.utils.FileUtil.isVideoFileName(fileName)) { + Map objectObjectHashMap = new HashMap<>(); + objectObjectHashMap.put("name", fileName); + objectObjectHashMap.put("value", fileName); + importRow.getVdpthList().add(objectObjectHashMap); + vdpthList.add(fileName); + importRow.getVdpthsWarnings().add(fileName); } } @@ -467,6 +490,9 @@ public class FishImportServiceImpl implements IFishImportService { Map imageFiles = result.getImageFiles(); for (String fileName : picpth.split(";")) { + if(StrUtil.isBlank(fileName)){ + continue; + } for (String entryName : imageFiles.keySet()) { if (entryName.equals(fileName) || entryName.endsWith("/" + fileName) || entryName.endsWith("\\" + fileName)) { Map objectObjectHashMap = new HashMap<>(); @@ -474,6 +500,13 @@ public class FishImportServiceImpl implements IFishImportService { objectObjectHashMap.put("value", fileName); importRow.getPicpthList().add(objectObjectHashMap); picpthList.add(fileName); + } else if (!com.yfd.platform.utils.FileUtil.isImageFileName(fileName)) { + Map objectObjectHashMap = new HashMap<>(); + objectObjectHashMap.put("name", fileName); + objectObjectHashMap.put("value", fileName); + importRow.getPicpthList().add(objectObjectHashMap); + picpthList.add(fileName); + importRow.getPicpthsWarnings().add(fileName); } } @@ -498,21 +531,14 @@ public class FishImportServiceImpl implements IFishImportService { importRow.getWarnings().add("stcd"); data.setStcd(cellValue); } else { - String stcd = resolveFpssCode(cellValue.trim()); - if (stcd == null) { + String stcd = sdFpssList.stream().filter(sdFpssBH -> sdFpssBH.getStnm().equals(cellValue.trim())).map(SdFpssBH::getStcd).findFirst().orElse( null); + if (StrUtil.isBlank(stcd)) { importRow.getWarnings().add("stcd"); data.setStcd(cellValue.trim()); data.setStnm(cellValue.trim()); } else { - if (directBHSet.contains(stcd)) { - data.setStnm(cellValue.trim()); - data.setStcd(stcd); - } else { - importRow.getWarnings().add("stcd"); - data.setStcd(cellValue.trim()); - data.setStnm(cellValue.trim()); - } - + data.setStnm(cellValue.trim()); + data.setStcd(stcd); } } break; @@ -520,7 +546,8 @@ public class FishImportServiceImpl implements IFishImportService { break; } } catch (Exception e) { - e.printStackTrace(); + log.error("字段[" + fieldName + "]解析异常: " + e.getMessage()); +// e.printStackTrace(); // importRow.getWarnings().add("字段[" + fieldName + "]解析异常: " + e.getMessage()); } } @@ -529,34 +556,111 @@ public class FishImportServiceImpl implements IFishImportService { return importRow; } + + /** + * 解析鱼类体长范围 + * 从混乱的字符串中提取所有数字(支持小数),返回 "最小值~最大值" 格式 + * 例如: "123123&234.dey76fd78" -> 提取出 123123, 234., 76, 78 -> "76~123123" + * + * @param input 原始字符串 + * @return 格式化后的范围字符串,如果没有有效数字则返回原字符串 + */ + private String parseFishSizeRange(String input) { + if (!StringUtils.hasText(input)) { + return input; + } + + // 使用正则表达式提取所有数字(包括整数和小数) + // 解释: \d+ 匹配一个或多个数字, (\.\d+)? 匹配可选的小数部分 + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\d+(?:\\.\\d+)?"); + java.util.regex.Matcher matcher = pattern.matcher(input); + + List numbers = new ArrayList<>(); + while (matcher.find()) { + try { + String numStr = matcher.group(); + // 排除单独的点或无效格式 + if (numStr != null && !numStr.isEmpty()) { + numbers.add(Double.parseDouble(numStr)); + } + } catch (NumberFormatException e) { + // 忽略无法解析的数字 + } + } + + // 如果没有提取到任何数字,返回原字符串或空 + if (numbers.isEmpty()) { + log.warn("鱼类体长字段未提取到有效数字: {}", input); + return input; + } + + // 找出最小值和最大值 + double min = numbers.stream().mapToDouble(Double::doubleValue).min().getAsDouble(); + double max = numbers.stream().mapToDouble(Double::doubleValue).max().getAsDouble(); + + // 格式化结果:如果是整数则不显示小数点,否则保留原有精度 + String minStr = formatNumber(min); + String maxStr = formatNumber(max); + + return minStr + "~" + maxStr; + } + + /** + * 格式化数字:如果是整数则去掉 .0,否则保留小数 + */ + private String formatNumber(double value) { + if (value == Math.floor(value) && !Double.isInfinite(value)) { + return String.valueOf((long) value); + } else { + // 去除末尾多余的 0,例如 12.50 -> 12.5 + return String.valueOf(value).replaceAll("\\.?0+$", ""); + } + } + + + private void validateStationFpssRelation(FishDraftData data, FishImportResult.FishImportRow importRow) { - loadStationAndBaseCache(); - if (StringUtils.hasText(data.getHbrvcd()) && StringUtils.hasText(data.getRstcd())) { - if (!validateStationBelongsToBase(data.getRstcd(), data.getHbrvcd())) { - if (!importRow.getWarnings().contains("hbrvcd")) { - importRow.getWarnings().add("hbrvcd"); - } - if (!importRow.getWarnings().contains("rstcd")) { - importRow.getWarnings().add("rstcd"); - } - if (!importRow.getWarnings().contains("stcd")) { - importRow.getWarnings().add("stcd"); - } + if (importRow.getWarnings().contains("hbrvcd")) { + if (!importRow.getWarnings().contains("rstcd")) { + importRow.getWarnings().add("rstcd"); + } + if (!importRow.getWarnings().contains("stcd")) { + importRow.getWarnings().add("stcd"); } } - if (StringUtils.hasText(data.getRstcd()) && StringUtils.hasText(data.getStcd())) { - if (!validateFpssBelongsToStation(data.getStcd(), data.getRstcd())) { - if (!importRow.getWarnings().contains("hbrvcd")) { - importRow.getWarnings().add("hbrvcd"); - } - if (!importRow.getWarnings().contains("stcd")) { - importRow.getWarnings().add("stcd"); - } - if (!importRow.getWarnings().contains("rstcd")) { - importRow.getWarnings().add("rstcd"); - } + if (importRow.getWarnings().contains("rstcd")) { + if (!importRow.getWarnings().contains("stcd")) { + importRow.getWarnings().add("stcd"); } } + + // loadStationAndBaseCache(); +// if (StringUtils.hasText(data.getHbrvcd()) && StringUtils.hasText(data.getRstcd())) { +// if (!validateStationBelongsToBase(data.getRstcd(), data.getHbrvcd())) { +// if (!importRow.getWarnings().contains("hbrvcd")) { +// importRow.getWarnings().add("hbrvcd"); +// } +// if (!importRow.getWarnings().contains("rstcd")) { +// importRow.getWarnings().add("rstcd"); +// } +// if (!importRow.getWarnings().contains("stcd")) { +// importRow.getWarnings().add("stcd"); +// } +// } +// } +// if (StringUtils.hasText(data.getRstcd()) && StringUtils.hasText(data.getStcd())) { +// if (!validateFpssBelongsToStation(data.getStcd(), data.getRstcd())) { +// if (!importRow.getWarnings().contains("hbrvcd")) { +// importRow.getWarnings().add("hbrvcd"); +// } +// if (!importRow.getWarnings().contains("stcd")) { +// importRow.getWarnings().add("stcd"); +// } +// if (!importRow.getWarnings().contains("rstcd")) { +// importRow.getWarnings().add("rstcd"); +// } +// } +// } } private void loadStationAndBaseCache() { @@ -1119,28 +1223,103 @@ public class FishImportServiceImpl implements IFishImportService { return true; } - private Date parseDate(String dateStr) { + + private static Date parseDate(String dateStr) { if (!StringUtils.hasText(dateStr)) { return null; } + dateStr = dateStr.trim(); + + // 支持的日期格式列表(按常用程度排序) String[] patterns = { - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd", - "yyyy/MM/dd", - "yyyy.MM.dd", - "yyyyMMdd" + "yyyy-MM-dd HH:mm:ss", // 2024-01-15 14:30:00 + "yyyy-MM-dd'T'HH:mm:ss", // ⭐ 新增:支持 2024-01-15T14:30:00 (ISO格式) + "yyyy-MM-dd HH:mm", // 2024-01-15 14:30 + "yyyy-MM-dd'T'HH:mm", // ⭐ 确保这一行存在且正确:支持 2024-01-15T14:30 + "yyyy-MM-dd", // 2024-01-15 + "yyyy/MM/dd HH:mm:ss", // 2024/01/15 14:30:00 + "yyyy/MM/dd HH:mm", // 2024/01/15 14:30 + "yyyy/MM/dd", // 2024/01/15 (标准双位) + "yyyy/M/d HH:mm:ss", // 支持 2024/1/1 14:30:00 + "yyyy/M/d HH:mm", // 支持 2024/1/1 14:30 + "yyyy/M/d", // 支持 2024/1/1 (单位数) + "yyyy.MM.dd HH:mm:ss", // 2024.01.15 14:30:00 + "yyyy.MM.dd", // 2024.01.15 + "yyyy.M.d", // 支持 2024.1.1 + "yyyyMMdd HHmmss", // 20240115 143000 + "yyyyMMdd", // 20240115 + "yyyy年MM月dd日", // 2024年01月15日 + "yyyy年M月d日", // 支持 2024年1月1日 + "yyyy年MM月dd日HH时mm分ss秒" // 2024年01月15日14时30分00秒 }; + for (String pattern : patterns) { try { SimpleDateFormat sdf = new SimpleDateFormat(pattern); sdf.setLenient(false); - return sdf.parse(dateStr); - } catch (ParseException ignored) { + ParsePosition pos = new ParsePosition(0); + Date parsedDate = sdf.parse(dateStr, pos); + if (parsedDate != null && pos.getIndex() == dateStr.length()) { + // 整个字符串都被成功解析 + Calendar cal = Calendar.getInstance(); + cal.setTime(parsedDate); + int year = cal.get(Calendar.YEAR); + if (year >= 1900 && year <= 2100) { + return parsedDate; + } + } + } catch (Exception e) { + // ignore } } + log.debug("无法解析日期: '{}'", dateStr); return null; } + +// private Date parseDate(String dateStr) { +// if (!StringUtils.hasText(dateStr)) { +// return null; +// } +// +// // 去除首尾空格 +// dateStr = dateStr.trim(); +// +// // 支持的日期格式列表(按常用程度排序) +// String[] patterns = { +// "yyyy-MM-dd HH:mm:ss", // 2024-01-15 14:30:00 +// "yyyy-MM-dd", // 2024-01-15 +// "yyyy/MM/dd HH:mm:ss", // 2024/01/15 14:30:00 +// "yyyy/MM/dd", // 2024/01/15 +// "yyyy.MM.dd HH:mm:ss", // 2024.01.15 14:30:00 +// "yyyy.MM.dd", // 2024.01.15 +// "yyyyMMdd HHmmss", // 20240115 143000 +// "yyyyMMdd", // 20240115 +// "yyyy年MM月dd日", // 2024年01月15日 +// "yyyy年MM月dd日HH时mm分ss秒" // 2024年01月15日14时30分00秒 +// }; +// +// for (String pattern : patterns) { +// try { +// SimpleDateFormat sdf = new SimpleDateFormat(pattern); +// sdf.setLenient(false); // 严格模式,不允许非法日期 +// Date parsedDate = sdf.parse(dateStr); +// +// // 验证解析后的日期是否合理(例如年份不能是 0001) +// Calendar cal = Calendar.getInstance(); +// cal.setTime(parsedDate); +// int year = cal.get(Calendar.YEAR); +// if (year >= 1900 && year <= 2100) { +// return parsedDate; +// } +// } catch (ParseException e) { +// // 尝试下一个格式 +// } +// } +// +// log.debug("无法解析日期: '{}'", dateStr); +// return null; +// } private Integer parseInteger(String value) { if (!StringUtils.hasText(value)) { return null; @@ -1247,9 +1426,40 @@ public class FishImportServiceImpl implements IFishImportService { result.setTempDir(zipContent.tempDir); result.setImageFiles(zipContent.images); result.setVideoFiles(zipContent.videos); - try (Workbook workbook = new XSSFWorkbook(new FileInputStream(zipContent.excelFilePath))) { + Workbook workbook = null; + // 1. 验证文件是否存在且有效 + File excelFile = new File(zipContent.excelFilePath); + if (!excelFile.exists() || excelFile.length() == 0) { + log.error("Excel文件不存在或为空: {}", zipContent.excelFilePath); + throw new RuntimeException("Excel文件不存在或已损坏"); + } + + try (InputStream fis = new FileInputStream(excelFile)) { + // 2. 使用 WorkbookFactory 自动识别 .xls 和 .xlsx 格式 + // 这样可以避免因为格式不匹配导致的 NotOfficeXmlFileException + workbook = WorkbookFactory.create(fis); + Sheet sheet = workbook.getSheetAt(0); result = parseSheet(sheet, result, uploadUserId); + + } catch (EncryptedDocumentException e) { + log.error("Excel文件已加密,无法解析: {}", excelFile.getName(), e); + throw new RuntimeException("Excel文件已设置密码,请移除密码后重新上传"); + } catch (InvalidFormatException e) { + log.error("Excel文件格式无效: {}", excelFile.getName(), e); + throw new RuntimeException("Excel文件格式无效或已损坏"); + } catch (IOException e) { + log.error("读取Excel文件IO异常: {}", excelFile.getName(), e); + throw new RuntimeException("读取Excel文件失败"); + } finally { + // 3. 确保 Workbook 被正确关闭,释放资源 + if (workbook != null) { + try { + workbook.close(); + } catch (IOException e) { + log.warn("关闭Workbook失败", e); + } + } } result.setExcelFileName(zipContent.excelFileName); result.setExcelFilePath(zipContent.excelFilePath); @@ -1293,8 +1503,6 @@ public class FishImportServiceImpl implements IFishImportService { @Override public void processAttachmentsAsync(List dataList, - Map imageFiles, - Map videoFiles, String tempDir) { if (dataList == null || dataList.isEmpty()) { return; @@ -1308,10 +1516,16 @@ public class FishImportServiceImpl implements IFishImportService { String vdpth = data.getVdpth(); String picpth = data.getPicpth(); - if(StrUtil.isBlank(vdpth)||StrUtil.isBlank(picpth)){ + if (StrUtil.isBlank(vdpth) && StrUtil.isBlank(picpth)) { log.error("数据不完整, 忽略处理"); return; } + List> vdpthList = data.getVdpthList(); + List> picpthList = data.getPicpthList(); + + Map videoFiles = buildFileMap(vdpthList, tempDir, "videos"); + Map imageFiles = buildFileMap(picpthList, tempDir, "images"); + if (StringUtils.hasText(vdpth) && videoFiles != null && !videoFiles.isEmpty()) { String uploadedVdpth = uploadVideoFilesAsync(vdpth, videoFiles); if (uploadedVdpth != null) { @@ -1325,27 +1539,28 @@ public class FishImportServiceImpl implements IFishImportService { data.setPicpth(uploadedPicpth); } } - - fishDraftDataService.updateById(data); log.info("异步上传附件完成, dataId: {},{},{}", data.getId(), data.getPicpth(), data.getVdpth()); } catch (Exception e) { log.error("异步上传附件失败, dataId: {}", data.getId(), e); } finally { SecurityContextHolder.clearContext(); } - }); + }, attachmentExecutor); futures.add(future); } CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenRun(() -> { - try { - // del 方法会递归删除目录及其所有内容 + fishDraftDataService.updateBatchById(dataList); + log.info("批量更新附件路径完成, 共{}条", dataList.size()); + } catch (Exception e) { + log.error("批量更新附件路径失败", e); + } + try { FileUtil.del(tempDir); } catch (Exception e) { - log.error("删除临时目录失败{}", e.getMessage()); + log.error("删除临时目录失败{}", e.getMessage()); } - }) .exceptionally(ex -> { log.error("等待异步任务完成时发生异常", ex); @@ -1353,6 +1568,27 @@ public class FishImportServiceImpl implements IFishImportService { }); } + + private Map buildFileMap(List> fileList, String tempDir, String subDir) { + if (fileList == null || fileList.isEmpty()) { + return null; + } + + Map fileMap = new HashMap<>(); + String baseDir = tempDir + File.separator + subDir + File.separator; + + for (Map item : fileList) { + String name = item.get("name"); + String value = item.get("value"); + if (name != null && value != null) { + fileMap.put(name, baseDir + value); + } + } + + return fileMap.isEmpty() ? null : fileMap; + } + + private String uploadVideoFilesAsync(String videoNames, Map videoMap) { String[] fileNames = videoNames.split(";"); List attachmentIds = new ArrayList<>(); @@ -1508,4 +1744,17 @@ public class FishImportServiceImpl implements IFishImportService { } return null; } + + @jakarta.annotation.PreDestroy + public void shutdown() { + attachmentExecutor.shutdown(); + try { + if (!attachmentExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + attachmentExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + attachmentExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/impl/FishStatisticsServiceImpl.java b/backend/src/main/java/com/yfd/platform/data/service/impl/FishStatisticsServiceImpl.java new file mode 100644 index 0000000..8e975ed --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/data/service/impl/FishStatisticsServiceImpl.java @@ -0,0 +1,70 @@ +package com.yfd.platform.data.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.common.DataSourceLoadOptionsBase; +import com.yfd.platform.common.DataSourceRequest; +import com.yfd.platform.data.domain.vo.FishStatisticsVO; +import com.yfd.platform.data.mapper.FishStatisticsMapper; +import com.yfd.platform.data.service.IFishStatisticsService; +import com.yfd.platform.utils.KendoUtil; +import com.yfd.platform.utils.QgcQueryWrapperUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Service +@Slf4j +public class FishStatisticsServiceImpl implements IFishStatisticsService { + + @Resource + private FishStatisticsMapper fishStatisticsMapper; + + @Override + public Page queryPage(DataSourceRequest dataSourceRequest) { + DataSourceLoadOptionsBase loadOptions = dataSourceRequest.toDevRequest(); + + String basinNamesStr = QgcQueryWrapperUtil.getFilterFieldValue(loadOptions, "basinCode"); + String stationNamesStr = QgcQueryWrapperUtil.getFilterFieldValue(loadOptions, "stationCode"); + +// List basinNames = parseFilterList(basinNamesStr); +// List stationNames = parseFilterList(stationNamesStr); + + int currentPage = dataSourceRequest.getSkip(); + int pageSize = dataSourceRequest.getTake(); + + if (currentPage <= 0) { + currentPage = 1; + } + if (pageSize <= 0) { + pageSize = 20; + } + + int startRow = (currentPage - 1) * pageSize; + int endRow = startRow + pageSize; + + List records = fishStatisticsMapper.queryStatistics( + basinNamesStr, stationNamesStr, startRow, endRow); + + int total = fishStatisticsMapper.countStatistics(basinNamesStr, basinNamesStr); + + Page page = new Page<>(); + page.setRecords(records); + page.setTotal(total); + page.setSize(pageSize); + page.setCurrent(currentPage); + + return page; + } + + private List parseFilterList(String filterValue) { + if (StrUtil.isBlank(filterValue)) { + return new ArrayList<>(); + } + return Arrays.asList(filterValue.split(",")); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/service/impl/ImportTaskServiceImpl.java b/backend/src/main/java/com/yfd/platform/data/service/impl/ImportTaskServiceImpl.java index c8cb5f6..7b6e8e9 100644 --- a/backend/src/main/java/com/yfd/platform/data/service/impl/ImportTaskServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/data/service/impl/ImportTaskServiceImpl.java @@ -3,35 +3,45 @@ package com.yfd.platform.data.service.impl; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yfd.platform.data.domain.FishDraftData; import com.yfd.platform.data.domain.FishImportResult; import com.yfd.platform.data.domain.ImportTask; +import com.yfd.platform.data.domain.ImportTaskRow; import com.yfd.platform.data.mapper.ImportTaskMapper; +import com.yfd.platform.data.mapper.ImportTaskRowMapper; import com.yfd.platform.data.service.AttachmentUploadService; import com.yfd.platform.data.service.IImportTaskService; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; -import java.util.Date; -import java.util.List; +import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; /** *

* 导入任务表 服务实现类 *

*/ +@Slf4j @Service public class ImportTaskServiceImpl extends ServiceImpl implements IImportTaskService { @Resource private ImportTaskMapper importTaskMapper; + @Resource + private ImportTaskRowMapper importTaskRowMapper; + @Resource private ObjectMapper objectMapper; @Resource @@ -133,14 +143,10 @@ public class ImportTaskServiceImpl extends ServiceImpl() + .eq(ImportTask::getId, id) + .set(ImportTask::getStatus, "CONFIRMED") + .set(ImportTask::getUpdatedAt, new Date())); // 如果没有配置自动填充,建议保留此行 } @Override @@ -151,6 +157,9 @@ public class ImportTaskServiceImpl extends ServiceImpl ids = expiredTasks.stream().map(ImportTask::getId).toList(); + for (String id : ids) { + importTaskRowMapper.deleteByTaskId(id); + } return this.removeByIds(ids); } @@ -164,34 +173,38 @@ public class ImportTaskServiceImpl extends ServiceImpl().eq(ImportTask::getId, taskId).select(ImportTask::getId,ImportTask::getStatus, ImportTask::getTempDir)); if (importTask == null) { return false; } String currentStatus = importTask.getStatus(); - if ("CONFIRMED".equals(currentStatus) || "FAILED".equals(currentStatus) || "CANCELLED".equals(currentStatus)) { +// if ("CONFIRMED".equals(currentStatus) || "FAILED".equals(currentStatus) || "CANCELLED".equals(currentStatus)) { +// return false; +// } + + if ("CONFIRMED".equals(currentStatus) || "CANCELLED".equals(currentStatus)) { return false; } String temp = importTask.getTempDir(); - if (StrUtil.isNotBlank( temp)) { - FileUtil.del(temp); - }else{ - if (importTask.getResultJson() != null && !importTask.getResultJson().isEmpty()) { - try { - FishImportResult importResult = objectMapper.readValue(importTask.getResultJson(), FishImportResult.class); - String tempDir = importResult.getTempDir(); - // del 方法会递归删除目录及其所有内容 - FileUtil.del(tempDir); - } catch (Exception e) { - e.printStackTrace(); - // ignore parse error - } - } - } + importTask.setStatus("CANCELLED"); importTask.setErrorMsg("用户取消: " + operatorId); importTask.setUpdatedAt(new Date()); - return this.updateById(importTask); + importTaskRowMapper.deleteByTaskId(taskId); + boolean result = this.updateById(importTask); + + if (result && StrUtil.isNotBlank(temp)) { + CompletableFuture.runAsync(() -> { + try { + FileUtil.del(temp); + log.info("异步删除临时目录成功: {}", temp); + } catch (Exception e) { + log.error("异步删除临时目录失败: {}", temp, e); + } + }); + } + + return result; } @Override @@ -206,14 +219,103 @@ public class ImportTaskServiceImpl extends ServiceImpl rows = new ArrayList<>(); + for (FishImportResult.FishImportRow row : importResult.getRows()) { + ImportTaskRow taskRow = new ImportTaskRow(); + taskRow.setTaskId(taskId); + taskRow.setRowIndex(row.getRowIndex()); + taskRow.setStatus(row.getStatus()); + + FishDraftData data = row.getData(); + if (data != null) { + taskRow.setDataId(data.getId()); + taskRow.setStcd(data.getStcd()); + taskRow.setStnm(data.getStnm()); + taskRow.setRstcd(data.getRstcd()); + taskRow.setEnnm(data.getEnnm()); + taskRow.setHbrvcd(data.getHbrvcd()); + taskRow.setHbrvnm(data.getHbrvnm()); + taskRow.setRvcd(data.getRvcd()); + taskRow.setBaseId(data.getBaseId()); + taskRow.setBaseName(data.getBaseName()); + taskRow.setStrdt(data.getStrdt()); + taskRow.setStrdtStr(data.getStrdtStr()); + taskRow.setEnddt(data.getEnddt()); + taskRow.setFtp(data.getFtp()); + taskRow.setFtpName(data.getFtpName()); + taskRow.setIsfs(data.getIsfs()); + taskRow.setDirection(data.getDirection()); + taskRow.setFcnt(data.getFcnt()); + taskRow.setFsz(data.getFsz()); + taskRow.setFwet(data.getFwet()); + taskRow.setWt(data.getWt()); + taskRow.setPicpth(data.getPicpth()); + taskRow.setVdpth(data.getVdpth()); + taskRow.setTm(data.getTm()); + taskRow.setSourceType(data.getSourceType()); + taskRow.setMouth(data.getMouth()); + taskRow.setYr(data.getYr()); + } + if (row.getWarnings() != null && !row.getWarnings().isEmpty()) { + taskRow.setWarnings(String.join(",", row.getWarnings())); + } + if (row.getUnrecognizedFields() != null && !row.getUnrecognizedFields().isEmpty()) { + taskRow.setUnrecognizedFields(String.join(",", row.getUnrecognizedFields())); + } + if (row.getVdpthList() != null && !row.getVdpthList().isEmpty()) { + taskRow.setVdpthListJson(objectMapper.writeValueAsString(row.getVdpthList())); + } + if (row.getPicpthList() != null && !row.getPicpthList().isEmpty()) { + taskRow.setPicpthListJson(objectMapper.writeValueAsString(row.getPicpthList())); + } + if (row.getVdpthsWarnings() != null && !row.getVdpthsWarnings().isEmpty()) { + taskRow.setVdpthWarnings(String.join(",", row.getVdpthsWarnings())); + } + if (row.getPicpthsWarnings() != null && !row.getPicpthsWarnings().isEmpty()) { + taskRow.setPicpthWarnings(String.join(",", row.getPicpthsWarnings())); + } + taskRow.setCreatedAt(new Date()); + rows.add(taskRow); + } + importTaskRowMapper.insert(rows); + } + } catch (Exception e) { + log.error("保存导入结果行数据失败, taskId: {}", taskId, e); + importTask.setErrorMsg("保存行数据失败: " + e.getMessage()); + this.updateById(importTask); + return false; + } + + return true; } @Override @@ -221,4 +323,140 @@ public class ImportTaskServiceImpl extends ServiceImpl getRowsByTaskId(String taskId) { + return importTaskRowMapper.selectByTaskId(taskId); + } + + @Override + public List getRowsByTaskIdAndStatus(String taskId, String status) { + return importTaskRowMapper.selectByTaskIdAndStatus(taskId, status); + } + + @Override + public FishImportResult buildImportResult(String taskId) { + ImportTask importTask = this.getById(taskId); + if (importTask == null) { + return null; + } + + FishImportResult result = new FishImportResult(); + result.setTaskId(taskId); + result.setTempDir(importTask.getTempDir()); + result.setSummary(importTask.getSummary()); + result.setCode(importTask.getCode()); + result.setMessage(importTask.getMessage()); + result.setTotalCount(importTask.getTotalCount() != null ? importTask.getTotalCount() : 0); + result.setSuccessCount(importTask.getSuccessCount() != null ? importTask.getSuccessCount() : 0); + result.setFailedCount(importTask.getFailCount() != null ? importTask.getFailCount() : 0); + + if (importTask.getUnrecognizedFields() != null && !importTask.getUnrecognizedFields().isEmpty()) { + result.setUnrecognizedFields(Arrays.asList(importTask.getUnrecognizedFields().split(","))); + } + + + List rows = importTaskRowMapper.selectByTaskId(taskId); + if (rows != null && !rows.isEmpty()) { + for (ImportTaskRow row : rows) { + FishImportResult.FishImportRow importRow = new FishImportResult.FishImportRow(row.getRowIndex()); + importRow.setStatus(row.getStatus()); + + FishDraftData data = new FishDraftData(); + data.setId(row.getDataId()); + data.setStcd(row.getStcd()); + data.setStnm(row.getStnm()); + data.setRstcd(row.getRstcd()); + data.setEnnm(row.getEnnm()); + data.setHbrvcd(row.getHbrvcd()); + data.setHbrvnm(row.getHbrvnm()); + data.setRvcd(row.getRvcd()); + data.setBaseId(row.getBaseId()); + data.setBaseName(row.getBaseName()); + data.setStrdt(row.getStrdt()); + data.setStrdtStr(row.getStrdtStr()); + data.setEnddt(row.getEnddt()); + data.setFtp(row.getFtp()); + data.setFtpName(row.getFtpName()); + data.setIsfs(row.getIsfs()); + data.setDirection(row.getDirection()); + data.setFcnt(row.getFcnt()); + data.setFsz(row.getFsz()); + data.setFwet(row.getFwet()); + data.setWt(row.getWt()); + data.setPicpth(row.getPicpth()); + data.setVdpth(row.getVdpth()); + data.setTm(row.getTm()); + data.setSourceType(row.getSourceType()); + data.setMouth(row.getMouth()); + data.setYr(row.getYr()); + importRow.setData(data); + + if (row.getWarnings() != null && !row.getWarnings().isEmpty()) { + importRow.setWarnings(Arrays.asList(row.getWarnings().split(","))); + } + if (row.getUnrecognizedFields() != null && !row.getUnrecognizedFields().isEmpty()) { + importRow.setUnrecognizedFields(Arrays.asList(row.getUnrecognizedFields().split(","))); + } + if (row.getVdpthListJson() != null && !row.getVdpthListJson().isEmpty()) { + try { + List> vdpthList = objectMapper.readValue( + row.getVdpthListJson(), new TypeReference<>() { + }); + importRow.setVdpthList(vdpthList); + } catch (Exception e) { + log.error("解析视频列表JSON失败, rowId: {}", row.getId(), e); + } + } + if (row.getPicpthListJson() != null && !row.getPicpthListJson().isEmpty()) { + try { + List> picpthList = objectMapper.readValue( + row.getPicpthListJson(), new TypeReference>>() {}); + importRow.setPicpthList(picpthList); + } catch (Exception e) { + log.error("解析图片列表JSON失败, rowId: {}", row.getId(), e); + } + } + if (row.getVdpthWarnings() != null && !row.getVdpthWarnings().isEmpty()) { + importRow.setVdpthsWarnings(Arrays.asList(row.getVdpthWarnings().split(","))); + } + if (row.getPicpthWarnings() != null && !row.getPicpthWarnings().isEmpty()) { + importRow.setPicpthsWarnings(Arrays.asList(row.getPicpthWarnings().split(","))); + } + + if ("SUCCESS".equals(row.getStatus())) { + result.addSuccessRow(importRow); + } else { + result.addFailedRow(importRow); + } + } + + Map imageFiles = new LinkedHashMap<>(); + Map videoFiles = new LinkedHashMap<>(); + for (FishImportResult.FishImportRow row : result.getRows()) { + if (row.getPicpthList() != null) { + for (Map pic : row.getPicpthList()) { + String name = pic.get("name"); + String path = pic.get("path"); + if (name != null && path != null) { + imageFiles.putIfAbsent(name, path); + } + } + } + if (row.getVdpthList() != null) { + for (Map vid : row.getVdpthList()) { + String name = vid.get("name"); + String path = vid.get("path"); + if (name != null && path != null) { + videoFiles.putIfAbsent(name, path); + } + } + } + } + result.setImageFiles(imageFiles); + result.setVideoFiles(videoFiles); + } + + return result; + } } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java b/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java index 662fb4b..d183bca 100644 --- a/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java +++ b/backend/src/main/java/com/yfd/platform/data/utils/ZipFileUtil.java @@ -1,5 +1,7 @@ package com.yfd.platform.data.utils; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -364,22 +366,99 @@ public class ZipFileUtil { } private static String saveFileToDir(InputStream is, File tempDir, String subFolder, String fileName) throws IOException { + // 使用 Hutool 构建文件路径 + String safeFileName = sanitizeFileName(fileName); File folder = new File(tempDir, subFolder); - if (!folder.exists()) { - folder.mkdirs(); + + // 使用 Hutool 创建目录(会自动创建父目录) + FileUtil.mkdir(folder); + + log.info("保存文件: fileName{} -> safeFileName{}->subFolder{}", fileName, safeFileName,subFolder); + // 构建完整文件路径 + File file = new File(folder, safeFileName); + + // 安全检查:防止目录穿越 + String canonicalPath = file.getCanonicalPath(); + String canonicalDir = folder.getCanonicalPath(); + if (!canonicalPath.startsWith(canonicalDir)) { + throw new IOException("非法的文件路径: " + fileName); } - File file = new File(folder, fileName); - try (FileOutputStream fos = new FileOutputStream(file)) { - byte[] buffer = new byte[4096]; - int len; - while ((len = is.read(buffer)) > 0) { - fos.write(buffer, 0, len); - } + // 使用 Hutool 从流复制到文件 + try { + FileUtil.writeFromStream(is, file); + log.debug("保存文件: {} -> {}, 大小: {} bytes", fileName, safeFileName, file.length()); + } catch (Exception e) { + throw new IOException("保存文件失败: " + fileName, e); } + return file.getAbsolutePath(); } + + /** + * 清理和标准化文件名,确保在 Linux 上正确显示中文 + */ + private static String sanitizeFileName(String fileName) { + if (StrUtil.isBlank(fileName)) { + return "unnamed_" + System.currentTimeMillis(); + } + + // 使用 Hutool 去除首尾空格 + String sanitized = StrUtil.trim(fileName); + + // 替换非法字符(Linux/Windows 都不允许的字符) + sanitized = sanitized.replaceAll("[\\\\/:*?\"<>|]", "_"); + + // 去除连续的下划线 + sanitized = sanitized.replaceAll("_+", "_"); + + // 如果文件名过长,截断(Linux 文件名最大 255 字节) + byte[] bytes = sanitized.getBytes(StandardCharsets.UTF_8); + if (bytes.length > 200) { + String extension = ""; + int dotIndex = sanitized.lastIndexOf('.'); + if (dotIndex > 0 && dotIndex < sanitized.length() - 1) { + extension = sanitized.substring(dotIndex); + sanitized = sanitized.substring(0, dotIndex); + } + + // 重新计算长度 + bytes = sanitized.getBytes(StandardCharsets.UTF_8); + int maxNameLength = 200 - extension.getBytes(StandardCharsets.UTF_8).length; + + if (bytes.length > maxNameLength) { + // 安全截断,避免切断多字节字符 + sanitized = StrUtil.subPre(sanitized, maxNameLength); + } + sanitized = sanitized + extension; + } + + // 确保文件名不为空 + if (StrUtil.isBlank(sanitized) || ".".equals(sanitized)) { + return "file_" + System.currentTimeMillis(); + } + + return sanitized; + } + +// private static String saveFileToDir(InputStream is, File tempDir, String subFolder, String fileName) throws IOException { +// File folder = new File(tempDir, subFolder); +// if (!folder.exists()) { +// folder.mkdirs(); +// } +// +// File file = new File(folder, fileName); +// try (FileOutputStream fos = new FileOutputStream(file)) { +// byte[] buffer = new byte[4096]; +// int len; +// while ((len = is.read(buffer)) > 0) { +// fos.write(buffer, 0, len); +// } +// } +// return file.getAbsolutePath(); +// } + private static boolean isImageFile(String fileName) { return fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") || fileName.endsWith(".png") || fileName.endsWith(".gif") || diff --git a/backend/src/main/java/com/yfd/platform/env/domain/StationBasinInfo.java b/backend/src/main/java/com/yfd/platform/env/domain/StationBasinInfo.java new file mode 100644 index 0000000..2e348b3 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/env/domain/StationBasinInfo.java @@ -0,0 +1,11 @@ +package com.yfd.platform.env.domain; + +import lombok.Data; + +@Data +public class StationBasinInfo { + private String stcd; // 电站编码 + private String ennm; // 电站名称 + private String hbrvcd; // 流域编码 + private String hbrvnm; // 流域名称 +} diff --git a/backend/src/main/java/com/yfd/platform/env/mapper/SdEngInfoBHMapper.java b/backend/src/main/java/com/yfd/platform/env/mapper/SdEngInfoBHMapper.java index 0c6b51c..45b9e69 100644 --- a/backend/src/main/java/com/yfd/platform/env/mapper/SdEngInfoBHMapper.java +++ b/backend/src/main/java/com/yfd/platform/env/mapper/SdEngInfoBHMapper.java @@ -2,6 +2,7 @@ package com.yfd.platform.env.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.yfd.platform.env.domain.SdEngInfoBH; +import com.yfd.platform.env.domain.StationBasinInfo; import org.apache.ibatis.annotations.Param; import java.util.List; @@ -37,4 +38,7 @@ public interface SdEngInfoBHMapper extends BaseMapper { * 根据基地流域编码列表批量查询电站 */ List selectByHbrvcdList(@Param("hbrvcdList") List hbrvcdList); + + + List selectStationBasinInfo(); } diff --git a/backend/src/main/java/com/yfd/platform/env/service/impl/SdEngInfoBHServiceImpl.java b/backend/src/main/java/com/yfd/platform/env/service/impl/SdEngInfoBHServiceImpl.java index 9d68ba5..99f2ade 100644 --- a/backend/src/main/java/com/yfd/platform/env/service/impl/SdEngInfoBHServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/env/service/impl/SdEngInfoBHServiceImpl.java @@ -83,16 +83,19 @@ public class SdEngInfoBHServiceImpl extends ServiceImpl authorizedStations = getUserAuthorizedStationCodes(); if (authorizedStations != null && !authorizedStations.isEmpty()) { - wrapper.in(SdEngInfoBH::getStcd, authorizedStations); - } else if (!"admin".equals(SecurityUtils.getCurrentUsername())){ + List list = this.list(wrapper); + return list.stream() + .filter(item -> authorizedStations.contains(item.getStcd())) + .collect(Collectors.toList()); + }else{ return new ArrayList<>(); } - - return this.list(wrapper); } - @Override public Set getUserAuthorizedStationCodes() { String userId = SecurityUtils.getUserId(); diff --git a/backend/src/main/java/com/yfd/platform/env/service/impl/SdFpssBHServiceImpl.java b/backend/src/main/java/com/yfd/platform/env/service/impl/SdFpssBHServiceImpl.java index 036578e..d620898 100644 --- a/backend/src/main/java/com/yfd/platform/env/service/impl/SdFpssBHServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/env/service/impl/SdFpssBHServiceImpl.java @@ -67,28 +67,42 @@ public class SdFpssBHServiceImpl extends ServiceImpl i @Override public List selectForDropdown(String rstcd, String stnm, String baseId) { + // 管理员直接查询,无需权限过滤 + if ("admin".equals(SecurityUtils.getCurrentUsername())) { + return queryFpssList(rstcd, stnm, baseId); + } + + // 获取用户有权限的工程编码 Set authorizedStations = getUserAuthorizedStationCodes(); - List result; - - if (StringUtils.hasText(baseId)) { - result = baseMapper.selectForDropdownWithBaseId(rstcd, stnm, baseId); - } else { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(StringUtils.hasText(rstcd), SdFpssBH::getRstcd, rstcd) - .like(StringUtils.hasText(stnm), SdFpssBH::getStnm, stnm) - .orderByDesc(SdFpssBH::getOrderIndex); - result = list(wrapper); - } - if (authorizedStations.isEmpty()&&"admin".equals(SecurityUtils.getCurrentUsername())) { - return result; + // 无权限直接返回空列表 + if (authorizedStations == null || authorizedStations.isEmpty()) { + return new ArrayList<>(); } + // 查询数据 + List result = queryFpssList(rstcd, stnm, baseId); + + // 权限过滤 return result.stream() - .filter(fpss -> authorizedStations.contains(fpss.getRstcd())) + .filter(fpss -> fpss.getRstcd() != null && authorizedStations.contains(fpss.getRstcd())) .collect(Collectors.toList()); } + /** + * 查询过鱼设施列表(公共方法) + */ + private List queryFpssList(String rstcd, String stnm, String baseId) { + if (StringUtils.hasText(baseId)) { + return baseMapper.selectForDropdownWithBaseId(rstcd, stnm, baseId); + } else { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(StringUtils.hasText(rstcd), SdFpssBH::getRstcd, rstcd) + .like(StringUtils.hasText(stnm), SdFpssBH::getStnm, stnm) + .orderByDesc(SdFpssBH::getOrderIndex); + return list(wrapper); + } + } @Override public Set getUserAuthorizedStationCodes() { String userId = SecurityUtils.getUserId(); diff --git a/backend/src/main/java/com/yfd/platform/env/service/impl/SdHbrvDicServiceImpl.java b/backend/src/main/java/com/yfd/platform/env/service/impl/SdHbrvDicServiceImpl.java index da19175..90ea0e9 100644 --- a/backend/src/main/java/com/yfd/platform/env/service/impl/SdHbrvDicServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/env/service/impl/SdHbrvDicServiceImpl.java @@ -92,8 +92,16 @@ public class SdHbrvDicServiceImpl extends ServiceImpl selectForDropdown(String hbrvnm, String baseid) { - Set authorizedStations = getUserAuthorizedStationCodes(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(hbrvnm != null && !hbrvnm.isEmpty(), SdHbrvDic::getHbrvnm, hbrvnm) + .eq(baseid != null && !baseid.isEmpty(), SdHbrvDic::getBaseid, baseid) + .eq(SdHbrvDic::getEnabled, 1) + .orderByAsc(SdHbrvDic::getOrderIndex); + if("admin".equals(SecurityUtils.getCurrentUsername())){ + return this.list(wrapper); + } + Set authorizedStations = getUserAuthorizedStationCodes(); if (authorizedStations != null && !authorizedStations.isEmpty()) { List engInfos = engInfoBHMapper.selectList( new LambdaQueryWrapper() @@ -104,21 +112,19 @@ public class SdHbrvDicServiceImpl extends ServiceImpl id != null && !id.isEmpty()) .distinct() - .collect(Collectors.toList()); + .toList(); if (!hbrvcds.isEmpty()) { - wrapper.in(SdHbrvDic::getHbrvcd, hbrvcds); + List list = this.list(wrapper); + return list.stream() + .filter(hbrvDic -> hbrvcds.contains(hbrvDic.getHbrvcd())) + .collect(Collectors.toList()); } else { return new ArrayList<>(); } - }else if (!"admin".equals(SecurityUtils.getCurrentUsername())){ + }else { return new ArrayList<>(); } - wrapper.like(hbrvnm != null && !hbrvnm.isEmpty(), SdHbrvDic::getHbrvnm, hbrvnm) - .eq(baseid != null && !baseid.isEmpty(), SdHbrvDic::getBaseid, baseid) - .eq(SdHbrvDic::getEnabled, 1) - .orderByAsc(SdHbrvDic::getOrderIndex); - return this.list(wrapper); } @Override diff --git a/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java b/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java index 279f5d6..e318367 100644 --- a/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java +++ b/backend/src/main/java/com/yfd/platform/system/controller/SmsVerifyCodeController.java @@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -41,6 +42,7 @@ import java.util.stream.Collectors; @RestController @RequestMapping("/sms") @Tag(name = "短信验证码管理") +@Slf4j public class SmsVerifyCodeController { @Resource @@ -69,6 +71,7 @@ public class SmsVerifyCodeController { @Value("${rsa.private_key}") private String privateKey; + private static final String URGE_CONTENT ="根据生态环境部要求,请贵电站尽快完成过鱼数据的报送工作,感谢支持。"; /** * 发送验证码 @@ -124,6 +127,53 @@ public class SmsVerifyCodeController { } + /** + * 批量发送短信 + */ + @PostMapping("/batchSendContent") + @Operation(summary = "批量发送短信") + public ResponseResult batchSendContent(@RequestBody SmsVerifyCodeRequest smsVerifyCodeRequest) { + + List phoneList = smsVerifyCodeRequest.getPhoneList(); + if(phoneList==null){ + return ResponseResult.error("手机号不能为空"); + } + + smsVerifyCodeService.batchSendContent(phoneList, smsVerifyCodeRequest.getContent()); + return ResponseResult.success(); + } + + /** + * 批量催促 + */ + @PostMapping("/batchUrgeContent") + @Operation(summary = "催促") + public ResponseResult batchUrgeContent(@RequestBody SmsVerifyCodeRequest smsVerifyCodeRequest) { + List userIds = smsVerifyCodeRequest.getUserIds(); + String content = smsVerifyCodeRequest.getContent(); + + if ((userIds == null || userIds.isEmpty())) { + return ResponseResult.error("用户ID不能为空"); + } + + if (content == null || content.isEmpty()) { + content = URGE_CONTENT; + } + + try { + int successCount = smsVerifyCodeService.urgeByUserIds(userIds, content); + Map result = new HashMap<>(); +// result.put("successCount", successCount); + result.put("totalCount", userIds.size()); + return ResponseResult.successData(result); + } catch (Exception e) { + log.error("批量催促短信发送失败", e); + return ResponseResult.error("短信发送失败: " + e.getMessage()); + } + } + + + /** * 注册用户 */ @@ -204,7 +254,7 @@ public class SmsVerifyCodeController { selectedBasinCodes.addAll(Arrays.asList(hbrvcdCode.split(","))); } - Set addedStationCodes = new HashSet<>(); +// Set addedStationCodes = new HashSet<>(); for (String basinCode : selectedBasinCodes) { if (StringUtils.isEmpty(basinCode)) { @@ -241,7 +291,7 @@ public class SmsVerifyCodeController { scope.setStatus(1); scope.setPermissionType("READ"); sysUserDataScopeService.addDataScope(scope); - addedStationCodes.add(basinCode); +// addedStationCodes.add(basinCode); } else { Set stationsInBasinAndSelected = allStationCodesInBasin.stream() .filter(selectedStationCodes::contains) @@ -255,27 +305,27 @@ public class SmsVerifyCodeController { scope.setStatus(1); scope.setPermissionType("READ"); sysUserDataScopeService.addDataScope(scope); - addedStationCodes.add(stationCd); +// addedStationCodes.add(stationCd); } } } - Set standaloneStations = selectedStationCodes.stream() - .filter(code -> !addedStationCodes.contains(code)) - .collect(Collectors.toSet()); - - for (String stationCd : standaloneStations) { - if (StringUtils.isEmpty(stationCd)) { - continue; - } - SysUserDataScope scope = new SysUserDataScope(); - scope.setUserId(userId); - scope.setOrgType("STATION"); - scope.setOrgId(stationCd); - scope.setStatus(1); - scope.setPermissionType("READ"); - sysUserDataScopeService.addDataScope(scope); - } +// Set standaloneStations = selectedStationCodes.stream() +// .filter(code -> !addedStationCodes.contains(code)) +// .collect(Collectors.toSet()); +// +// for (String stationCd : standaloneStations) { +// if (StringUtils.isEmpty(stationCd)) { +// continue; +// } +// SysUserDataScope scope = new SysUserDataScope(); +// scope.setUserId(userId); +// scope.setOrgType("STATION"); +// scope.setOrgId(stationCd); +// scope.setStatus(1); +// scope.setPermissionType("READ"); +// sysUserDataScopeService.addDataScope(scope); +// } SysUser user = new SysUser(); user.setId(userId); userService.updateUserRoles( user,"c13481a486c9ee559cf305284df4d207"); diff --git a/backend/src/main/java/com/yfd/platform/system/domain/SmsVerifyCodeRequest.java b/backend/src/main/java/com/yfd/platform/system/domain/SmsVerifyCodeRequest.java index 70c3cb9..1b92d54 100644 --- a/backend/src/main/java/com/yfd/platform/system/domain/SmsVerifyCodeRequest.java +++ b/backend/src/main/java/com/yfd/platform/system/domain/SmsVerifyCodeRequest.java @@ -3,6 +3,8 @@ package com.yfd.platform.system.domain; import lombok.Data; +import java.util.List; + @Data public class SmsVerifyCodeRequest { @@ -61,5 +63,14 @@ public class SmsVerifyCodeRequest { */ private String stationCode; + private List phoneList; + + private String content; + + /** + * 用户编号 + */ + private List userIds; + } diff --git a/backend/src/main/java/com/yfd/platform/system/domain/SysUser.java b/backend/src/main/java/com/yfd/platform/system/domain/SysUser.java index e37f595..b23df27 100644 --- a/backend/src/main/java/com/yfd/platform/system/domain/SysUser.java +++ b/backend/src/main/java/com/yfd/platform/system/domain/SysUser.java @@ -163,4 +163,10 @@ public class SysUser implements Serializable { @TableField(exist = false) List roles; + + @TableField(exist = false) + private String basinNames; + + @TableField(exist = false) + private String stationNames; } diff --git a/backend/src/main/java/com/yfd/platform/system/mapper/SysRoleMapper.java b/backend/src/main/java/com/yfd/platform/system/mapper/SysRoleMapper.java index 09201a1..ea37488 100644 --- a/backend/src/main/java/com/yfd/platform/system/mapper/SysRoleMapper.java +++ b/backend/src/main/java/com/yfd/platform/system/mapper/SysRoleMapper.java @@ -63,6 +63,12 @@ public interface SysRoleMapper extends BaseMapper { ***********************************/ List getRoleByUserId(String id); + + /** + * 批量获取用户角色(含userId映射) + */ + List> getUserRolesByUserIds(@Param("userIds") List userIds); + /********************************** * 用途说明: 根据角色ID删除菜单与角色关联信息 * 参数说明 id 角色id diff --git a/backend/src/main/java/com/yfd/platform/system/service/ISmsVerifyCodeService.java b/backend/src/main/java/com/yfd/platform/system/service/ISmsVerifyCodeService.java index 95b3765..909a6ae 100644 --- a/backend/src/main/java/com/yfd/platform/system/service/ISmsVerifyCodeService.java +++ b/backend/src/main/java/com/yfd/platform/system/service/ISmsVerifyCodeService.java @@ -3,6 +3,8 @@ package com.yfd.platform.system.service; import com.baomidou.mybatisplus.extension.service.IService; import com.yfd.platform.system.domain.SmsVerifyCode; +import java.util.List; + /** *

* 短信验证码表 服务类 @@ -46,4 +48,7 @@ public interface ISmsVerifyCodeService extends IService { * @return 是否发送成功 */ boolean sendAuditNotify(String phone, String auditStatus, String reason); + + void batchSendContent(List phoneList, String content); + int urgeByUserIds(List userIds, String content); } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/system/service/impl/SmsVerifyCodeServiceImpl.java b/backend/src/main/java/com/yfd/platform/system/service/impl/SmsVerifyCodeServiceImpl.java index 6f25515..20f531d 100644 --- a/backend/src/main/java/com/yfd/platform/system/service/impl/SmsVerifyCodeServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/system/service/impl/SmsVerifyCodeServiceImpl.java @@ -1,17 +1,24 @@ package com.yfd.platform.system.service.impl; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yfd.platform.system.domain.SmsVerifyCode; +import com.yfd.platform.system.domain.SysUser; import com.yfd.platform.system.mapper.SmsVerifyCodeMapper; +import com.yfd.platform.system.mapper.SysUserMapper; import com.yfd.platform.system.service.ISmsVerifyCodeService; import com.yfd.platform.utils.SmsSender; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; +import java.util.List; import java.util.Random; +import java.util.regex.Pattern; +import java.util.stream.Collectors; /** *

@@ -19,14 +26,19 @@ import java.util.Random; *

*/ @Service +@Slf4j public class SmsVerifyCodeServiceImpl extends ServiceImpl implements ISmsVerifyCodeService { private static final int CODE_VALID_MINUTES = 5; private static final Random RANDOM = new Random(); + private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$"); @Resource private SmsSender smsSender; + @Resource + private SysUserMapper sysUserMapper; + @Override @Transactional(rollbackFor = Exception.class) public String sendVerifyCode(String phone, Integer type) { @@ -120,4 +132,69 @@ public class SmsVerifyCodeServiceImpl extends ServiceImpl phoneList, String content) { +// for (String phone : phoneList) { +// +// } + String phone = StrUtil.join(",", phoneList); + try { + smsSender.send(phone, content); + } catch (Exception e) { + log.debug("批量发送短信失败"+phone); + } + + } + + + @Override + public int urgeByUserIds(List userIds, String content) { + if (userIds == null || userIds.isEmpty()) { + log.warn("催促短信发送失败:用户ID列表为空"); + return 0; + } + List ids = userIds.stream().distinct().toList(); + List sysUsers = sysUserMapper.selectList(new LambdaQueryWrapper().in(SysUser::getId, ids) + .eq(SysUser::getRegStatus, "APPROVED").eq(SysUser::getStatus,1).select(SysUser::getPhone)); + + if (sysUsers.isEmpty()) { + log.warn("催促短信发送失败:未找到有效的手机号,userIds: {}", ids); + return 0; + } + List validPhones = sysUsers.stream().map(SysUser::getPhone).filter(this::isValidPhone).toList(); + + if (validPhones.isEmpty()) { + log.warn("催促短信发送失败:没有合规的手机号"); + return 0; + } + + int successCount = 0; +// for (String phone : validPhones) { +// +// } + String phone = StrUtil.join(",", validPhones); + try { + boolean sent = smsSender.send(phone, content); + if (sent) { + successCount++; + log.info("催促短信发送成功: {}", phone); + } else { + log.warn("催促短信发送失败: {}", phone); + } + } catch (Exception e) { + log.error("催促短信发送异常: {}", phone, e); + } + + + log.info("催促短信发送完成,总数: {}, 成功: {}", validPhones.size(), successCount); + return successCount; + } + + private boolean isValidPhone(String phone) { + if (StrUtil.isBlank(phone)) { + return false; + } + return PHONE_PATTERN.matcher(phone).matches(); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/system/service/impl/SysOrganizationServiceImpl.java b/backend/src/main/java/com/yfd/platform/system/service/impl/SysOrganizationServiceImpl.java index 4bc9925..bb956d3 100644 --- a/backend/src/main/java/com/yfd/platform/system/service/impl/SysOrganizationServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/system/service/impl/SysOrganizationServiceImpl.java @@ -14,6 +14,7 @@ import com.yfd.platform.system.mapper.SysRoleMapper; import com.yfd.platform.system.service.ISysOrganizationService; import com.yfd.platform.system.service.IUserService; import com.yfd.platform.utils.ObjectConverterUtil; +import com.yfd.platform.utils.SecurityUtils; import org.springframework.stereotype.Service; import jakarta.annotation.Resource; @@ -56,21 +57,13 @@ public class SysOrganizationServiceImpl extends ServiceImpl> getOrgTree(String parentid, String params) { - List orgList = new ArrayList<>(); - QueryWrapper queryWrapper = new QueryWrapper<>(); - //根据父级id查询 - queryWrapper.eq("parentid", parentid); - if (StrUtil.isNotEmpty(params)) { - queryWrapper.like("orgname", params); // 根据 部门名称 - } SysUser userInfo = userService.getUserInfo(); + + // 构建权限过滤条件 + Set allowedOrgIds = new HashSet<>(); if (userInfo.getUsertype() != 0) { - List roleByUserId = - sysRoleMapper.getRoleByUserId(userInfo.getId()); - List ids = new ArrayList<>(); - // 循环当前角色 + List roleByUserId = sysRoleMapper.getRoleByUserId(userInfo.getId()); for (SysRole sysRole : roleByUserId) { - // 获取角色的组织Id String orgscope = sysRole.getOrgscope(); if (StrUtil.isBlank(orgscope)) { continue; @@ -78,21 +71,33 @@ public class SysOrganizationServiceImpl extends ServiceImpl stringList = Arrays.asList(split); - Set set = new HashSet<>(); - if (stringList.size() > 0) { - List list = - sysOrganizationMapper.selectList(new LambdaQueryWrapper().in(SysOrganization::getId, stringList)); - list.forEach(l -> set.add(l.getParentid())); + if (!stringList.isEmpty()) { + allowedOrgIds.addAll(stringList); + // 查询这些组织的父级ID + List list = sysOrganizationMapper.selectList( + new LambdaQueryWrapper().in(SysOrganization::getId, stringList)); + for (SysOrganization org : list) { + if (org.getParentid() != null) { + allowedOrgIds.add(org.getParentid()); + } + } } - ids.addAll(stringList); - ids.addAll(set); } - queryWrapper.in("id", ids); } - orgList = this.list(queryWrapper.orderByAsc("orgcode")); - // 将实体对象转换为 Map,确保字段名与实体类一致 - // 将实体对象转换为 Map,确保字段名与实体类一致 - List> listMap = orgList.stream().map(org -> { + + // 查询所有组织数据 + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!allowedOrgIds.isEmpty()) { + queryWrapper.in("id", allowedOrgIds); + } + if (StrUtil.isNotEmpty(params)) { + queryWrapper.like("orgname", params); + } + + List allOrgList = this.list(queryWrapper.orderByAsc("orgcode")); + + // 将所有组织数据转换为Map结构 + List> allOrgMaps = allOrgList.stream().map(org -> { Map map = new HashMap<>(); map.put("id", org.getId()); map.put("orgtype", org.getOrgtype()); @@ -109,12 +114,39 @@ public class SysOrganizationServiceImpl extends ServiceImpl map : listMap) { - List> childList = child(map.get( - "id").toString());//查询下一子集 - map.put("childList", childList); //添加新列 子集 + + // 构建父子关系映射 + Map>> parentToChildrenMap = new HashMap<>(); + for (Map orgMap : allOrgMaps) { + String parentId = (String) orgMap.get("parentid"); + parentToChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(orgMap); + } + + // 过滤出指定父级ID的组织作为根节点 + List> rootOrgs = parentToChildrenMap.getOrDefault(parentid, new ArrayList<>()); + + // 构建完整的树形结构 + buildTreeStructure(rootOrgs, parentToChildrenMap); + + return rootOrgs; + } + + + /** + * 构建树形结构 + * @param orgList 当前层级的组织列表 + * @param parentToChildrenMap 父子关系映射 + */ + private void buildTreeStructure(List> orgList, + Map>> parentToChildrenMap) { + for (Map orgMap : orgList) { + String orgId = (String) orgMap.get("id"); + List> children = parentToChildrenMap.getOrDefault(orgId, new ArrayList<>()); + if (!children.isEmpty()) { + buildTreeStructure(children, parentToChildrenMap); // 递归构建子树 + } + orgMap.put("childList", children); } - return listMap; } /*********************************** @@ -202,35 +234,42 @@ public class SysOrganizationServiceImpl extends ServiceImpl getOrganizationById(String id, String orgName) { - - LambdaQueryWrapper queryWrapper = - new LambdaQueryWrapper<>(); SysUser userInfo = userService.getUserInfo(); + + // 收集所有允许的组织ID + Set allowedOrgIds = new HashSet<>(); if (userInfo.getUsertype() != 0) { - List roleByUserId = - sysRoleMapper.getRoleByUserId(userInfo.getId()); - List ids = new ArrayList<>(); - // 循环当前角色 + List roleByUserId = sysRoleMapper.getRoleByUserId(userInfo.getId()); for (SysRole sysRole : roleByUserId) { - // 获取角色的组织Id String orgscope = sysRole.getOrgscope(); if (StrUtil.isBlank(orgscope)) { continue; } - // 拆分组织Id + // 拆分组织Id并添加到集合中(自动去重) String[] split = orgscope.split(","); - List stringList = Arrays.asList(split); - ids.addAll(stringList); + for (String orgId : split) { + if (StrUtil.isNotBlank(orgId)) { + allowedOrgIds.add(orgId.trim()); + } + } } - if (ObjectUtil.isNotEmpty(ids)) { - queryWrapper.in(SysOrganization::getId, ids); - } - } + + // 构建查询条件 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysOrganization::getParentid, id); + if (StrUtil.isNotBlank(orgName)) { queryWrapper.like(SysOrganization::getOrgname, orgName); } - queryWrapper.eq(SysOrganization::getParentid, id).orderByDesc(SysOrganization::getOrgcode); + + // 如果有权限限制,添加IN条件 + if (!allowedOrgIds.isEmpty()) { + queryWrapper.in(SysOrganization::getId, allowedOrgIds); + } + + queryWrapper.orderByDesc(SysOrganization::getOrgcode); + return this.list(queryWrapper); } @@ -243,20 +282,34 @@ public class SysOrganizationServiceImpl extends ServiceImpl> getOrgScopeTree(String roleId) { - LambdaQueryWrapper queryWrapper = - new LambdaQueryWrapper<>(); + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if(!"admin".equals(SecurityUtils.getCurrentUsername())){ + String userId = SecurityUtils.getUserId(); + List roles = sysRoleMapper.getRoleByUserId(userId); + List ids = new ArrayList<>(); + for (SysRole role : roles) { + String orgscope = role.getOrgscope(); + if(StrUtil.isNotBlank(orgscope)){ + ids.addAll(Arrays.asList(orgscope.split(","))); + } + } + if(ids.isEmpty()){ + return new ArrayList<>(); + } + queryWrapper.in(SysOrganization::getId, ids); + } queryWrapper.eq(SysOrganization::getIsvaild, '1'); queryWrapper.orderByAsc(SysOrganization::getOrgcode); - List> listMaps = this.listMaps(queryWrapper); + List> mapList = this.listMaps(queryWrapper); + List> listMaps = ObjectConverterUtil.convertMapFieldsToEntityFormat(SysOrganization.class, mapList); // 获取当前角色 SysRole sysRole = sysRoleMapper.selectById(roleId); String orgscope = sysRole.getOrgscope(); List ids = new ArrayList<>(); if (StrUtil.isNotBlank(orgscope)) { - String[] split = orgscope.split(","); - ids = Arrays.asList(split); + ids.addAll(Arrays.asList(orgscope.split(","))); } - for (Map map : listMaps) { String id = (String) map.get("id"); if (ids.contains(id)) { diff --git a/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java b/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java index e2bc0c9..31aa4ea 100644 --- a/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/system/service/impl/UserServiceImpl.java @@ -10,6 +10,13 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.data.domain.SysUserDataScope; +import com.yfd.platform.data.mapper.SysUserDataScopeMapper; +import com.yfd.platform.env.domain.SdEngInfoBH; +import com.yfd.platform.env.domain.SdHbrvDic; +import com.yfd.platform.env.domain.StationBasinInfo; +import com.yfd.platform.env.mapper.SdEngInfoBHMapper; +import com.yfd.platform.env.mapper.SdHbrvDicMapper; import com.yfd.platform.system.domain.LoginUser; import com.yfd.platform.system.domain.SysRole; import com.yfd.platform.system.domain.SysUser; @@ -55,12 +62,21 @@ public class UserServiceImpl extends ServiceImpl impleme @Resource private PasswordEncoder passwordEncoder; + + @Resource + private SdHbrvDicMapper sdHbrvDicMapper; + + @Resource + private SdEngInfoBHMapper sdEngInfoBHMapper; /** * 文件空间配置 */ @Resource private FileSpaceProperties fileSpaceProperties; + @Resource + private SysUserDataScopeMapper sysUserDataScopeMapper; + /********************************** * 用途说明:获取当前用户账号及名称 * 参数说明 @@ -224,7 +240,6 @@ public class UserServiceImpl extends ServiceImpl impleme * 返回值说明: 是否更新成功 ************************************/ // ... existing code ... - @Override @Transactional(rollbackFor = Exception.class) public Map updateById(SysUser sysUser, String roleids) { @@ -309,7 +324,8 @@ public class UserServiceImpl extends ServiceImpl impleme /** * 处理用户角色分配(增量更新) - * @param userId 用户 ID + * + * @param userId 用户 ID * @param roleIds 角色 ID 字符串(逗号分隔) */ private void handleUserRoles(String userId, String roleIds) { @@ -430,9 +446,9 @@ public class UserServiceImpl extends ServiceImpl impleme //根据当前用户id 查询角色表的级别 currentUser.getUser() 获取当前用户id String level = sysUserMapper.getMaxLevel(id); //判断是否获取级别 - if ("admin".equals(SecurityUtils.getCurrentUsername())||StrUtil.isNotEmpty(level)) { + if ("admin".equals(SecurityUtils.getCurrentUsername()) || StrUtil.isNotEmpty(level)) { //判断当前用户级别 管理员及以上权限 - if ("admin".equals(SecurityUtils.getCurrentUsername())||Integer.parseInt(level) <= 2) { + if ("admin".equals(SecurityUtils.getCurrentUsername()) || Integer.parseInt(level) <= 2) { SysUser sysUser = sysUserMapper.selectById(id); UpdateWrapper updateWrapper = new UpdateWrapper<>(); String password = PasswordGenerator.generateRandomPassword(sysUser.getUsername()); @@ -465,7 +481,7 @@ public class UserServiceImpl extends ServiceImpl impleme //根据当前用户id 查询角色表的级别 currentUser.getUser() 获取当前用户id String level = sysUserMapper.getMaxLevel(id); //判断当前用户级别 管理员及以上权限 - if ("admin".equals(SecurityUtils.getCurrentUsername())||Integer.parseInt(level) <= 2) { + if ("admin".equals(SecurityUtils.getCurrentUsername()) || Integer.parseInt(level) <= 2) { UpdateWrapper updateWrapper = new UpdateWrapper<>(); //根据id修改用户状态,最近修改人,最近修改时间 updateWrapper.eq("id", id).set("status", status).set( @@ -547,8 +563,8 @@ public class UserServiceImpl extends ServiceImpl impleme @Override public Page queryUsers(String orgid, - String username, - Page page) { + String username, + Page page) { Page mapPage = sysUserMapper.queryUsers(orgid, username, page); mapPage.getRecords().forEach(record -> { @@ -632,10 +648,10 @@ public class UserServiceImpl extends ServiceImpl impleme } @Override - public Page queryPendingAuditUsers(Page page,String name,String regStatus) { + public Page queryPendingAuditUsers(Page page, String name, String regStatus) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.in(SysUser::getRegStatus, "PENDING","APPROVED","REJECTED"); - queryWrapper.eq(ObjectUtil.isNotEmpty(regStatus),SysUser::getRegStatus, regStatus); + queryWrapper.in(SysUser::getRegStatus, "PENDING", "APPROVED", "REJECTED"); + queryWrapper.eq(ObjectUtil.isNotEmpty(regStatus), SysUser::getRegStatus, regStatus); queryWrapper.and(StrUtil.isNotBlank(name), wrapper -> wrapper.like(SysUser::getNickname, name) .or() @@ -643,11 +659,85 @@ public class UserServiceImpl extends ServiceImpl impleme ); queryWrapper.orderByDesc(SysUser::getRegTime); Page mapPage = this.page(page, queryWrapper); - mapPage.getRecords().forEach(record -> { - String id = record.getId(); - List sysRoles = sysRoleMapper.getRoleByUserId(id); - record.setRoles(sysRoles); - }); + + List records = mapPage.getRecords(); + if (records == null || records.isEmpty()) { + return mapPage; + } + + List userIds = records.stream() + .map(SysUser::getId) + .collect(Collectors.toList()); + + Map> userRoleMap = new HashMap<>(); + List> userRoles = sysRoleMapper.getUserRolesByUserIds(userIds); + if (userRoles != null) { + for (Map row : userRoles) { + String userId = (String) row.get("userId"); + SysRole role = new SysRole(); + role.setId((String) row.get("id")); + role.setRolecode((String) row.get("rolecode")); + role.setRolename((String) row.get("rolename")); + role.setLevel((String) row.get("level")); + role.setDescription((String) row.get("description")); + role.setIsvaild((String) row.get("isvaild")); + role.setOrgscope((String) row.get("orgscope")); + role.setOptscope((String) row.get("optscope")); + role.setBusscope((String) row.get("busscope")); + userRoleMap.computeIfAbsent(userId, k -> new ArrayList<>()).add(role); + } + } + + Map> userScopeMap = new HashMap<>(); + List allScopes = sysUserDataScopeMapper.selectValidPermissionsWithNameByUserIds(userIds); + if (allScopes != null) { + for (SysUserDataScope scope : allScopes) { + userScopeMap.computeIfAbsent(scope.getUserId(), k -> new ArrayList<>()).add(scope); + } + } + List stationBasinInfos = sdEngInfoBHMapper.selectStationBasinInfo(); + for (SysUser record : records) { + String userId = record.getId(); + + List roles = userRoleMap.getOrDefault(userId, Collections.emptyList()); + record.setRoles(roles); + + List scopes = userScopeMap.getOrDefault(userId, Collections.emptyList()); + Set basinNameSet = new LinkedHashSet<>(); + Set stationNameSet = new LinkedHashSet<>(); + + for (SysUserDataScope scope : scopes) { + String orgType = scope.getOrgType(); + String orgId = scope.getOrgId(); + String orgName = scope.getOrgName(); + + if ("HBRVCD".equals(orgType)) { + if (orgName != null) { + basinNameSet.add(orgName); + } + if (orgId != null) { + if (stationBasinInfos != null) { + List stcdList = stationBasinInfos.stream().filter(info -> info.getHbrvcd().equals(orgId)).map(StationBasinInfo::getEnnm).toList(); + stationNameSet.addAll(stcdList); + } + } + } else if ("STATION".equals(orgType)) { + if (orgName != null) { + stationNameSet.add(orgName); + } + if (orgId != null) { + if (stationBasinInfos != null) { + String hbrbcd = stationBasinInfos.stream().filter(info -> info.getStcd().equals(orgId)).map(StationBasinInfo::getHbrvnm).findFirst().orElse(null); + basinNameSet.add(hbrbcd); + } + } + } + } + + record.setBasinNames(basinNameSet.isEmpty() ? null : String.join(",", basinNameSet)); + record.setStationNames(stationNameSet.isEmpty() ? null : String.join(",", stationNameSet)); + } + return mapPage; } diff --git a/backend/src/main/java/com/yfd/platform/utils/FileUtil.java b/backend/src/main/java/com/yfd/platform/utils/FileUtil.java index 6940aee..3261a3d 100644 --- a/backend/src/main/java/com/yfd/platform/utils/FileUtil.java +++ b/backend/src/main/java/com/yfd/platform/utils/FileUtil.java @@ -391,6 +391,42 @@ public class FileUtil extends cn.hutool.core.io.FileUtil { fis.close(); } + /** + * 判断文件名是否为图片类型 + */ + public static boolean isImageFileName(String fileName) { + if (fileName == null || fileName.isEmpty()) { + return false; + } + + String lowerName = fileName.toLowerCase(); + return lowerName.endsWith(".jpg") + || lowerName.endsWith(".jpeg") + || lowerName.endsWith(".png") + || lowerName.endsWith(".gif") + || lowerName.endsWith(".bmp") + || lowerName.endsWith(".webp") + || lowerName.endsWith(".svg"); + } + + /** + * 判断文件名是否为视频类型 + */ + public static boolean isVideoFileName(String fileName) { + if (fileName == null || fileName.isEmpty()) { + return false; + } + + String lowerName = fileName.toLowerCase(); + return lowerName.endsWith(".mp4") + || lowerName.endsWith(".avi") + || lowerName.endsWith(".mov") + || lowerName.endsWith(".wmv") + || lowerName.endsWith(".flv") + || lowerName.endsWith(".mkv"); + } + + public static String getMd5(File file) { return getMd5(getByte(file)); } diff --git a/backend/src/main/java/com/yfd/platform/utils/MultipartStreamParser.java b/backend/src/main/java/com/yfd/platform/utils/MultipartStreamParser.java new file mode 100644 index 0000000..62876a9 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/utils/MultipartStreamParser.java @@ -0,0 +1,334 @@ +package com.yfd.platform.utils; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * 流式 multipart 文件上传解析器。 + *

+ * 绕过 Spring 的 {@code MultipartResolver}(它会先把整个请求体解析完才调用 Controller), + * 直接从 {@link HttpServletRequest#getInputStream()} 边读边写目标文件, + * 80MB+ 的大文件也能在 Controller 方法内立即开始接收。 + *

+ * 前提:{@code spring.servlet.multipart.resolve-lazily=true} + */ +@Slf4j +public final class MultipartStreamParser { + + private static final int BUFFER_SIZE = 8192; + + private MultipartStreamParser() { + } + + /** + * 从 multipart/form-data 请求中流式提取第一个文件并写入目标目录。 + * + * @param request HTTP 请求(Content-Type: multipart/form-data) + * @param targetDir 目标目录(文件将保存为 upload.zip) + * @return 解析结果(原始文件名、目标文件、文件大小) + */ + public static StreamedFile streamToFile(HttpServletRequest request, Path targetDir) throws IOException { + long t0 = System.currentTimeMillis(); + + String contentType = request.getContentType(); + if (contentType == null || !contentType.startsWith("multipart/form-data")) { + throw new IOException("请求不是 multipart/form-data 类型"); + } + + String boundary = extractBoundary(contentType); + byte[] closingDelim = ("\r\n--" + boundary + "--").getBytes(StandardCharsets.ISO_8859_1); + + InputStream rawInput = new BufferedInputStream(request.getInputStream(), BUFFER_SIZE); + + long t1 = System.currentTimeMillis(); + String originalFileName = readPartHeaders(rawInput, boundary); + long t2 = System.currentTimeMillis(); + + Files.createDirectories(targetDir); + File targetFile = new File(targetDir.toFile(), "upload.zip"); + long fileSize = streamBinaryToFile(rawInput, targetFile, closingDelim); + long t3 = System.currentTimeMillis(); + + log.info("流式接收完成: {} -> {} ({} bytes), 头解析{}ms, 体写入{}ms, 总耗时{}ms", + originalFileName, targetFile.getAbsolutePath(), fileSize, + t2 - t1, t3 - t2, t3 - t0); + return new StreamedFile(originalFileName, targetFile, fileSize); + } + + // ==================== 阶段1:读 part 头 ==================== + + /** + * 跳过第一个 boundary 行,读取该 part 的 HTTP 头,从中提取 filename。 + */ + private static String readPartHeaders(InputStream in, String boundary) throws IOException { + String firstBoundary = "--" + boundary; + String fileName = null; + + // 跳过第一个 boundary 行 + String line = readLine(in); + if (line == null || !line.equals(firstBoundary)) { + // 可能前面有空行或 boundary 带了 \r + if (line != null && line.startsWith(firstBoundary)) { + // ok + } else { + throw new IOException("未找到 multipart boundary,收到: " + line); + } + } + + // 读取 part 头,直到空行 + while ((line = readLine(in)) != null) { + if (line.isEmpty()) { + break; // 空行 = 头结束,接下来是二进制数据 + } + if (line.startsWith("Content-Disposition") || line.startsWith("content-disposition")) { + fileName = extractFileName(line); + } + } + + if (fileName == null) { + fileName = "unknown.zip"; + } + return fileName; + } + + /** + * 从 Content-Disposition 头中提取 filename 值。 + * 格式:form-data; name="file"; filename="xxx.zip" + */ + static String extractFileName(String headerLine) { + for (String part : headerLine.split(";")) { + part = part.trim(); + if (part.startsWith("filename")) { + int eq = part.indexOf('='); + if (eq >= 0) { + String val = part.substring(eq + 1).trim(); + if (val.startsWith("\"") && val.endsWith("\"")) { + val = val.substring(1, val.length() - 1); + } + + // 先进行 URL 解码(处理 %E4%B8%AD%E6%96%87 这种编码) + try { + val = java.net.URLDecoder.decode(val, StandardCharsets.UTF_8.name()); + } catch (Exception e) { + log.debug("URL解码失败,使用原始值: {}", val); + } + + // 再进行字符编码转换(处理 GBK/UTF-8 乱码) + String decodedFileName = decodeFileName(val); + return decodedFileName; + } + } + } + return null; + } + + /** + * 尝试修复乱码的文件名,支持 GBK 和 UTF-8 编码 + */ + private static String decodeFileName(String rawFileName) { + if (rawFileName == null || rawFileName.isEmpty()) { + return rawFileName; + } + + try { + byte[] rawBytes = rawFileName.getBytes(StandardCharsets.ISO_8859_1); + + // 优先尝试 UTF-8 解码(现代浏览器常用) + String utf8Decoded = new String(rawBytes, StandardCharsets.UTF_8); + if (isValidChinese(utf8Decoded)) { + return utf8Decoded; + } + + // 再尝试 GBK 解码(Windows/旧版浏览器常用) + String gbkDecoded = new String(rawBytes, "GBK"); + if (isValidChinese(gbkDecoded)) { + return gbkDecoded; + } + + // 都不包含中文,返回 UTF-8 解码结果 + return utf8Decoded; + } catch (Exception e) { + log.warn("文件名解码失败,使用原始值: {}", rawFileName, e); + return rawFileName; + } + } + + /** + * 简单判断字符串是否包含有效的中文字符 + */ + private static boolean isValidChinese(String str) { + if (str == null || str.isEmpty()) { + return false; + } + for (char c : str.toCharArray()) { + // 检查是否在中文 Unicode 范围内 + if (c >= '\u4e00' && c <= '\u9fa5') { + return true; + } + // 检查中文标点符号范围 + if (c >= '\u3000' && c <= '\u303f') { + return true; + } + } + return false; + } + + // ==================== 阶段2:流式写二进制 ==================== + + /** + * 从输入流读取二进制数据写入目标文件,遇到 closingDelim 时停止。 + *

+ * 使用重叠窗口策略:每次读取一个 chunk,保留末尾 (delimLen-1) 字节 + * 与下一个 chunk 拼接,防止 boundary 跨 chunk 被截断。 + */ + private static long streamBinaryToFile(InputStream in, File targetFile, byte[] closingDelim) throws IOException { + int delimLen = closingDelim.length; + int overlap = delimLen - 1; + + byte[] buf = new byte[BUFFER_SIZE]; + byte[] carry = new byte[overlap]; // 上一轮末尾的重叠字节 + int carryLen = 0; + long totalWritten = 0; + + try (OutputStream out = new BufferedOutputStream(new FileOutputStream(targetFile), BUFFER_SIZE)) { + + while (true) { + int n = in.read(buf); + if (n == -1) { + // 流结束,写出剩余的重叠字节 + if (carryLen > 0) { + out.write(carry, 0, carryLen); + totalWritten += carryLen; + } + break; + } + + // 将上一轮的重叠字节 + 本轮数据拼接,搜索 closing boundary + byte[] searchBuf = new byte[carryLen + n]; + if (carryLen > 0) { + System.arraycopy(carry, 0, searchBuf, 0, carryLen); + } + System.arraycopy(buf, 0, searchBuf, carryLen, n); + + int boundaryPos = indexOf(searchBuf, closingDelim); + if (boundaryPos >= 0) { + // 找到 closing boundary,写出之前的数据 + if (boundaryPos > 0) { + out.write(searchBuf, 0, boundaryPos); + totalWritten += boundaryPos; + } + break; + } + + // 未找到 boundary:写出安全部分(保留末尾 overlap 字节) + int safeLen = searchBuf.length - overlap; + if (safeLen > 0) { + out.write(searchBuf, 0, safeLen); + totalWritten += safeLen; + // 保留末尾 overlap 字节到 carry + System.arraycopy(searchBuf, safeLen, carry, 0, overlap); + carryLen = overlap; + } else { + // 数据量不足 overlap,全部保留 + System.arraycopy(searchBuf, 0, carry, 0, searchBuf.length); + carryLen = searchBuf.length; + } + } + } + + return totalWritten; + } + + // ==================== 工具方法 ==================== + + /** + * 从 Content-Type 头中提取 boundary 值。 + * 格式:multipart/form-data; boundary=----WebKitFormBoundary... + */ + static String extractBoundary(String contentType) { + for (String part : contentType.split(";")) { + part = part.trim(); + if (part.startsWith("boundary=")) { + return part.substring("boundary=".length()); + } + } + throw new IllegalArgumentException("无法从 Content-Type 提取 boundary: " + contentType); + } + + /** + * 按行读取(以 \r\n 或 \n 结尾),返回行内容(不含换行符)。 + */ + private static String readLine(InputStream in) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(256); + int prev = -1; + int b; + while ((b = in.read()) != -1) { + if (b == '\n') { + // 去掉末尾的 \r + byte[] bytes = bos.toByteArray(); + int len = bytes.length; + if (len > 0 && bytes[len - 1] == '\r') { + len--; + } + return new String(bytes, 0, len, StandardCharsets.ISO_8859_1); + } + bos.write(b); + prev = b; + } + // 流结束 + if (bos.size() > 0) { + return bos.toString(StandardCharsets.ISO_8859_1); + } + return null; + } + + /** + * 在 byte 数组中查找子数组的位置(Boyer-Moore 简化版)。 + */ + private static int indexOf(byte[] haystack, byte[] needle) { + if (needle.length == 0) return 0; + if (haystack.length < needle.length) return -1; + + outer: + for (int i = 0; i <= haystack.length - needle.length; i++) { + for (int j = 0; j < needle.length; j++) { + if (haystack[i + j] != needle[j]) { + continue outer; + } + } + return i; + } + return -1; + } + + // ==================== 结果 DTO ==================== + + public static class StreamedFile { + private final String originalFileName; + private final File targetFile; + private final long fileSize; + + StreamedFile(String originalFileName, File targetFile, long fileSize) { + this.originalFileName = originalFileName; + this.targetFile = targetFile; + this.fileSize = fileSize; + } + + public String getOriginalFileName() { + return originalFileName; + } + + public File getTargetFile() { + return targetFile; + } + + public long getFileSize() { + return fileSize; + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 1df2467..0b53e07 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -24,8 +24,11 @@ spring: matching-strategy: ant_path_matcher servlet: multipart: - max-file-size: 30MB - max-request-size: 100MB + max-file-size: 300MB + max-request-size: 500MB + file-size-threshold: 1KB + location: /tmp/upload + resolve-lazily: true logging: file: diff --git a/backend/src/main/resources/application-devtw.yml b/backend/src/main/resources/application-devtw.yml index 64b040c..5f7dcb3 100644 --- a/backend/src/main/resources/application-devtw.yml +++ b/backend/src/main/resources/application-devtw.yml @@ -1,5 +1,8 @@ server: port: 8093 + tomcat: + connection-timeout: 300000 + max-swallow-size: 500MB spring: #应用名称 @@ -13,11 +16,94 @@ spring: url: "${DB_MASTER_URL:jdbc:oracle:thin:@172.16.21.134:1521/SDLYZ}" username: "${DB_MASTER_USERNAME:QGC_REFA}" password: "${DB_MASTER_PASSWORD:Y4M4K1oCkL8U}" + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 30000 + async-init: true + keep-alive-between-time-millis: 120000 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 180000 + max-evictable-idle-time-millis: 300000 + phy-timeout-millis: 25200000 + validation-query: SELECT 1 FROM DUAL + validation-query-timeout: 3 + test-while-idle: true + test-on-borrow: false + test-on-return: false + keep-alive: true + remove-abandoned: true + remove-abandoned-timeout: 1800 + log-abandoned: true + break-after-acquire-failure: true + time-between-connect-error-millis: 300000 + pool-prepared-statements: true + max-open-prepared-statements: 100 + max-pool-prepared-statement-per-connection-size: 100 + connection-properties: oracle.net.CONNECT_TIMEOUT=10000;oracle.jdbc.ReadTimeout=60000;oracle.net.READ_TIMEOUT=60000 slave: driverClassName: oracle.jdbc.OracleDriver url: "${DB_SLAVE_URL:jdbc:oracle:thin:@172.16.21.134:1521/SDLYZ}" username: "${DB_SLAVE_USERNAME:QGC_REFA}" password: "${DB_SLAVE_PASSWORD:Y4M4K1oCkL8U}" + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 30000 + async-init: true + keep-alive-between-time-millis: 120000 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 180000 + max-evictable-idle-time-millis: 300000 + phy-timeout-millis: 25200000 + validation-query: SELECT 1 FROM DUAL + validation-query-timeout: 3 + test-while-idle: true + test-on-borrow: false + test-on-return: false + keep-alive: true + remove-abandoned: true + remove-abandoned-timeout: 1800 + log-abandoned: true + break-after-acquire-failure: true + time-between-connect-error-millis: 300000 + pool-prepared-statements: true + max-open-prepared-statements: 100 + max-pool-prepared-statement-per-connection-size: 100 + connection-properties: oracle.net.CONNECT_TIMEOUT=10000;oracle.jdbc.ReadTimeout=60000;oracle.net.READ_TIMEOUT=60000 + filter: + stat: + enabled: true + log-slow-sql: true + slow-sql-millis: 3000 + merge-sql: true + slf4j: + enabled: true + wall: + enabled: true + log-violation: true + throw-exception: true + config: + select-where-alway-true-check: true + select-having-alway-true-check: true + delete-where-alway-true-check: true + update-where-alay-true-check: true + update-where-alway-true-check: true + update-where-none-check: true + multi-statement-allow: false + web-stat-filter: + enabled: true + url-pattern: /* + exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*' + session-stat-enable: true + principal-session-name: admin + profile-enable: true + stat-view-servlet: + enabled: true + url-pattern: /druid/* + login-username: admin + login-password: admin + reset-enable: false jackson: date-format: yyyy-MM-dd HH:mm:ss @@ -29,6 +115,9 @@ spring: multipart: max-file-size: 300MB max-request-size: 500MB + file-size-threshold: 1KB + location: /tmp/upload + resolve-lazily: true logging: file: @@ -122,6 +211,6 @@ attachment: token: ${ATTACHMENT_TOKEN:qgcBkod25ngBa4wu8BtfCPYsJ7lQGVDoexH} upload-url: ${ATTACHMENT_UPLOAD_URL:http://172.16.31.185:18200/upload} video-url: ${ATTACHMENT_VIDEO_URL:http://172.16.31.185:18200/upload} - delete-url: ${ATTACHMENT_DELETE_URL:http://172.16.31.185:18200/delete} + delete-url: ${ATTACHMENT_DELETE_URL:http://172.16.31.185:18200/FileDelete} # upload-url: ${ATTACHMENT_UPLOAD_URL:https://211.99.26.225:12125/upload} # video-url: ${ATTACHMENT_VIDEO_URL:https://211.99.26.225:12125/upload} \ No newline at end of file diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 392b634..8520bc6 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -1,6 +1,8 @@ server: port: 8093 - + tomcat: + connection-timeout: 300000 + max-swallow-size: 500MB spring: #应用名称 application: @@ -13,12 +15,94 @@ spring: url: "${DB_MASTER_URL:jdbc:oracle:thin:@172.16.31.190:1521/SDLYZ}" username: "${DB_MASTER_USERNAME:QGC_REFA}" password: "${DB_MASTER_PASSWORD:Y4M4K1oCkL8U}" + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 30000 + async-init: true + keep-alive-between-time-millis: 120000 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 180000 + max-evictable-idle-time-millis: 300000 + phy-timeout-millis: 25200000 + validation-query: SELECT 1 FROM DUAL + validation-query-timeout: 3 + test-while-idle: true + test-on-borrow: false + test-on-return: false + keep-alive: true + remove-abandoned: true + remove-abandoned-timeout: 1800 + log-abandoned: true + break-after-acquire-failure: true + time-between-connect-error-millis: 300000 + pool-prepared-statements: true + max-open-prepared-statements: 100 + max-pool-prepared-statement-per-connection-size: 100 + connection-properties: oracle.net.CONNECT_TIMEOUT=10000;oracle.jdbc.ReadTimeout=60000;oracle.net.READ_TIMEOUT=60000 slave: driverClassName: oracle.jdbc.OracleDriver url: "${DB_SLAVE_URL:jdbc:oracle:thin:@172.16.31.190:1521/SDLYZ}" username: "${DB_SLAVE_USERNAME:QGC_REFA}" password: "${DB_SLAVE_PASSWORD:Y4M4K1oCkL8U}" - + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 30000 + async-init: true + keep-alive-between-time-millis: 120000 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 180000 + max-evictable-idle-time-millis: 300000 + phy-timeout-millis: 25200000 + validation-query: SELECT 1 FROM DUAL + validation-query-timeout: 3 + test-while-idle: true + test-on-borrow: false + test-on-return: false + keep-alive: true + remove-abandoned: true + remove-abandoned-timeout: 1800 + log-abandoned: true + break-after-acquire-failure: true + time-between-connect-error-millis: 300000 + pool-prepared-statements: true + max-open-prepared-statements: 100 + max-pool-prepared-statement-per-connection-size: 100 + connection-properties: oracle.net.CONNECT_TIMEOUT=10000;oracle.jdbc.ReadTimeout=60000;oracle.net.READ_TIMEOUT=60000 + filter: + stat: + enabled: true + log-slow-sql: true + slow-sql-millis: 3000 + merge-sql: true + slf4j: + enabled: true + wall: + enabled: true + log-violation: true + throw-exception: true + config: + select-where-alway-true-check: true + select-having-alway-true-check: true + delete-where-alway-true-check: true + update-where-alay-true-check: true + update-where-alway-true-check: true + update-where-none-check: true + multi-statement-allow: false + web-stat-filter: + enabled: true + url-pattern: /* + exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*' + session-stat-enable: true + principal-session-name: admin + profile-enable: true + stat-view-servlet: + enabled: true + url-pattern: /druid/* + login-username: admin + login-password: admin + reset-enable: false jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 @@ -29,7 +113,9 @@ spring: multipart: max-file-size: 300MB max-request-size: 500MB - + file-size-threshold: 1KB + location: /tmp/upload + resolve-lazily: true logging: file: name: logs/platform-dev.log diff --git a/backend/src/main/resources/application-server.yml b/backend/src/main/resources/application-server.yml index b4e0057..6d5f3aa 100644 --- a/backend/src/main/resources/application-server.yml +++ b/backend/src/main/resources/application-server.yml @@ -1,5 +1,5 @@ server: - port: 8090 + port: 8093 spring: #应用名称 @@ -9,44 +9,120 @@ spring: type: com.alibaba.druid.pool.DruidDataSource druid: master: - driverClassName: com.mysql.cj.jdbc.Driver - url: "${DB_MASTER_URL:jdbc:mysql://43.138.168.68:3306/frameworkdb2025?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true}" - username: "${DB_MASTER_USERNAME:root}" - password: "${DB_MASTER_PASSWORD:}" + driverClassName: oracle.jdbc.OracleDriver + url: "${DB_MASTER_URL:jdbc:oracle:thin:@172.16.21.134:1521/SDLYZ}" + username: "${DB_MASTER_USERNAME:QGC_REFA}" + password: "${DB_MASTER_PASSWORD:Y4M4K1oCkL8U}" + slave: + driverClassName: oracle.jdbc.OracleDriver + url: "${DB_SLAVE_URL:jdbc:oracle:thin:@172.16.21.134:1521/SDLYZ}" + username: "${DB_SLAVE_USERNAME:QGC_REFA}" + password: "${DB_SLAVE_PASSWORD:Y4M4K1oCkL8U}" + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 mvc: pathmatch: matching-strategy: ant_path_matcher servlet: multipart: - max-file-size: 30MB - max-request-size: 100MB + max-file-size: 300MB + max-request-size: 500MB + file-size-threshold: 1KB + location: /tmp/upload + resolve-lazily: true logging: file: - name: logs/projectname.log - + name: logs/platform-dev.log level: - com.genersoft.iot: debug - com.genersoft.iot.vmp.storager.dao: info - com.genersoft.iot.vmp.gb28181: info + root: info + com.yfd.platform: info +# com.yfd.platform.*.mapper: trace # 在线文档: swagger-ui(生产环境建议关闭) swagger-ui: - enabled: false + enabled: true -file-space: #项目文档空间 - files: D:\demoproject\files\ #单独上传的文件附件 - useravatar: D:\demoproject\useravatar\ #用户头像 - system: D:\demoproject\system\ #系统文档根目录,用于头像等静态资源 +mybatis-plus: + # mapper-locations: classpath*:**/mapper/*Mapper.xml,classpath*:**/mapping/*Mapper.xml + global-config: + banner: false + db-config: + id-type: ASSIGN_ID + insert-strategy: not_null + update-strategy: not_null + select-strategy: not_empty + table-underline: true + logic-delete-value: 1 + logic-not-delete-value: 0 + logic-delete-field: isDeleted + configuration: + map-underscore-to-camel-case: true + cache-enabled: false + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + + +# 登录相关配置 +login: + # 登录缓存 + cache-enable: true + # 是否限制单用户登录 + single-login: false + # 验证码 + login-code: + # 验证码类型配置 查看 LoginProperties 类 + code-type: arithmetic # 启动自动数据库初始化(仅 dev/server): app: + # ZIP导入临时目录配置 + zip-import: + temp-dir: ${ZIP_IMPORT_TEMP_DIR:/qgc-platform/tmp/zip_import_temp} init: - enabled: true + enabled: false schema: classpath:db-init/sql/min-schema.sql # data 文件可选;为避免复杂 dump 解析问题,先不导入 # data: marker-table: sys_user marker-version: v1.0.0 + # 登录图形验证码有效时间/分钟 + expiration: 2 + # 验证码高度 + width: 111 + # 验证码宽度 + heigth: 36 + # 内容长度 + length: 2 + # 字体名称,为空则使用默认字体 + font-name: + # 字体大小 + font-size: 25 +# IP 本地解析 +ip: + local-parsing: true + + +file-space: #项目文档空间 + files: /qgc-platform/files/ #单独上传的文件附件 + system: /qgc-platform/system/ #单独上传的文件 + +task: + pool: + # 核心线程池大小 + core-pool-size: 10 + # 最大线程数 + max-pool-size: 30 + # 活跃时间 + keep-alive-seconds: 60 + # 队列容量 + queue-capacity: 50 + +attachment: + token: ${ATTACHMENT_TOKEN:qgcBkod25ngBa4wu8BtfCPYsJ7lQGVDoexH} + upload-url: ${ATTACHMENT_UPLOAD_URL:http://172.16.31.185:18200/upload} + video-url: ${ATTACHMENT_VIDEO_URL:http://172.16.31.185:18200/upload} + delete-url: ${ATTACHMENT_DELETE_URL:http://172.16.31.185:18200/delete} \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index d8e6c7e..03a78ea 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -38,5 +38,5 @@ springdoc: swagger-ui: enabled: true path: /swagger-ui.html - packages-to-scan: com.yfd.platform +# packages-to-scan: com.yfd.platform diff --git a/backend/src/main/resources/mapper/data/FishStatisticsMapper.xml b/backend/src/main/resources/mapper/data/FishStatisticsMapper.xml new file mode 100644 index 0000000..d652a58 --- /dev/null +++ b/backend/src/main/resources/mapper/data/FishStatisticsMapper.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + WITH + user_scope_detail AS ( + SELECT DISTINCT + su.ID AS USER_ID, + e.STCD AS STATION_CODE, + h.HBRVCD AS BASIN_CODE, + h.HBRVNM AS BASIN_NAME, + e.ENNM AS STATION_NAME + FROM QGC_REFA.SYS_USER su + JOIN QGC_REFA.SYS_USER_DATA_SCOPE sud + ON su.ID = sud.USER_ID + AND sud.STATUS = 1 + AND sud.ORG_TYPE = 'HBRVCD' + JOIN QGC_REFA.SD_HBRV_DIC h + ON sud.ORG_ID = h.HBRVCD + AND h.ENABLED = 1 + AND h.IS_DELETED = 0 + JOIN QGC_REFA.SD_ENGINFO_B_H e + ON e.HBRVCD = h.HBRVCD + AND e.USFL = 1 + WHERE su.STATUS = 1 + AND su.REG_STATUS IN ('PENDING', 'APPROVED', 'REJECTED') + + UNION + + SELECT DISTINCT + su.ID AS USER_ID, + e.STCD AS STATION_CODE, + h.HBRVCD AS BASIN_CODE, + h.HBRVNM AS BASIN_NAME, + e.ENNM AS STATION_NAME + FROM QGC_REFA.SYS_USER su + JOIN QGC_REFA.SYS_USER_DATA_SCOPE sud + ON su.ID = sud.USER_ID + AND sud.STATUS = 1 + AND sud.ORG_TYPE = 'STATION' + JOIN QGC_REFA.SD_ENGINFO_B_H e + ON sud.ORG_ID = e.STCD + AND e.USFL = 1 + JOIN QGC_REFA.SD_HBRV_DIC h + ON e.HBRVCD = h.HBRVCD + AND h.ENABLED = 1 + AND h.IS_DELETED = 0 + WHERE su.STATUS = 1 + AND su.REG_STATUS IN ('PENDING', 'APPROVED', 'REJECTED') + ), + + filtered_users AS ( + SELECT DISTINCT USER_ID + FROM user_scope_detail + + + AND BASIN_CODE = #{basinCode} + + + AND STATION_CODE = #{stationCode} + + + ), + + distinct_basins AS ( + SELECT DISTINCT USER_ID,BASIN_CODE, BASIN_NAME + FROM user_scope_detail + WHERE USER_ID IN (SELECT USER_ID FROM filtered_users) + ), + + distinct_stations AS ( + SELECT DISTINCT USER_ID, STATION_CODE, STATION_NAME + FROM user_scope_detail + WHERE USER_ID IN (SELECT USER_ID FROM filtered_users) + ), + + basin_agg AS ( + SELECT + USER_ID, + RTRIM( + XMLAGG(XMLELEMENT(E, BASIN_NAME || ',') ORDER BY BASIN_NAME) + .EXTRACT('//text()').GETCLOBVAL(), + ',' + ) AS BASIN_NAMES, + MIN(BASIN_CODE) AS MIN_BASIN_CODE -- 辅助排序字段(VARCHAR2) + FROM distinct_basins + GROUP BY USER_ID + ), + + station_agg AS ( + SELECT + USER_ID, + RTRIM( + XMLAGG(XMLELEMENT(E, STATION_NAME || ',') ORDER BY STATION_NAME) + .EXTRACT('//text()').GETCLOBVAL(), + ',' + ) AS STATION_NAMES, + MIN(STATION_CODE) AS MIN_STATION_CODE -- 辅助排序字段(VARCHAR2) + FROM distinct_stations + GROUP BY USER_ID + ), + + fish_monthly AS ( + SELECT + CREATED_BY, + TO_CHAR(STRDT, 'YYYY-MM') AS REPORT_MONTH, + MIN(STRDT) AS MIN_STRDT, + MAX(STRDT) AS MAX_ENDDT, + SUM(FCNT) AS TOTAL_FCNT + FROM QGC_REFA.FISH_DRAFT_DATA + WHERE DELETED_FLAG = 0 + AND STATUS IN ('PENDING','APPROVED') + GROUP BY CREATED_BY, TO_CHAR(STRDT, 'YYYY-MM') + ), + + all_users AS ( + SELECT USER_ID FROM filtered_users + ) + + SELECT + u.USER_ID, + su.REAL_NAME, + su.PHONE, + su.NICKNAME || '/' || su.PHONE AS CONTACT, + ba.MIN_BASIN_CODE, + sa.MIN_STATION_CODE, + ba.BASIN_NAMES, + sa.STATION_NAMES, + fm.REPORT_MONTH, + fm.MIN_STRDT, + fm.MAX_ENDDT, + fm.TOTAL_FCNT, + CASE WHEN fm.CREATED_BY IS NOT NULL THEN 1 ELSE 0 END AS HAS_DATA + FROM all_users u + JOIN QGC_REFA.SYS_USER su + ON u.USER_ID = su.ID + LEFT JOIN basin_agg ba + ON u.USER_ID = ba.USER_ID + LEFT JOIN station_agg sa + ON u.USER_ID = sa.USER_ID + LEFT JOIN fish_monthly fm + ON u.USER_ID = fm.CREATED_BY + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/main/resources/mapper/data/ImportTaskRowMapper.xml b/backend/src/main/resources/mapper/data/ImportTaskRowMapper.xml new file mode 100644 index 0000000..2de26c3 --- /dev/null +++ b/backend/src/main/resources/mapper/data/ImportTaskRowMapper.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DELETE FROM IMPORT_TASK_ROW WHERE TASK_ID = #{taskId} + + + \ No newline at end of file diff --git a/backend/src/main/resources/mapper/data/SysUserDataScopeMapper.xml b/backend/src/main/resources/mapper/data/SysUserDataScopeMapper.xml index 1fb8aa6..572746d 100644 --- a/backend/src/main/resources/mapper/data/SysUserDataScopeMapper.xml +++ b/backend/src/main/resources/mapper/data/SysUserDataScopeMapper.xml @@ -72,4 +72,67 @@ ORDER BY CREATED_AT DESC + + + + diff --git a/backend/src/main/resources/mapper/env/SdEngInfoBHMapper.xml b/backend/src/main/resources/mapper/env/SdEngInfoBHMapper.xml index bee8c09..a01627c 100644 --- a/backend/src/main/resources/mapper/env/SdEngInfoBHMapper.xml +++ b/backend/src/main/resources/mapper/env/SdEngInfoBHMapper.xml @@ -231,6 +231,23 @@ + + + + + + + + + + + + + delete from sys_role_users where userid !=(select u.id from sys_user u where u.account="admin") and roleid=#{roleid} and userid=#{urserid}