|
|
@@ -0,0 +1,145 @@
|
|
|
+package com.xuekairui.user.service;
|
|
|
+
|
|
|
+import com.aliyun.oss.OSS;
|
|
|
+import com.aliyun.oss.OSSClientBuilder;
|
|
|
+import com.aliyun.oss.model.PutObjectRequest;
|
|
|
+import com.xuekairui.common.BusinessException;
|
|
|
+import com.xuekairui.common.ErrorCode;
|
|
|
+import com.xuekairui.user.config.OssProperties;
|
|
|
+import jakarta.annotation.PostConstruct;
|
|
|
+import jakarta.annotation.PreDestroy;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.util.UUID;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 阿里云 OSS 文件上传服务
|
|
|
+ * <p>
|
|
|
+ * 支持将 MultipartFile 上传到 OSS,返回可访问的文件 URL。
|
|
|
+ * 使用 UUID 生成唯一文件名,按日期分目录存储。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class OssService {
|
|
|
+
|
|
|
+ private final OssProperties ossProperties;
|
|
|
+
|
|
|
+ private OSS ossClient;
|
|
|
+
|
|
|
+ @PostConstruct
|
|
|
+ public void init() {
|
|
|
+ if (ossProperties.getAccessKeyId() == null || ossProperties.getAccessKeyId().isBlank()) {
|
|
|
+ log.warn("OSS 未配置 access-key-id,跳过初始化");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (ossProperties.getBucketName() == null || ossProperties.getBucketName().isBlank()) {
|
|
|
+ log.warn("OSS 未配置 bucket-name,跳过初始化");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ ossClient = new OSSClientBuilder().build(
|
|
|
+ ossProperties.getEndpoint(),
|
|
|
+ ossProperties.getAccessKeyId(),
|
|
|
+ ossProperties.getAccessKeySecret());
|
|
|
+ log.info("OSS 客户端初始化完成: endpoint={}, bucket={}",
|
|
|
+ ossProperties.getEndpoint(), ossProperties.getBucketName());
|
|
|
+ }
|
|
|
+
|
|
|
+ @PreDestroy
|
|
|
+ public void destroy() {
|
|
|
+ if (ossClient != null) {
|
|
|
+ ossClient.shutdown();
|
|
|
+ log.info("OSS 客户端已关闭");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传文件到 OSS 并返回可访问 URL
|
|
|
+ *
|
|
|
+ * @param file 上传的文件
|
|
|
+ * @return 可公开访问的文件 URL
|
|
|
+ */
|
|
|
+ public String upload(MultipartFile file) {
|
|
|
+ ensureReady();
|
|
|
+
|
|
|
+ // 获取原始文件名扩展名
|
|
|
+ String originalFilename = file.getOriginalFilename();
|
|
|
+ String extension = "";
|
|
|
+ if (originalFilename != null && originalFilename.contains(".")) {
|
|
|
+ extension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成唯一文件名:basePath/日期/UUID.扩展名
|
|
|
+ String datePath = java.time.LocalDate.now().toString().replace("-", "/");
|
|
|
+ String fileName = UUID.randomUUID().toString().replace("-", "") + extension;
|
|
|
+ String objectKey = normalizeBasePath() + datePath + "/" + fileName;
|
|
|
+
|
|
|
+ try (InputStream inputStream = file.getInputStream()) {
|
|
|
+ PutObjectRequest putRequest = new PutObjectRequest(
|
|
|
+ ossProperties.getBucketName(), objectKey, inputStream);
|
|
|
+ // 不设置 Content-Type,OSS 会自动根据后缀推断
|
|
|
+
|
|
|
+ ossClient.putObject(putRequest);
|
|
|
+
|
|
|
+ String fileUrl = buildAccessUrl(objectKey);
|
|
|
+ log.info("OSS 上传成功: objectKey={}, size={} bytes, url={}",
|
|
|
+ objectKey, file.getSize(), fileUrl);
|
|
|
+ return fileUrl;
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.error("OSS 上传失败: filename={}", originalFilename, e);
|
|
|
+ throw new BusinessException(ErrorCode.BUSINESS_ERROR, "文件上传失败,请稍后重试");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 确保 OSS 客户端可用
|
|
|
+ */
|
|
|
+ private void ensureReady() {
|
|
|
+ if (ossClient == null) {
|
|
|
+ throw new BusinessException(ErrorCode.BUSINESS_ERROR, "OSS 服务未配置,请联系管理员");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 规范化 basePath,确保以 /结尾,不以 / 开头
|
|
|
+ */
|
|
|
+ private String normalizeBasePath() {
|
|
|
+ String path = ossProperties.getBasePath();
|
|
|
+ if (path == null || path.isBlank()) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ path = path.trim();
|
|
|
+ // 去掉开头的 /
|
|
|
+ if (path.startsWith("/")) {
|
|
|
+ path = path.substring(1);
|
|
|
+ }
|
|
|
+ // 确保以 / 结尾
|
|
|
+ if (!path.endsWith("/")) {
|
|
|
+ path = path + "/";
|
|
|
+ }
|
|
|
+ return path;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建可访问的 URL
|
|
|
+ */
|
|
|
+ private String buildAccessUrl(String objectKey) {
|
|
|
+ String urlPrefix = ossProperties.getUrlPrefix();
|
|
|
+ if (urlPrefix != null && !urlPrefix.isBlank()) {
|
|
|
+ // 自定义域名
|
|
|
+ String prefix = urlPrefix.trim();
|
|
|
+ if (prefix.endsWith("/")) {
|
|
|
+ prefix = prefix.substring(0, prefix.length() - 1);
|
|
|
+ }
|
|
|
+ return prefix + "/" + objectKey;
|
|
|
+ }
|
|
|
+ // 默认 OSS 公网 URL: https://{bucket}.{endpoint}/{objectKey}
|
|
|
+ return "https://" + ossProperties.getBucketName() + "."
|
|
|
+ + ossProperties.getEndpoint() + "/" + objectKey;
|
|
|
+ }
|
|
|
+}
|