From 513ae81160c5e65cb63f0d642cd5a18172bd81c8 Mon Sep 17 00:00:00 2001 From: aikai Date: Fri, 27 Jun 2025 14:44:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(infra):=20=E6=B7=BB=E5=8A=A0=20MinIO?= =?UTF-8?q?=E9=A2=84=E7=AD=BE=E5=90=8D=E4=B8=8A=E4=BC=A0=E5=92=8C=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=8A=9F=E8=83=BD-=20=E6=96=B0=E5=A2=9E=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=A2=84=E7=AD=BE=E5=90=8D=E4=B8=8A=E4=BC=A0=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E5=92=8C=E9=A2=84=E7=AD=BE=E5=90=8D=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=20URL=20=E7=9A=84=E6=8E=A5=E5=8F=A3-=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=92=8C=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E7=9A=84=E5=90=8E=E7=AB=AF=E9=80=BB=E8=BE=91=20-=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20MinIO=20=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9=20-=E4=BC=98=E5=8C=96=E5=AD=98=E5=82=A8=E6=A1=B6?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E4=B8=AA=E5=AD=98=E5=82=A8=E6=A1=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oa/vo/workorder/BpmOAWorkOrderRespVO.java | 5 +- .../service/oa/BpmOAWorkOrderServiceImpl.java | 5 +- .../controller/admin/file/FileController.java | 67 +++++++- .../infra/service/minio/MinioService.java | 158 ++++++++++++++++-- .../src/main/resources/application-dev.yaml | 9 + 5 files changed, 217 insertions(+), 27 deletions(-) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/workorder/BpmOAWorkOrderRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/workorder/BpmOAWorkOrderRespVO.java index 1badcdb7..9fa9906e 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/workorder/BpmOAWorkOrderRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/workorder/BpmOAWorkOrderRespVO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.workorder; +import cn.iocoder.yudao.framework.common.pojo.UploadUserFile; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -77,7 +78,7 @@ public class BpmOAWorkOrderRespVO { private String processInstanceId; @Schema(description = "附件文件列表") - private List fileItems; + private List fileItems; @Schema(description = "工单跟踪记录") private List trackInfo; @@ -88,4 +89,4 @@ public class BpmOAWorkOrderRespVO { @Schema(description = "更新时间", example = "2024-12-19 15:30:00") private LocalDateTime updateTime; -} \ No newline at end of file +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOAWorkOrderServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOAWorkOrderServiceImpl.java index 67f5fc80..949e807b 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOAWorkOrderServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOAWorkOrderServiceImpl.java @@ -30,6 +30,7 @@ import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -83,6 +84,7 @@ public class BpmOAWorkOrderServiceImpl extends BpmOABaseService implements BpmOA // 获取当前登录用户信息 AdminUserRespDTO userRespDTO = userApi.getUser(userId).getCheckedData(); + List list = createReqVO.getFileItems(); // 创建工单DO BpmOAWorkOrderDO workOrder = BpmOAWorkOrderDO.builder() .title(createReqVO.getTitle()) @@ -90,6 +92,7 @@ public class BpmOAWorkOrderServiceImpl extends BpmOABaseService implements BpmOA .level(createReqVO.getLevel()) .content(createReqVO.getContent()) .expectedTime(createReqVO.getExpectedTime()) + .fileItems(new ArrayList<>(list)) .fromUserId(userId) .fromDeptId(userRespDTO.getDeptId()) .status(1) // 默认状态:待分配 @@ -332,7 +335,7 @@ public class BpmOAWorkOrderServiceImpl extends BpmOABaseService implements BpmOA respVO.setProcessInstanceId(workOrder.getProcessInstanceId()); respVO.setCreateTime(workOrder.getCreateTime()); respVO.setUpdateTime(workOrder.getUpdateTime()); - + respVO.setFileItems(workOrder.getFileItems()); // 设置类型名称 respVO.setTypeName(getWorkOrderTypeName(workOrder.getType())); diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java index 61440f91..52b73965 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -13,6 +13,7 @@ import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.*; import cn.iocoder.yudao.module.infra.dal.dataobject.file.*; import cn.iocoder.yudao.module.infra.service.file.FileService; +import cn.iocoder.yudao.module.infra.service.minio.MinioService; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.PostApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; @@ -39,6 +40,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.time.LocalDate; +import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @@ -66,12 +68,15 @@ public class FileController { @Resource private SecurityProperties securityProperties; + @Resource + private MinioService minioService; + @PostMapping("/uploadBpmFileProcessInstanceId") @Operation(summary = "更新文件的流程实例ID") @OperateLog(logArgs = false) // 上传文件,没有记录操作日志的必要 public String uploadBpmFileProcessInstanceId(@Valid @RequestBody BpmFileUploadReqVO reqVO) throws Exception { - fileService.uploadBpmFileProcessInstanceId(reqVO); - return "success" ; + fileService.uploadBpmFileProcessInstanceId(reqVO); + return "success"; } @PostMapping("/bpmUpload") @@ -102,7 +107,7 @@ public class FileController { @GetMapping("/{configId}/get/**") @PermitAll @Operation(summary = "下载文件") - @Parameter(name = "configId", description = "配置编号", required = true) + @Parameter(name = "configId", description = "配置编号", required = true) public void getFileContent(HttpServletRequest request, HttpServletResponse response, @PathVariable("configId") Long configId) throws Exception { @@ -146,8 +151,8 @@ public class FileController { @Operation(summary = "上传业务类型附件【如:工作日/周报附件】") @OperateLog(logArgs = false) // 上传文件,没有记录操作日志的必要 @PermitAll - public CommonResult businessUpload(@RequestParam("uploadFiles") MultipartFile file,@RequestParam("businessType")Long businessType) throws Exception { - return success(fileService.createBusinessReturnFile(file,businessType)); + public CommonResult businessUpload(@RequestParam("uploadFiles") MultipartFile file, @RequestParam("businessType") Long businessType) throws Exception { + return success(fileService.createBusinessReturnFile(file, businessType)); } @DeleteMapping("/deleteBusinessFile") @@ -164,7 +169,7 @@ public class FileController { @OperateLog(logArgs = false) // 上传文件,没有记录操作日志的必要 public String uploadBusinessFileProcessInstanceId(@Valid @RequestBody BusinessFileUploadReqVO reqVO) throws Exception { fileService.uploadBusinessFileProcessInstanceId(reqVO); - return "success" ; + return "success"; } //add by yj 2024 04-11 End @@ -194,7 +199,6 @@ public class FileController { @RequestParam(value = "postId", required = false) Long postId) { - // 查询当前部门编号下 是否存在小程序码 QRCodeDO qrCodeDO = fileService.getQRCode(deptId, null); @@ -213,7 +217,7 @@ public class FileController { return error(OA_QRCODE_ERROR); } - }else { // 存在的时候, 判断是否已存在相同参数的小程序码 + } else { // 存在的时候, 判断是否已存在相同参数的小程序码 // 查询是否存在 参数一致的小程序码 qrCodeDO = fileService.getQRCode(deptId, scene.toString()); @@ -226,7 +230,7 @@ public class FileController { return error(OA_QRCODE_ERROR); } - }else { + } else { fileService.updateQRCodeFile(qrCodeDO.getId()); } } @@ -296,4 +300,49 @@ public class FileController { // 上传小程序码 获得url return fileService.updateQRCodeFile(id, deptId, QRCode.getName(), content); } + + @GetMapping("/presigned-url") + @Operation(summary = "生成预签名上传凭证") + public CommonResult> generatePresignedUrl( + @RequestParam String fileName, + @RequestParam String fileType) { + try { + Map credentials = minioService.generatePresignedUploadCredentials(fileName, fileType); + return success(credentials); + } catch (Exception e) { + log.error("生成预签名上传凭证失败", e); + return CommonResult.error(500, "生成预签名上传凭证失败: " + e.getMessage()); + } + } + + @GetMapping("/presigned-download-url") + @Operation(summary = "获取MinIO预签名下载URL") + @PermitAll + public CommonResult> generatePresignedDownloadUrl( + @RequestParam("objectName") String objectName) { + try { + // 参数验证 + if (objectName == null || objectName.trim().isEmpty()) { + return CommonResult.error(400, "参数错误:objectName不能为空"); + } + + // 调用MinioService生成预签名下载URL + Map result = minioService.generatePresignedDownloadUrl(objectName); + return success(result); + + } catch (RuntimeException e) { + String errorMessage = e.getMessage(); + if (errorMessage != null && errorMessage.contains("文件不存在")) { + log.warn("文件不存在: {}", objectName); + return CommonResult.error(404, "文件不存在"); + } else { + log.error("生成预签名下载URL失败", e); + return CommonResult.error(500, "生成预签名下载URL失败: " + errorMessage); + } + } catch (Exception e) { + log.error("生成预签名下载URL失败", e); + return CommonResult.error(500, "生成预签名下载URL失败: " + e.getMessage()); + } + } + } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/minio/MinioService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/minio/MinioService.java index 2c6948ff..db0473c3 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/minio/MinioService.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/minio/MinioService.java @@ -1,11 +1,17 @@ package cn.iocoder.yudao.module.infra.service.minio; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.module.infra.config.MinioConfigProperties; import io.minio.BucketExistsArgs; +import io.minio.GetPresignedObjectUrlArgs; import io.minio.MakeBucketArgs; import io.minio.MinioClient; +import io.minio.PostPolicy; import io.minio.PutObjectArgs; import io.minio.RemoveObjectArgs; +import io.minio.StatObjectArgs; +import io.minio.StatObjectResponse; +import io.minio.http.Method; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -13,8 +19,12 @@ import org.springframework.web.multipart.MultipartFile; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.io.InputStream; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; /** @@ -43,22 +53,26 @@ public class MinioService { .build(); // 检查桶是否存在,不存在则创建 - boolean bucketExists = minioClient.bucketExists( - BucketExistsArgs.builder() - .bucket(minioConfigProperties.getBucketName()) - .build() - ); + String[] bucketsToCreate = {minioConfigProperties.getBucketName(), "user-uploads"}; - if (!bucketExists) { - minioClient.makeBucket( - MakeBucketArgs.builder() - .bucket(minioConfigProperties.getBucketName()) + for (String bucketName : bucketsToCreate) { + boolean bucketExists = minioClient.bucketExists( + BucketExistsArgs.builder() + .bucket(bucketName) .build() ); - log.info("创建存储桶: {}", minioConfigProperties.getBucketName()); + + if (!bucketExists) { + minioClient.makeBucket( + MakeBucketArgs.builder() + .bucket(bucketName) + .build() + ); + log.info("创建存储桶: {}", bucketName); + } } - log.info("MinIO 客户端初始化成功. Endpoint: {}, Bucket: {}", + log.info("MinIO 客户端初始化成功. Endpoint: {}, Bucket: {}", minioConfigProperties.getEndpoint(), minioConfigProperties.getBucketName()); } catch (Exception e) { log.error("MinIO 客户端初始化失败", e); @@ -100,9 +114,9 @@ public class MinioService { ); // 生成文件访问URL - String fileUrl = minioConfigProperties.getEndpoint() + "/" + + String fileUrl = minioConfigProperties.getEndpoint() + "/" + minioConfigProperties.getBucketName() + "/" + path; - + log.info("文件上传成功: {}", fileUrl); return fileUrl; } catch (Exception e) { @@ -123,7 +137,7 @@ public class MinioService { } List fileUrls = new ArrayList<>(); - + for (MultipartFile file : files) { if (!file.isEmpty()) { String fileUrl = uploadFile(file, null); @@ -155,4 +169,118 @@ public class MinioService { } } -} \ No newline at end of file + /** + * 生成预签名上传凭证 + * + * @param fileName 文件名 + * @param fileType 文件类型 + * @return 预签名上传表单数据 + */ + public Map generatePresignedUploadCredentials(String fileName, String fileType) { + try { + // 1. 安全验证 - 从SecurityContext获取当前用户 + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (userId == null) { + throw new IllegalStateException("用户未登录"); + } + + // 2. 生成唯一对象名 + String sanitizedFileName = sanitizeFilename(fileName); + String objectName = String.format("user_%d/%s_%s", userId, UUID.randomUUID(), sanitizedFileName); + + // 3. 创建PostPolicy + // 使用固定的 user-uploads bucket 用于用户上传 + String bucketName = "user-uploads"; + PostPolicy policy = new PostPolicy(bucketName, ZonedDateTime.now().plusHours(1)); + + // 设置对象名 + policy.addEqualsCondition("key", objectName); + + // 添加文件大小限制:最小1字节,最大10GB + policy.addContentLengthRangeCondition(1, 10L * 1024 * 1024 * 1024); + + // 添加Content-Type条件 + policy.addEqualsCondition("Content-Type", fileType); + + // 4. 调用MinIO客户端获取预签名表单数据 + Map formData = minioClient.getPresignedPostFormData(policy); + + // 5. 构建返回结构 + Map result = new java.util.HashMap<>(); + result.put("url", minioConfigProperties.getEndpoint() + "/" + bucketName); + result.put("fields", formData); + result.put("objectName", objectName); + + log.info("生成预签名上传凭证成功,用户ID: {}, 对象名: {}", userId, objectName); + return result; + + } catch (Exception e) { + log.error("生成预签名上传凭证失败", e); + throw new RuntimeException("生成预签名上传凭证失败: " + e.getMessage(), e); + } + } + + /** + * 文件名安全处理 + * + * @param fileName 原文件名 + * @return 安全的文件名 + */ + private String sanitizeFilename(String fileName) { + if (fileName == null || fileName.trim().isEmpty()) { + return "unknown"; + } + return fileName.replaceAll("[^a-zA-Z0-9.-]", "_"); + } + + /** + * 生成预签名下载URL + * + * @param objectName 对象名称 + * @return 包含下载URL等信息的Map + */ + public Map generatePresignedDownloadUrl(String objectName) { + try { + // 1. 参数验证 + if (objectName == null || objectName.trim().isEmpty()) { + throw new IllegalArgumentException("对象名称不能为空"); + } + // 3. 验证文件是否存在 + String bucketName = "user-uploads"; + try { + StatObjectResponse stat = minioClient.statObject( + StatObjectArgs.builder() + .bucket(bucketName) + .object(objectName) + .build() + ); + log.debug("文件存在检查通过: {}, 大小: {}", objectName, stat.size()); + } catch (Exception e) { + log.warn("文件不存在: {}", objectName); + throw new RuntimeException("文件不存在"); + } + + // 4. 生成预签名下载URL + String downloadUrl = minioClient.getPresignedObjectUrl( + GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(bucketName) + .object(objectName) + .build() + ); + + // 6. 构建返回结果 + Map result = new java.util.HashMap<>(); + result.put("objectName", objectName); + result.put("downloadUrl", downloadUrl); + + log.info("生成预签名下载URL成功,对象名: {}", objectName); + return result; + + } catch (Exception e) { + log.error("生成预签名下载URL失败,对象名: {}", objectName, e); + throw new RuntimeException("生成预签名下载URL失败: " + e.getMessage(), e); + } + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-dev.yaml b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-dev.yaml index ea85b302..820ea8c4 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-dev.yaml +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application-dev.yaml @@ -110,6 +110,15 @@ spring: # Spring Boot Admin Server 服务端的相关配置 context-path: /admin # 配置 Spring + +# MinIO 配置项 +minio: + endpoint: http://113.105.111.100:9000 # MinIO 服务地址 + access-key: 6kyyo7KvGZ5tFKfuxUl2 # 访问密钥 + secret-key: PmAFxtRcBlvg5ZGPlnzydQrnKtj1PQGHnd7x8hx7 # 密钥 + bucket-name: dev-bucket # 默认存储桶名称 + + --- #################### 芋道相关配置 #################### # 芋道配置项,设置当前项目所有自定义的配置