|
|
@@ -0,0 +1,229 @@
|
|
|
+package com.yingpai.gupiao.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.yingpai.gupiao.domain.po.SysOssConfig;
|
|
|
+import com.yingpai.gupiao.mapper.SysOssConfigMapper;
|
|
|
+import com.yingpai.gupiao.service.OssService;
|
|
|
+import jakarta.annotation.PostConstruct;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
|
|
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
|
|
+import software.amazon.awssdk.core.sync.RequestBody;
|
|
|
+import software.amazon.awssdk.regions.Region;
|
|
|
+import software.amazon.awssdk.services.s3.S3Client;
|
|
|
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
|
|
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
|
|
+
|
|
|
+import java.net.URI;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.UUID;
|
|
|
+
|
|
|
+/**
|
|
|
+ * OSS服务实现(读取RuoYi的sys_oss_config配置)
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class OssServiceImpl implements OssService {
|
|
|
+
|
|
|
+ private final SysOssConfigMapper ossConfigMapper;
|
|
|
+
|
|
|
+ private S3Client s3Client;
|
|
|
+ private SysOssConfig currentConfig;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化OSS客户端
|
|
|
+ */
|
|
|
+ @PostConstruct
|
|
|
+ public void init() {
|
|
|
+ refreshConfig();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 刷新OSS配置
|
|
|
+ */
|
|
|
+ private void refreshConfig() {
|
|
|
+ // 查询默认启用的OSS配置(status=0表示默认)
|
|
|
+ LambdaQueryWrapper<SysOssConfig> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(SysOssConfig::getStatus, "0");
|
|
|
+ SysOssConfig config = ossConfigMapper.selectOne(wrapper);
|
|
|
+
|
|
|
+ if (config == null) {
|
|
|
+ log.warn("[OSS] 未找到默认OSS配置,文件上传将使用本地存储");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 配置未变化则不重建客户端
|
|
|
+ if (currentConfig != null && isSameConfig(currentConfig, config)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ currentConfig = config;
|
|
|
+ buildS3Client(config);
|
|
|
+ log.info("[OSS] 初始化OSS客户端成功,configKey={}", config.getConfigKey());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建S3客户端
|
|
|
+ */
|
|
|
+ private void buildS3Client(SysOssConfig config) {
|
|
|
+ try {
|
|
|
+ // trim所有配置值,防止数据库中有空白字符
|
|
|
+ String accessKey = config.getAccessKey() != null ? config.getAccessKey().trim() : "";
|
|
|
+ String secretKey = config.getSecretKey() != null ? config.getSecretKey().trim() : "";
|
|
|
+ String endpoint = config.getEndpoint() != null ? config.getEndpoint().trim() : "";
|
|
|
+ String regionStr = config.getRegion() != null ? config.getRegion().trim() : "";
|
|
|
+
|
|
|
+ StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
|
|
|
+ AwsBasicCredentials.create(accessKey, secretKey)
|
|
|
+ );
|
|
|
+
|
|
|
+ String endpointUrl = getEndpoint(config);
|
|
|
+ Region region = regionStr != null && !regionStr.isEmpty()
|
|
|
+ ? Region.of(regionStr)
|
|
|
+ : Region.US_EAST_1;
|
|
|
+
|
|
|
+ // 判断是否为云服务商(阿里云、腾讯云等使用虚拟主机样式,MinIO使用路径样式)
|
|
|
+ boolean isPathStyle = !isCloudService(endpoint);
|
|
|
+
|
|
|
+ this.s3Client = S3Client.builder()
|
|
|
+ .credentialsProvider(credentialsProvider)
|
|
|
+ .endpointOverride(URI.create(endpointUrl))
|
|
|
+ .region(region)
|
|
|
+ .forcePathStyle(isPathStyle)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[OSS] 构建S3客户端失败: {}", e.getMessage());
|
|
|
+ this.s3Client = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String upload(MultipartFile file) {
|
|
|
+ // 每次上传前刷新配置(支持后台动态修改)
|
|
|
+ refreshConfig();
|
|
|
+
|
|
|
+ if (s3Client == null || currentConfig == null) {
|
|
|
+ throw new RuntimeException("OSS未配置或配置错误");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String originalFilename = file.getOriginalFilename();
|
|
|
+ String suffix = originalFilename != null && originalFilename.contains(".")
|
|
|
+ ? originalFilename.substring(originalFilename.lastIndexOf("."))
|
|
|
+ : "";
|
|
|
+
|
|
|
+ // 生成文件路径:prefix/yyyy/MM/dd/uuid.ext
|
|
|
+ String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
|
|
+ String uuid = UUID.randomUUID().toString().replace("-", "");
|
|
|
+ String key = (currentConfig.getPrefix() != null && !currentConfig.getPrefix().isEmpty()
|
|
|
+ ? currentConfig.getPrefix() + "/" : "")
|
|
|
+ + datePath + "/" + uuid + suffix;
|
|
|
+
|
|
|
+ // 上传文件
|
|
|
+ PutObjectRequest putRequest = PutObjectRequest.builder()
|
|
|
+ .bucket(currentConfig.getBucketName())
|
|
|
+ .key(key)
|
|
|
+ .contentType(file.getContentType())
|
|
|
+ .build();
|
|
|
+
|
|
|
+ s3Client.putObject(putRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
|
|
|
+
|
|
|
+ // 返回访问URL
|
|
|
+ String url = getAccessUrl(currentConfig) + "/" + key;
|
|
|
+ log.info("[OSS] 文件上传成功: {}", url);
|
|
|
+ return url;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[OSS] 文件上传失败: {}", e.getMessage());
|
|
|
+ throw new RuntimeException("文件上传失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void delete(String url) {
|
|
|
+ if (s3Client == null || currentConfig == null) {
|
|
|
+ log.warn("[OSS] OSS未配置,无法删除文件");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String baseUrl = getAccessUrl(currentConfig) + "/";
|
|
|
+ if (!url.startsWith(baseUrl)) {
|
|
|
+ log.warn("[OSS] URL不匹配当前OSS配置: {}", url);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String key = url.replace(baseUrl, "");
|
|
|
+ DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
|
|
|
+ .bucket(currentConfig.getBucketName())
|
|
|
+ .key(key)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ s3Client.deleteObject(deleteRequest);
|
|
|
+ log.info("[OSS] 文件删除成功: {}", url);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[OSS] 文件删除失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取endpoint(带协议)
|
|
|
+ */
|
|
|
+ private String getEndpoint(SysOssConfig config) {
|
|
|
+ String protocol = "1".equals(config.getIsHttps()) ? "https://" : "http://";
|
|
|
+ return protocol + config.getEndpoint();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取访问URL
|
|
|
+ */
|
|
|
+ private String getAccessUrl(SysOssConfig config) {
|
|
|
+ String protocol = "1".equals(config.getIsHttps()) ? "https://" : "http://";
|
|
|
+
|
|
|
+ // 如果配置了自定义域名
|
|
|
+ if (config.getDomain() != null && !config.getDomain().isEmpty()) {
|
|
|
+ String domain = config.getDomain();
|
|
|
+ if (domain.startsWith("http://") || domain.startsWith("https://")) {
|
|
|
+ return domain;
|
|
|
+ }
|
|
|
+ return protocol + domain;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 云服务商使用虚拟主机样式:bucket.endpoint
|
|
|
+ if (isCloudService(config.getEndpoint())) {
|
|
|
+ return protocol + config.getBucketName() + "." + config.getEndpoint();
|
|
|
+ }
|
|
|
+
|
|
|
+ // MinIO等使用路径样式:endpoint/bucket
|
|
|
+ return protocol + config.getEndpoint() + "/" + config.getBucketName();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断是否为云服务商
|
|
|
+ */
|
|
|
+ private boolean isCloudService(String endpoint) {
|
|
|
+ if (endpoint == null) return false;
|
|
|
+ return endpoint.contains("aliyuncs.com")
|
|
|
+ || endpoint.contains("myqcloud.com")
|
|
|
+ || endpoint.contains("qiniucs.com")
|
|
|
+ || endpoint.contains("amazonaws.com");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断配置是否相同
|
|
|
+ */
|
|
|
+ private boolean isSameConfig(SysOssConfig c1, SysOssConfig c2) {
|
|
|
+ return c1.getOssConfigId().equals(c2.getOssConfigId())
|
|
|
+ && c1.getAccessKey().equals(c2.getAccessKey())
|
|
|
+ && c1.getSecretKey().equals(c2.getSecretKey())
|
|
|
+ && c1.getBucketName().equals(c2.getBucketName())
|
|
|
+ && c1.getEndpoint().equals(c2.getEndpoint());
|
|
|
+ }
|
|
|
+}
|