Răsfoiți Sursa

图片上传api

hr~ 3 săptămâni în urmă
părinte
comite
e868b7ac34

+ 6 - 0
pom.xml

@@ -26,9 +26,15 @@
         <druid.version>1.2.1</druid.version>
         <commons.io.version>2.5</commons.io.version>
         <fastjson.version>1.2.74</fastjson.version>
+        <qcloud.version>5.6.257</qcloud.version>
     </properties>
 
     <dependencies>
+        <dependency>
+            <groupId>com.qcloud</groupId>
+            <artifactId>cos_api</artifactId>
+            <version>${qcloud.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>

+ 57 - 0
py-base/src/main/java/com/poyee/config/TencentCosConfig.java

@@ -0,0 +1,57 @@
+package com.poyee.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 腾讯云OSS配置
+ *
+ * @author: zheng
+ * @date: 2026/01/26
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "tencent.cos")
+public class TencentCosConfig {
+
+    /**
+     * 腾讯云SecretId
+     */
+    private String secretId;
+
+    /**
+     * 腾讯云SecretKey
+     */
+    private String secretKey;
+
+    /**
+     * 所属地域
+     */
+    private String region;
+
+    /**
+     * 存储桶名称
+     */
+    private String bucketName;
+
+    /**
+     * 访问域名
+     */
+    private String domain;
+
+    /**
+     * 文件路径前缀
+     */
+    private String prefix;
+
+    /**
+     * 文件大小限制(默认10MB)
+     */
+    private Long maxFileSize = 10 * 1024 * 1024L;
+
+    /**
+     * 允许上传的文件类型
+     */
+    private String[] allowFileTypes = {"jpg", "jpeg", "png", "gif", "bmp", "webp"};
+}

+ 366 - 0
py-base/src/main/java/com/poyee/util/TencentCosUtil.java

@@ -0,0 +1,366 @@
+package com.poyee.util;
+
+import com.poyee.config.TencentCosConfig;
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.ClientConfig;
+import com.qcloud.cos.auth.BasicCOSCredentials;
+import com.qcloud.cos.auth.COSCredentials;
+import com.qcloud.cos.auth.COSSigner;
+import com.qcloud.cos.model.ObjectMetadata;
+import com.qcloud.cos.model.PutObjectRequest;
+import com.qcloud.cos.model.PutObjectResult;
+import com.qcloud.cos.region.Region;
+import com.qcloud.cos.utils.Jackson;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 腾讯云COS工具类
+ *
+ * @author: zheng
+ * @date: 2026/01/26
+ */
+@Slf4j
+@Component
+public class TencentCosUtil {
+
+    @Resource
+    private TencentCosConfig tencentCosConfig;
+
+    /**
+     * 获取COS客户端
+     */
+    private COSClient getCosClient() {
+        // 1 初始化用户身份信息(secretId, secretKey)
+        COSCredentials cred = new BasicCOSCredentials(tencentCosConfig.getSecretId(), tencentCosConfig.getSecretKey());
+        // 2 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
+        ClientConfig clientConfig = new ClientConfig(new Region(tencentCosConfig.getRegion()));
+        // 3 生成cos客户端
+        return new COSClient(cred, clientConfig);
+    }
+
+    /**
+     * 上传文件
+     *
+     * @param file 文件
+     * @return 文件访问路径
+     */
+    public String uploadFile(MultipartFile file) throws IOException {
+        return uploadFile(file, null);
+    }
+
+    /**
+     * 上传文件
+     *
+     * @param file   文件
+     * @param folder 文件夹名称,可为空
+     * @return 文件访问路径
+     */
+    public String uploadFile(MultipartFile file, String folder) throws IOException {
+        // 检查文件大小
+        if (file.getSize() > tencentCosConfig.getMaxFileSize()) {
+            throw new IOException("文件大小超过限制,最大允许" + (tencentCosConfig.getMaxFileSize() / 1024 / 1024) + "MB");
+        }
+
+        // 检查文件类型
+        String originalFilename = file.getOriginalFilename();
+        if (originalFilename == null) {
+            throw new IOException("文件名不能为空");
+        }
+
+        String fileExtension = getFileExtension(originalFilename);
+        if (!isAllowedFileType(fileExtension)) {
+            throw new IOException("不支持的文件类型:" + fileExtension);
+        }
+
+        // 生成文件路径
+        String fileName = generateFileName(fileExtension);
+        String filePath = tencentCosConfig.getPrefix();
+        if (folder != null && !folder.isEmpty()) {
+            filePath += folder + "/";
+        }
+        filePath += fileName;
+
+        COSClient cosClient = null;
+        try {
+            cosClient = getCosClient();
+            // 设置文件元数据
+            ObjectMetadata metadata = new ObjectMetadata();
+            metadata.setContentLength(file.getSize());
+            metadata.setContentType(file.getContentType());
+
+            // 上传文件
+            PutObjectRequest putObjectRequest = new PutObjectRequest(
+                    tencentCosConfig.getBucketName(), filePath, file.getInputStream(), metadata);
+            PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
+
+            if (putObjectResult.getETag() != null) {
+                // 返回文件访问路径
+                return getFileUrl(filePath);
+            } else {
+                throw new IOException("文件上传失败");
+            }
+        } finally {
+            if (cosClient != null) {
+                cosClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 上传文件流
+     *
+     * @param inputStream 文件流
+     * @param fileName    文件名
+     * @return 文件访问路径
+     */
+    public String uploadFile(InputStream inputStream, String fileName) throws IOException {
+        return uploadFile(inputStream, fileName, null);
+    }
+
+    /**
+     * 上传文件流
+     *
+     * @param inputStream 文件流
+     * @param fileName    文件名
+     * @param folder      文件夹名称,可为空
+     * @return 文件访问路径
+     */
+    public String uploadFile(InputStream inputStream, String fileName, String folder) throws IOException {
+        // 检查文件类型
+        String fileExtension = getFileExtension(fileName);
+        if (!isAllowedFileType(fileExtension)) {
+            throw new IOException("不支持的文件类型:" + fileExtension);
+        }
+
+        // 生成文件路径
+        String newFileName = generateFileName(fileExtension);
+        String filePath = tencentCosConfig.getPrefix();
+        if (folder != null && !folder.isEmpty()) {
+            filePath += folder + "/";
+        }
+        filePath += newFileName;
+
+        COSClient cosClient = null;
+        try {
+            cosClient = getCosClient();
+            // 设置文件元数据
+            ObjectMetadata metadata = new ObjectMetadata();
+            metadata.setContentType(getContentType(fileExtension));
+
+            // 上传文件
+            PutObjectRequest putObjectRequest = new PutObjectRequest(
+                    tencentCosConfig.getBucketName(), filePath, inputStream, metadata);
+            PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
+
+            if (putObjectResult.getETag() != null) {
+                // 返回文件访问路径
+                return getFileUrl(filePath);
+            } else {
+                throw new IOException("文件上传失败");
+            }
+        } finally {
+            if (cosClient != null) {
+                cosClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 删除文件
+     *
+     * @param fileUrl 文件访问路径
+     */
+    public void deleteFile(String fileUrl) {
+        if (fileUrl == null || fileUrl.isEmpty()) {
+            return;
+        }
+
+        // 从URL中提取文件路径
+        String filePath = getFilePathFromUrl(fileUrl);
+        if (filePath == null) {
+            return;
+        }
+
+        COSClient cosClient = null;
+        try {
+            cosClient = getCosClient();
+            cosClient.deleteObject(tencentCosConfig.getBucketName(), filePath);
+            log.info("删除文件成功: {}", fileUrl);
+        } catch (Exception e) {
+            log.error("删除文件失败: {}", fileUrl, e);
+        } finally {
+            if (cosClient != null) {
+                cosClient.shutdown();
+            }
+        }
+    }
+
+    /**
+     * 生成文件名
+     *
+     * @param fileExtension 文件扩展名
+     * @return 文件名
+     */
+    private String generateFileName(String fileExtension) {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        String dateStr = sdf.format(new Date());
+        String uuid = UUID.randomUUID().toString().replace("-", "");
+        return dateStr + "/" + uuid + "." + fileExtension;
+    }
+
+    /**
+     * 获取文件扩展名
+     *
+     * @param fileName 文件名
+     * @return 文件扩展名
+     */
+    private String getFileExtension(String fileName) {
+        if (fileName == null || fileName.isEmpty()) {
+            return "";
+        }
+        int dotIndex = fileName.lastIndexOf('.');
+        if (dotIndex == -1 || dotIndex == fileName.length() - 1) {
+            return "";
+        }
+        return fileName.substring(dotIndex + 1).toLowerCase();
+    }
+
+    /**
+     * 检查文件类型是否允许
+     *
+     * @param fileExtension 文件扩展名
+     * @return 是否允许
+     */
+    private boolean isAllowedFileType(String fileExtension) {
+        if (fileExtension == null || fileExtension.isEmpty()) {
+            return false;
+        }
+        for (String type : tencentCosConfig.getAllowFileTypes()) {
+            if (type.equalsIgnoreCase(fileExtension)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 根据文件扩展名获取Content-Type
+     *
+     * @param fileExtension 文件扩展名
+     * @return Content-Type
+     */
+    private String getContentType(String fileExtension) {
+        switch (fileExtension.toLowerCase()) {
+            case "jpg":
+            case "jpeg":
+                return "image/jpeg";
+            case "png":
+                return "image/png";
+            case "gif":
+                return "image/gif";
+            case "bmp":
+                return "image/bmp";
+            case "webp":
+                return "image/webp";
+            default:
+                return "application/octet-stream";
+        }
+    }
+
+    /**
+     * 获取文件访问路径
+     *
+     * @param filePath 文件路径
+     * @return 文件访问路径
+     */
+    private String getFileUrl(String filePath) {
+        // ranking-1309648802.cos.ap-shanghai.myqcloud.com
+        return "https://" + tencentCosConfig.getBucketName() + ".cos." + tencentCosConfig.getRegion() + ".myqcloud.com" + filePath;
+    }
+
+    /**
+     * 从URL中提取文件路径
+     *
+     * @param fileUrl 文件URL
+     * @return 文件路径
+     */
+    private String getFilePathFromUrl(String fileUrl) {
+        if (fileUrl == null || fileUrl.isEmpty()) {
+            return null;
+        }
+
+        try {
+            // 如果是自定义域名
+            if (tencentCosConfig.getDomain() != null && !tencentCosConfig.getDomain().isEmpty() && fileUrl.startsWith(tencentCosConfig.getDomain())) {
+                return fileUrl.substring(tencentCosConfig.getDomain().length() + 1);
+            }
+
+            // 如果是默认域名
+            String defaultDomain = "https://" + tencentCosConfig.getBucketName() + ".cos." + tencentCosConfig.getRegion() + ".myqcloud.com/";
+            if (fileUrl.startsWith(defaultDomain)) {
+                return fileUrl.substring(defaultDomain.length());
+            }
+
+            return null;
+        } catch (Exception e) {
+            log.error("从URL中提取文件路径失败: {}", fileUrl, e);
+            return null;
+        }
+    }
+
+
+    public String postObjectUpload(String filename) {
+        String bucketName = tencentCosConfig.getBucketName();
+//        String endpoint = "cos.ap-shanghai.myqcloud.com";
+        String key = filename;
+//        String contentType = "image/jpeg";
+        String secretId = tencentCosConfig.getSecretId();
+        String secretKey = tencentCosConfig.getSecretKey();
+        long startTimestamp = System.currentTimeMillis() / 1000;
+        long endTimestamp = startTimestamp + 10 * 60;
+        String endTimestampStr = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").
+                format(endTimestamp * 1000);
+        String keyTime = startTimestamp + ";" + endTimestamp;
+        // 设置表单的body字段值
+        Map<String, String> formFields = new HashMap<>();
+        formFields.put("q-sign-algorithm", "sha1");
+        formFields.put("key", key);
+        formFields.put("q-ak", secretId);
+        formFields.put("q-key-time", keyTime);
+        // 构造policy,参考文档: https://cloud.tencent.com/document/product/436/14690
+        String policy = "{\n" +
+                "    \"expiration\": \"" + endTimestampStr + "\",\n" +
+                "    \"conditions\": [\n" +
+                "        { \"bucket\": \"" + bucketName + "\" },\n" +
+                "        { \"q-sign-algorithm\": \"sha1\" },\n" +
+                "        { \"q-ak\": \"" + secretId + "\" },\n" +
+                "        { \"q-sign-time\":\"" + keyTime + "\" }\n" +
+                "    ]\n" +
+                "}";
+        // policy需要base64后算放入表单中
+        String encodedPolicy = new String(Base64.encodeBase64(policy.getBytes()));
+        // 设置policy
+        formFields.put("policy", encodedPolicy);
+        // 根据编码后的policy和secretKey计算签名
+        COSSigner cosSigner = new COSSigner();
+        String signature = cosSigner.buildPostObjectSignature(secretKey,keyTime, policy);
+        // 设置签名
+        formFields.put("q-signature", signature);
+        log.info("表单参数:{}",Jackson.toJsonPrettyString(formFields));
+        // 根据以上表单参数,构造最开始的body部分
+        return Jackson.toJsonPrettyString(formFields);
+    }
+
+}

+ 34 - 0
py-goods/src/main/java/com/poyee/controller/ImageUploadController.java

@@ -0,0 +1,34 @@
+package com.poyee.controller;
+
+import com.poyee.base.dto.Result;
+import com.poyee.dto.AccessTokenDTO;
+import com.poyee.util.TencentCosUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+
+/**
+ * 图片上传管理
+ *
+ * @author: gengjintao
+ * @date: 2026/01/07
+ */
+@Slf4j
+@Api(tags = "图片上传管理")
+@RestController
+@AllArgsConstructor
+@RequestMapping("/api/image")
+public class ImageUploadController {
+
+    private final TencentCosUtil tencentCosUtil;
+
+    @ApiOperation("生成cos签名")
+    @CrossOrigin(origins = "*")
+    @PostMapping("/oss")
+    public Result<String> getAccessToken(@RequestBody AccessTokenDTO request) {
+        return Result.success(tencentCosUtil.postObjectUpload(request.getFilename()));
+    }
+}

+ 16 - 0
py-goods/src/main/java/com/poyee/dto/AccessTokenDTO.java

@@ -0,0 +1,16 @@
+package com.poyee.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@ApiModel("获取token Request")
+@NoArgsConstructor
+@AllArgsConstructor
+public class AccessTokenDTO {
+    @ApiModelProperty("文件名")
+    private String filename;
+}

+ 12 - 1
py-starter/src/main/resources/application.yml

@@ -26,4 +26,15 @@ logging:
   fluentd:
     enabled: ${FLUENTD_ENABLED:false}
     host: ${FLUENTD_HOST:127.0.0.1}
-    port: ${FLUENTD_PORT:24225}
+    port: ${FLUENTD_PORT:24225}
+# 腾讯云COS配置
+tencent:
+  cos:
+    secret-id: ${TENCENT_COS_SECRET_ID:AKIDCKFE2nX8iMqMjuwoUyLXbWHqMhttxujP}
+    secret-key: ${TENCENT_COS_SECRET_KEY:bFpFTXVRH65BBBC4mRzOdjhZHpBNQNAn}
+    region: ${TENCENT_COS_REGION:ap-shanghai}
+    bucket-name: ${TENCENT_COS_BUCKET_NAME:public-1383070248}
+    domain: ${TENCENT_COS_DOMAIN:https://your-domain.com}
+    prefix: ${TENCENT_COS_PREFIX:/mongoo/rating/images/}
+    max-file-size: ${TENCENT_COS_MAX_FILE_SIZE:10485760}
+    allow-file-types: ${TENCENT_COS_ALLOW_FILE_TYPES:jpg,jpeg,png,gif,bmp,webp}