diff --git a/src/main/java/com/youlai/system/controller/FileController.java b/src/main/java/com/youlai/system/controller/FileController.java index ff3efe24..6fbad038 100644 --- a/src/main/java/com/youlai/system/controller/FileController.java +++ b/src/main/java/com/youlai/system/controller/FileController.java @@ -1,8 +1,8 @@ package com.youlai.system.controller; import com.youlai.system.common.result.Result; -import com.youlai.system.pojo.vo.FileInfoVO; -import com.youlai.system.service.FileService; +import com.youlai.system.model.dto.FileInfo; +import com.youlai.system.service.OssService; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -18,15 +18,15 @@ import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor public class FileController { - private final FileService fileService; + private final OssService ossService; @PostMapping @Operation(summary = "文件上传", security = {@SecurityRequirement(name = "Authorization")}) - public Result uploadFile( + public Result uploadFile( @Parameter(description ="表单文件对象") @RequestParam(value = "file") MultipartFile file ) { - FileInfoVO fileInfoVO = fileService.uploadFile(file); - return Result.success(fileInfoVO); + FileInfo fileInfo = ossService.uploadFile(file); + return Result.success(fileInfo); } @DeleteMapping @@ -35,7 +35,7 @@ public class FileController { public Result deleteFile( @Parameter(description ="文件路径") @RequestParam String filePath ) { - boolean result = fileService.deleteFile(filePath); + boolean result = ossService.deleteFile(filePath); return Result.judge(result); } } diff --git a/src/main/java/com/youlai/system/pojo/vo/FileInfoVO.java b/src/main/java/com/youlai/system/model/dto/FileInfo.java similarity index 80% rename from src/main/java/com/youlai/system/pojo/vo/FileInfoVO.java rename to src/main/java/com/youlai/system/model/dto/FileInfo.java index 32296b74..86be5d94 100644 --- a/src/main/java/com/youlai/system/pojo/vo/FileInfoVO.java +++ b/src/main/java/com/youlai/system/model/dto/FileInfo.java @@ -1,11 +1,11 @@ -package com.youlai.system.pojo.vo; +package com.youlai.system.model.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "文件对象") @Data -public class FileInfoVO { +public class FileInfo { @Schema(description = "文件名称") private String name; diff --git a/src/main/java/com/youlai/system/service/FileService.java b/src/main/java/com/youlai/system/service/OssService.java similarity index 72% rename from src/main/java/com/youlai/system/service/FileService.java rename to src/main/java/com/youlai/system/service/OssService.java index a1db7c88..37202dea 100644 --- a/src/main/java/com/youlai/system/service/FileService.java +++ b/src/main/java/com/youlai/system/service/OssService.java @@ -1,6 +1,6 @@ package com.youlai.system.service; -import com.youlai.system.pojo.vo.FileInfoVO; +import com.youlai.system.model.dto.FileInfo; import org.springframework.web.multipart.MultipartFile; /** @@ -9,16 +9,16 @@ import org.springframework.web.multipart.MultipartFile; * 已实现 MinIO * * @author haoxr - * @date 2022/11/19 + * @since 2022/11/19 */ -public interface FileService { +public interface OssService { /** * 上传文件 * @param file 表单文件对象 * @return */ - FileInfoVO uploadFile(MultipartFile file); + FileInfo uploadFile(MultipartFile file); /** * 删除文件 diff --git a/src/main/java/com/youlai/system/service/impl/oss/AliyunOssService.java b/src/main/java/com/youlai/system/service/impl/oss/AliyunOssService.java new file mode 100644 index 00000000..f1f241ed --- /dev/null +++ b/src/main/java/com/youlai/system/service/impl/oss/AliyunOssService.java @@ -0,0 +1,102 @@ +package com.youlai.system.service.impl.oss; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.model.ObjectMetadata; +import com.aliyun.oss.model.PutObjectRequest; +import com.aliyun.oss.model.PutObjectResult; +import com.youlai.system.model.dto.FileInfo; +import com.youlai.system.service.OssService; +import jakarta.annotation.PostConstruct; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.time.LocalDateTime; + +/** + * Aliyun 对象存储服务类 + * + * @author haoxr + * @since 2.3.0 + */ +@Component +@ConditionalOnProperty(value = "oss.type", havingValue = "aliyun") +@ConfigurationProperties(prefix = "oss.aliyun") +@RequiredArgsConstructor +@Data +public class AliyunOssService implements OssService { + /** + * 服务Endpoint + */ + private String endpoint; + /** + * 访问凭据 + */ + private String accessKeyId; + /** + * 凭据密钥 + */ + private String accessKeySecret; + /** + * 存储桶名称 + */ + private String bucketName; + + private OSS aliyunOssClient; + + @PostConstruct + public void init() { + aliyunOssClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); + } + + @Override + @SneakyThrows + public FileInfo uploadFile(MultipartFile file) { + + // 生成文件名(日期文件夹) + String suffix = FileUtil.getSuffix(file.getOriginalFilename()); + String uuid = IdUtil.simpleUUID(); + String fileName = DateUtil.format(LocalDateTime.now(), "yyyy/MM/dd") + "/" + uuid + "." + suffix; + // try-with-resource 语法糖自动释放流 + try (InputStream inputStream = file.getInputStream()) { + // 创建PutObjectRequest对象,指定Bucket名称、对象名称和输入流 + PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream); + + // 设置上传文件的元信息,例如Content-Type + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(file.getContentType()); + putObjectRequest.setMetadata(metadata); + + // 上传文件 + PutObjectResult putObjectResult = aliyunOssClient.putObject(putObjectRequest); + + // 获取文件访问路径 + String fileUrl = "https://" + bucketName + ".oss-cn-hangzhou.aliyuncs.com/" + fileName; + FileInfo fileInfo = new FileInfo(); + fileInfo.setName(fileName); + fileInfo.setUrl(fileUrl); + return fileInfo; + } catch (Exception e) { + throw new RuntimeException("文件上传失败"); + } + } + + @Override + public boolean deleteFile(String filePath) { + Assert.notBlank(filePath, "删除文件路径不能为空"); + String tempStr = "/" + bucketName + "/"; + String fileName = filePath.substring(filePath.indexOf(tempStr) + tempStr.length()); // 2022/11/20/test.jpg + aliyunOssClient.deleteObject(bucketName, fileName); + return true; + } +} diff --git a/src/main/java/com/youlai/system/service/impl/MinioServiceImpl.java b/src/main/java/com/youlai/system/service/impl/oss/MinioOssService.java similarity index 59% rename from src/main/java/com/youlai/system/service/impl/MinioServiceImpl.java rename to src/main/java/com/youlai/system/service/impl/oss/MinioOssService.java index 45613b2f..37538dee 100644 --- a/src/main/java/com/youlai/system/service/impl/MinioServiceImpl.java +++ b/src/main/java/com/youlai/system/service/impl/oss/MinioOssService.java @@ -1,82 +1,74 @@ -package com.youlai.system.service.impl; +package com.youlai.system.service.impl.oss; import cn.hutool.core.date.DateUtil; 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 com.youlai.system.pojo.vo.FileInfoVO; -import com.youlai.system.service.FileService; +import com.youlai.system.model.dto.FileInfo; +import com.youlai.system.service.OssService; import io.minio.*; +import io.minio.errors.*; import io.minio.http.Method; -import lombok.Setter; +import jakarta.annotation.PostConstruct; +import lombok.Data; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; /** - * MinIO 文件实现类 - * * @author haoxr - * @date 2022/12/17 + * @since 2023/6/2 */ @Component -@ConfigurationProperties(prefix = "minio") -@Slf4j -public class MinioServiceImpl implements FileService, InitializingBean { +@ConditionalOnProperty(value = "oss.type", havingValue = "minio") +@ConfigurationProperties(prefix = "oss.minio") +@RequiredArgsConstructor +@Data +public class MinioOssService implements OssService { /** - * MinIO的API地址 + * 服务Endpoint */ - @Setter private String endpoint; - /** - * 用户名 + * 访问凭据 */ - @Setter private String accessKey; - /** - * 密钥 + * 凭据密钥 */ - @Setter private String secretKey; - /** * 存储桶名称 */ - @Setter private String bucketName; - /** - * 自定义域名(非必须) + * 自定义域名 */ - @Setter private String customDomain; - private MinioClient minioClient; - @Override - public void afterPropertiesSet() { - log.info("MinIO Client init..."); - Assert.notBlank(endpoint, "MinIO endpoint can not be null"); - Assert.notBlank(accessKey, "MinIO accessKey can not be null"); - Assert.notBlank(secretKey, "MinIO secretKey can not be null"); - Assert.notBlank(bucketName, "MinIO bucketName can not be null"); - this.minioClient = MinioClient.builder() + // 依赖注入完成之后执行初始化 + @PostConstruct + public void init() { + minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } + /** * 上传文件 * @@ -84,45 +76,44 @@ public class MinioServiceImpl implements FileService, InitializingBean { * @return */ @Override - @SneakyThrows - public FileInfoVO uploadFile(MultipartFile file) { - // 存储桶不存在则创建 - createBucketIfAbsent(bucketName); + public FileInfo uploadFile(MultipartFile file) { // 生成文件名(日期文件夹) String suffix = FileUtil.getSuffix(file.getOriginalFilename()); String uuid = IdUtil.simpleUUID(); String fileName = DateUtil.format(LocalDateTime.now(), "yyyy/MM/dd") + "/" + uuid + "." + suffix; - - InputStream inputStream = file.getInputStream(); - - // 文件上传 - PutObjectArgs putObjectArgs = PutObjectArgs.builder() - .bucket(bucketName) - .object(fileName) - .contentType(file.getContentType()) - .stream(inputStream, inputStream.available(), -1) - .build(); - minioClient.putObject(putObjectArgs); - - // 返回文件路径 - String fileUrl; - if (StrUtil.isBlank(customDomain)) { // 未配置自定义域名 - GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder() - .bucket(bucketName).object(fileName) - .method(Method.GET) + // try-with-resource 语法糖自动释放流 + try (InputStream inputStream = file.getInputStream()) { + // 文件上传 + PutObjectArgs putObjectArgs = PutObjectArgs.builder() + .bucket(bucketName) + .object(fileName) + .contentType(file.getContentType()) + .stream(inputStream, inputStream.available(), -1) .build(); + minioClient.putObject(putObjectArgs); - fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs); - fileUrl = fileUrl.substring(0, fileUrl.indexOf("?")); - } else { // 配置自定义文件路径域名 - fileUrl = customDomain + '/' + bucketName + "/" + fileName; + // 返回文件路径 + String fileUrl; + if (StrUtil.isBlank(customDomain)) { // 未配置自定义域名 + GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder() + .bucket(bucketName).object(fileName) + .method(Method.GET) + .build(); + + fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs); + fileUrl = fileUrl.substring(0, fileUrl.indexOf("?")); + } else { // 配置自定义文件路径域名 + fileUrl = customDomain + '/' + bucketName + "/" + fileName; + } + + FileInfo fileInfo = new FileInfo(); + fileInfo.setName(fileName); + fileInfo.setUrl(fileUrl); + return fileInfo; + } catch (Exception e) { + throw new RuntimeException("文件上传失败"); } - - FileInfoVO fileInfoVO = new FileInfoVO(); - fileInfoVO.setName(fileName); - fileInfoVO.setUrl(fileUrl); - return fileInfoVO; } @@ -134,7 +125,6 @@ public class MinioServiceImpl implements FileService, InitializingBean { * @return */ @Override - @SneakyThrows public boolean deleteFile(String filePath) { Assert.notBlank(filePath, "删除文件路径不能为空"); String tempStr = "/" + bucketName + "/"; @@ -144,7 +134,13 @@ public class MinioServiceImpl implements FileService, InitializingBean { .bucket(bucketName) .object(fileName) .build(); - minioClient.removeObject(removeObjectArgs); + try { + minioClient.removeObject(removeObjectArgs); + } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | + InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | + XmlParserException e) { + throw new RuntimeException(e); + } return true; } @@ -163,17 +159,15 @@ public class MinioServiceImpl implements FileService, InitializingBean { * Resource: 指定存储桶 * Action: 操作行为 */ - StringBuilder builder = new StringBuilder(); - builder.append("{\"Version\":\"2012-10-17\"," + + return "{\"Version\":\"2012-10-17\"," + "\"Statement\":[{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]}," + "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"]," + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]}," + "{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]}," + "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"]," - + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}"); - - return builder.toString(); + + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}"; } /** @@ -198,5 +192,4 @@ public class MinioServiceImpl implements FileService, InitializingBean { minioClient.setBucketPolicy(setBucketPolicyArgs); } } - } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 82743030..c37b758e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -30,8 +30,11 @@ mybatis-plus: db-config: # 主键ID类型 id-type: none + # 逻辑删除字段名称 logic-delete-field: deleted + # 逻辑删除-删除值 logic-delete-value: 1 + # 逻辑删除-未删除值 logic-not-delete-value: 0 configuration: # 驼峰下划线转换 @@ -47,15 +50,34 @@ auth: # token 有效期(单位:秒) ttl: 18000 -# MinIO 分布式文件系统 -minio: - endpoint: http://localhost:9000 - access-key: minioadmin - secret-key: minioadmin - # 存储桶名称 - bucket-name: default - # 自定义域名(非必须),Nginx配置反向代理转发文件路径 - custom-domain: +oss: + # OSS 类型 (目前支持aliyun、minio) + type: minio + # MinIO 对象存储服务 + minio: + # 服务Endpoint + endpoint: http://localhost:9000 + # 访问凭据 + access-key: minioadmin + # 凭据密钥 + secret-key: minioadmin + # 存储桶名称 + bucket-name: default + # (可选)自定义域名,如果配置了域名,生成的文件URL是域名格式,未配置则URL则是IP格式 (eg: https://oss.youlai.tech) + custom-domain: + # 阿里云OSS对象存储服务 + aliyun: + # 服务Endpoint + endpoint: oss-cn-hangzhou.aliyuncs.com + # 访问凭据 + access-key-id: your-access-key-id + # 凭据密钥 + access-key-secret: your-access-key-secret + bucket-name: default + # (可选)自定义域名,如果配置了域名,生成的文件URL是域名格式,未配置则URL则是IP格式 (eg: https://oss.youlai.tech) + custom-domain: + + # springdoc配置: https://springdoc.org/properties.html springdoc: @@ -68,7 +90,6 @@ springdoc: # 验证码配置 easy-captcha: - enable: true # 验证码类型: arithmetic-算术 type: arithmetic # 验证码有效时间(单位:秒) @@ -77,8 +98,6 @@ easy-captcha: # xxl-job 定时任务配置 xxl: job: - # xxl-job 开关 - enabled: false admin: # 多个地址使用,分割 addresses: http://127.0.0.1:8080/xxl-job-admin @@ -92,8 +111,11 @@ xxl: logretentiondays: 30 # 系统配置 -system-config: - # 数据权限配置 - data-permission: +system: + config: # 数据权限开关 - enabled: true \ No newline at end of file + data-permission-enabled: true + # 定时任务 xxl-job 开关 + xxl-job-enabled: false + # WebSocket 开关 + websocket-enabled: true \ No newline at end of file