diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java index 57116aa..750e9cb 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/controller/admin/project/ProjectRequirementController.java @@ -76,6 +76,13 @@ public class ProjectRequirementController { return success(requirementService.getRequirementTree(pageReqVO)); } + @GetMapping("/execution-options") + @Operation(summary = "获取执行可关联的项目需求下拉选项") + public CommonResult> getExecutionRequirementOptions( + @Valid ProjectRequirementPageReqVO pageReqVO) { + return success(requirementService.getExecutionRequirementOptions(pageReqVO)); + } + @PostMapping("/change-status") @Operation(summary = "变更需求状态") public CommonResult changeRequirementStatus(@Valid @RequestBody ProjectRequirementStatusActionReqVO reqVO) { diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementMapper.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementMapper.java index 0e15379..049f59e 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementMapper.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/dal/mysql/project/ProjectRequirementMapper.java @@ -22,20 +22,28 @@ public interface ProjectRequirementMapper extends BaseMapperX selectPage(ProjectRequirementPageReqVO reqVO) { - return selectPage(reqVO, buildQueryWrapper(reqVO)); + return selectPage(reqVO, buildQueryWrapper(reqVO, null)); + } + + /** + * 分页查询需求列表,并排除指定状态集合。 + */ + default PageResult selectPage(ProjectRequirementPageReqVO reqVO, Collection excludeStatusCodes) { + return selectPage(reqVO, buildQueryWrapper(reqVO, excludeStatusCodes)); } /** * 查询所有符合条件的需求列表,用于树查询 */ default List selectList(ProjectRequirementPageReqVO reqVO) { - return selectList(buildQueryWrapper(reqVO)); + return selectList(buildQueryWrapper(reqVO, null)); } /** * 构建查询条件 */ - private LambdaQueryWrapperX buildQueryWrapper(ProjectRequirementPageReqVO reqVO) { + private LambdaQueryWrapperX buildQueryWrapper(ProjectRequirementPageReqVO reqVO, + Collection excludeStatusCodes) { LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); if (StringUtils.hasText(reqVO.getTitle())) { queryWrapper.like(ProjectRequirementDO::getTitle, reqVO.getTitle()); @@ -49,8 +57,11 @@ public interface ProjectRequirementMapper extends BaseMapperX getRequirementTree(ProjectRequirementPageReqVO pageReqVO); + PageResult getExecutionRequirementOptions(ProjectRequirementPageReqVO pageReqVO); + void changeRequirementStatus(ProjectRequirementStatusActionReqVO reqVO); void changeRequirementStatusForReview(ProjectRequirementStatusActionReqVO reqVO); diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java index 4d3fc84..5e7d387 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/project/ProjectRequirementServiceImpl.java @@ -255,21 +255,9 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService @Override @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#pageReqVO.projectId", accessible = true) public PageResult getRequirementPage(ProjectRequirementPageReqVO pageReqVO) { - if (pageReqVO.getModuleId() != null) { - if (isAllRequirementsModule(pageReqVO.getModuleId())) { - pageReqVO.setModuleId(null); - } else { - List moduleIds = getAllModuleIdsWithChildren(pageReqVO.getModuleId(), pageReqVO.getProjectId()); - pageReqVO.setModuleIds(moduleIds); - pageReqVO.setModuleId(null); - } - } + normalizePageModuleFilter(pageReqVO); PageResult pageResult = requirementMapper.selectPage(pageReqVO); - Map statusModelMap = getStatusModelMap(); - List list = pageResult.getList().stream() - .map(requirement -> buildRequirementRespVO(requirement, statusModelMap)) - .collect(Collectors.toList()); - return new PageResult<>(list, pageResult.getTotal()); + return toRequirementRespPage(pageResult); } @Override @@ -325,6 +313,16 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService return new PageResult<>(list, (long) total); } + @Override + @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#pageReqVO.projectId", accessible = true) + public PageResult getExecutionRequirementOptions(ProjectRequirementPageReqVO pageReqVO) { + normalizePageModuleFilter(pageReqVO); + Set excludedStatusCodes = new HashSet<>(getTerminalStatusCodes()); + excludedStatusCodes.add(STATUS_REVIEW_REJECTED); + PageResult pageResult = requirementMapper.selectPage(pageReqVO, excludedStatusCodes); + return toRequirementRespPage(pageResult); + } + @Override @Transactional(rollbackFor = Exception.class) @CheckObjectPermission(objectType = PROJECT_OBJECT_TYPE, objectId = "#reqVO.projectId", @@ -749,6 +747,27 @@ public class ProjectRequirementServiceImpl implements ProjectRequirementService return childrenMap; } + private void normalizePageModuleFilter(ProjectRequirementPageReqVO pageReqVO) { + if (pageReqVO.getModuleId() == null) { + return; + } + if (isAllRequirementsModule(pageReqVO.getModuleId())) { + pageReqVO.setModuleId(null); + return; + } + List moduleIds = getAllModuleIdsWithChildren(pageReqVO.getModuleId(), pageReqVO.getProjectId()); + pageReqVO.setModuleIds(moduleIds); + pageReqVO.setModuleId(null); + } + + private PageResult toRequirementRespPage(PageResult pageResult) { + Map statusModelMap = getStatusModelMap(); + List list = pageResult.getList().stream() + .map(requirement -> buildRequirementRespVO(requirement, statusModelMap)) + .collect(Collectors.toList()); + return new PageResult<>(list, pageResult.getTotal()); + } + /** * 一次性加载当前对象类型下的状态模型,供列表和树查询复用。 */ diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java index 4da0e93..6176446 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/controller/admin/file/FileController.java @@ -131,6 +131,13 @@ public class FileController { // https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1432/ path = URLUtil.decode(path, StandardCharsets.UTF_8, false); + FileDO file = fileService.getFileByConfigIdAndPath(configId, path); + if (file == null) { + log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path); + response.setStatus(HttpStatus.NOT_FOUND.value()); + return; + } + // 读取内容 byte[] content = fileService.getFileContent(configId, path); if (content == null) { @@ -138,7 +145,7 @@ public class FileController { response.setStatus(HttpStatus.NOT_FOUND.value()); return; } - writeAttachment(response, path, content); + writeAttachment(response, StrUtil.blankToDefault(file.getName(), file.getPath()), content); } @GetMapping("/page") diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/client/s3/S3FileClient.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/client/s3/S3FileClient.java index 3efffd1..9683c34 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/client/s3/S3FileClient.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/framework/file/core/client/s3/S3FileClient.java @@ -136,11 +136,11 @@ public class S3FileClient extends AbstractFileClient { } private String resolveObjectKey(String urlOrPath) { - String cleaned = HttpUtils.removeUrlQuery(urlOrPath); - if (!HttpUtil.isHttp(cleaned) && !HttpUtil.isHttps(cleaned)) { - return cleaned; + if (!HttpUtil.isHttp(urlOrPath) && !HttpUtil.isHttps(urlOrPath)) { + return urlOrPath; } + String cleaned = HttpUtils.removeUrlQuery(urlOrPath); String path = StrUtil.removePrefix(cleaned, config.getDomain() + "/"); if (StrUtil.equals(path, cleaned)) { URI uri = URI.create(cleaned); diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileService.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileService.java index a1493a8..a64e841 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileService.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileService.java @@ -63,6 +63,15 @@ public interface FileService { Long createFile(FileCreateReqVO createReqVO); FileDO getFile(Long id); + /** + * 按 (configId, path) 获取文件记录。 + * + * @param configId 文件配置编号 + * @param path 文件存储路径 + * @return 文件记录,不存在返回 null + */ + FileDO getFileByConfigIdAndPath(Long configId, String path); + /** * 通过文件 URL 获得文件。 * diff --git a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileServiceImpl.java b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileServiceImpl.java index a9ebc94..8432844 100644 --- a/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileServiceImpl.java +++ b/rdms-system/rdms-system-boot/src/main/java/com/njcn/rdms/module/system/service/file/FileServiceImpl.java @@ -3,6 +3,7 @@ package com.njcn.rdms.module.system.service.file; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import com.google.common.annotations.VisibleForTesting; @@ -42,14 +43,6 @@ public class FileServiceImpl implements FileService { * 目的:按照日期,进行分目录 */ static boolean PATH_PREFIX_DATE_ENABLE = true; - /** - * 上传文件的后缀,是否包含时间戳 - *

- * 目的:保证文件的唯一性,避免覆盖 - * 定制:可按需调整成 UUID、或者其他方式 - */ - static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true; - @Resource private FileConfigService fileConfigService; @@ -103,34 +96,27 @@ public class FileServiceImpl implements FileService { @VisibleForTesting String generateUploadPath(String name, String directory) { - // 1. 生成前缀、后缀 + // 1. 生成前缀,并将存储文件名切换为安全 key: + // path 仅承担对象存储 key / 路径职责,不再复用原文件名,避免 %、&、中文等字符进入 URI 路径。 String prefix = null; if (PATH_PREFIX_DATE_ENABLE) { prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN); } - String suffix = null; - if (PATH_SUFFIX_TIMESTAMP_ENABLE) { - suffix = String.valueOf(System.currentTimeMillis()); + String ext = FileUtil.extName(name); + String storageName = IdUtil.fastSimpleUUID(); + if (StrUtil.isNotEmpty(ext)) { + storageName = storageName + StrUtil.DOT + ext; } - // 2.1 先拼接 suffix 后缀 - if (StrUtil.isNotEmpty(suffix)) { - String ext = FileUtil.extName(name); - if (StrUtil.isNotEmpty(ext)) { - name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext; - } else { - name = name + StrUtil.C_UNDERLINE + suffix; - } - } - // 2.2 再拼接 prefix 前缀 + // 2. 先拼接 prefix 前缀 if (StrUtil.isNotEmpty(prefix)) { - name = prefix + StrUtil.SLASH + name; + storageName = prefix + StrUtil.SLASH + storageName; } - // 2.3 最后拼接 directory 目录 + // 3. 最后拼接 directory 目录 if (StrUtil.isNotEmpty(directory)) { - name = directory + StrUtil.SLASH + name; + storageName = directory + StrUtil.SLASH + storageName; } - return name; + return storageName; } @Override @@ -166,6 +152,14 @@ public class FileServiceImpl implements FileService { return validateFileExists(id); } + @Override + public FileDO getFileByConfigIdAndPath(Long configId, String path) { + if (configId == null || StrUtil.isBlank(path)) { + return null; + } + return fileMapper.selectByConfigIdAndPath(configId, path); + } + @Override public FileDO getFileByUrl(String url) { if (StrUtil.isBlank(url)) {