Spring Boot 项目中使用腾讯云对象存储(COS)

news/2025/2/8 15:26:17 标签: java, 腾讯云, cos

第1部分:环境与基本配置

在使用腾讯云对象存储(COS)之前,我们需要在 Spring Boot 项目中完成一些必要的环境准备工作,包括引入依赖、在腾讯云控制台创建访问密钥以及进行一些基础的配置。

1.1 申请腾讯云COS的账户、开通服务及获取密钥

  • 如果你还没有腾讯云的账号,先注册一个腾讯云账号并进行实名认证。
  • 进入 腾讯云对象存储控制台,创建存储桶(Bucket)。
    • 你可以自定义命名你的存储桶,如 my-bucket-1250000000,注意名称要全局唯一。
    • 存储桶所在的区域(Region)需要记住,比如 ap-beijingap-shanghai 等。
  • 在 访问管理控制台 里创建或查看已有的 SecretIdSecretKey
    • SecretId 和 SecretKey 会用来对请求进行签名,保证请求合法性。

1.2 引入依赖

腾讯云官方提供了多种 SDK,Java 版本的 SDK 可以通过 Maven 或者 Gradle 引入。以下以 Maven 为例展示如何引入依赖,版本号可以在 官方文档 或 Maven中央仓库 查看并更新。

<dependency>
    <groupId>com.qcloud</groupId>
    <artifactId>cos_api</artifactId>
    <version>5.6.59</version>
</dependency>

注意:版本号可能会更新,请自行查看最新版本。

1.3 Spring Boot 中的配置

在 Spring Boot 项目中,我们可以通过 application.yml 或者 application.properties 来配置 COS 的相关信息,方便后续读取和管理。

1.3.1 application.yml 示例

tencent:
  cos:
    secret-id: yourSecretId
    secret-key: yourSecretKey
    region: ap-shanghai
    bucket-name: my-bucket-1250000000
  • secret-id:就是上面申请到的 SecretId
  • secret-key:对应的 SecretKey
  • region:你创建存储桶时选择的区域
  • bucket-name:创建的存储桶名称(一般会有 AppId 后缀,比如 my-bucket-1250000000

1.3.2 编写配置类

为了方便在项目中使用,可以新建一个配置类来读取以上参数并创建一个 COSClient(官方 SDK 提供的客户端类,用于操作 COS)。

java">package com.example.demo.config;

import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.region.Region;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CosConfig {

    @Value("${tencent.cos.secret-id}")
    private String secretId;
    
    @Value("${tencent.cos.secret-key}")
    private String secretKey;
    
    @Value("${tencent.cos.region}")
    private String region;

    @Bean
    public COSClient cosClient() {
        // 1. 初始化用户身份信息(secretId, secretKey)。
        BasicCOSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
        // 2. 设置 bucket 的区域, COS 地域的简称请参阅 https://cloud.tencent.com/document/product/436/6224
        ClientConfig clientConfig = new ClientConfig(new Region(region));
        // 3. 生成 cos 客户端。
        return new COSClient(cred, clientConfig);
    }
}

这样我们在需要使用 COSClient 的地方就可以通过 Spring 的依赖注入直接使用了(如 @Autowired 或构造函数注入)。
同时,将 bucketName 放在配置文件中也很常见,可以继续在同一个类或者在其他地方读取:

java">@Value("${tencent.cos.bucket-name}")
private String bucketName;

这样就把“初始化 COSClient”以及“读取配置信息”都抽离到一个配置类中了。

第2部分:基础操作 

2.1 上传文件

2.1.1 小文件上传示例

如果文件不是特别大(官方文档中建议单个文件小于 5GB,直接使用 putObject 上传),可以直接使用 putObject 方法上传。常规步骤如下:

java">package com.example.demo.service;

import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.PutObjectResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.File;

@Service
public class CosService {

    @Autowired
    private COSClient cosClient;

    @Value("${tencent.cos.bucket-name}")
    private String bucketName;

    /**
     * 上传文件到 COS
     *
     * @param localFilePath 本地文件路径,如 "C:/Users/xxx/Desktop/test.jpg"
     * @param key           在 COS 上的 key,如 "images/test.jpg"
     * @return 文件访问链接(如果是公共读桶,可以直接访问;若是私有读,需要后面另行签名或鉴权)
     */
    public String uploadFile(String localFilePath, String key) {
        // 1. 准备要上传的文件
        File localFile = new File(localFilePath);

        // 2. 构造上传请求
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, localFile);

        // 3. 调用 COSClient 的 putObject 方法上传
        PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);

        // putObjectResult 里面会有一些 ETag 等信息,如果需要可以进行处理

        // 4. 拼接访问链接
        // 通常 COS 对象的访问链接格式:
        // https://<bucketName>.cos.<region>.myqcloud.com/<key>
        String url = String.format("https://%s.cos.%s.myqcloud.com/%s", 
                bucketName, // my-bucket-1250000000
                "ap-shanghai", // 这里要和你的实际地区对应
                key
        );
        return url;
    }
}
  • key 可以视为“在 COS 上的文件路径”。
  • 这里的 url 如果是公共读的存储桶,就可以直接访问;如果是私有读的存储桶,直接访问会 403 Forbidden,需要在后端生成带签名的 URL 或者使用临时密钥签名来访问(这部分会在访问权限的章节里详细讲解)。

上传的注意点:

  • 如果 Key 中包含类似 / 的分隔符,就会看起来像一个目录结构,实际上 COS 里的对象是平面的,不存在真正的“文件夹”概念,但前缀带来的层级感可以让管理更有条理。
  • 如果你要覆盖同名文件,直接 putObject 同样的 Key 即可,会被覆盖。
  • 若要在后端生成 Key(比如使用 UUID 或按日期命名),可以在后端代码里动态拼接。

2.1.2 大文件(> 5GB)上传

大文件的情况,需要使用分块上传(也叫 Multipart Upload),否则如果直接 putObject 超过5GB会报错。我们在第3部分:大文件高级功能再做详细的流程讲解和代码演示,这里先埋个伏笔,让你知道有这个限制即可。


2.2 下载文件

从 COS 下载文件到本地,可以使用 getObject 方法。以下代码示例演示了从 COS 下载一个对象并保存到本地:

java">public String downloadFile(String key, String localFilePath) {
    // localFilePath 如 "C:/Users/xxx/Desktop/downloaded.jpg"
    File downFile = new File(localFilePath);
    cosClient.getObject(
            new com.qcloud.cos.model.GetObjectRequest(bucketName, key),
            downFile
    );
    return "Download success. File saved to: " + localFilePath;
}

下载为流

有时候我们并不想直接写到本地文件,而是需要获取一个 InputStream 在程序里使用(比如直接返回给用户或做其他处理)。可以这样获取:

java">public InputStream downloadFileAsStream(String key) {
    com.qcloud.cos.model.S3Object s3Object = cosClient.getObject(bucketName, key);
    // 里面封装了文件内容的输入流
    return s3Object.getObjectContent();
}
  • 拿到流之后,请注意使用完要及时关闭,避免资源泄露。
  • 如果是给浏览器下载,需要自己包装成 ResponseEntity<InputStreamResource> 或者其他形式输出给前端,这就属于 Spring MVC 的文件下载范畴了。

2.3 删除文件

删除对象也很简单,只需要调用 deleteObject 方法即可:

java">public void deleteFile(String key) {
    cosClient.deleteObject(bucketName, key);
}

如果要一次性删除多条对象,可以使用批量删除的方法 deleteObjects,它接受一个 DeleteObjectsRequest 对象:

java">public void deleteFiles(List<String> keys) {
    DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucketName);
    List<DeleteObjectsRequest.KeyVersion> keyList = keys.stream()
            .map(k -> new DeleteObjectsRequest.KeyVersion(k))
            .collect(Collectors.toList());

    deleteObjectsRequest.setKeys(keyList);
    cosClient.deleteObjects(deleteObjectsRequest);
}

2.4 列举文件

列举存储桶中的对象(也叫“罗列”或“查询”),可以通过 listObjectslistObjectsV2 方法。常见用途包括:

  • 查询某个“目录”(即某个前缀)下的文件列表。
  • 获取所有文件的列表并分页。

示例:列举指定前缀(如 images/)下的对象:

java">public List<String> listFiles(String prefix) {
    // prefix: 可以指定一个前缀(相当于"目录"),例如 "images/"
    ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
    listObjectsRequest.setBucketName(bucketName);
    listObjectsRequest.setPrefix(prefix);
    // 设置最多列举多少个, 可以按需调整
    listObjectsRequest.setMaxKeys(100);

    ObjectListing objectListing = cosClient.listObjects(listObjectsRequest);
    List<COSObjectSummary> summaryList = objectListing.getObjectSummaries();
    List<String> keys = new ArrayList<>();
    for (COSObjectSummary summary : summaryList) {
        // 获取文件的 key
        String key = summary.getKey();
        keys.add(key);
    }
    return keys;
}
  • 如果文件很多,需要不断拿到 objectListing.isTruncated() 判断是否还有更多结果,并通过 objectListing.getNextMarker() 继续分页获取。也可以使用 listObjectsV2 提供的更灵活分页方式。
  • 也可以不设置前缀 prefix,直接列举所有对象,但通常会有权限或性能上的考虑,一般我们希望分级管理。

2.5 关于 Key 的设计与访问链接

2.5.1 Key 的设置

  • 文件名与目录结构:在 COS 上并没有真正的目录层级,但我们可以在 Key 中使用 / 来模拟目录,比如:
    • images/avatar.jpg
    • videos/2025/02/06/bigmovie.mp4
  • 动态生成 Key:很多时候,为了避免用户上传文件名重复导致冲突,会在后端生成一个随机的文件名或者带时间戳/UUID。例如:
    java">String key = "images/" + UUID.randomUUID().toString() + ".jpg";
    
  • Key 长度:官方文档中说明 Key 的最大长度可以达到 1024 字符,基本够用,但尽量不要过度嵌套或过长。

2.5.2 访问链接拼接

默认的访问域名格式是:

https://<bucketName>.cos.<region>.myqcloud.com/<key>

例如:

  • bucket: my-bucket-1250000000
  • region: ap-shanghai
  • key: images/test.jpg

最终链接可写成:

https://my-bucket-1250000000.cos.ap-shanghai.myqcloud.com/images/test.jpg

注意:

  • 如果存储桶是公共读公共读私有写,则这个链接可以直接访问文件。
  • 如果存储桶是私有读写,则通过此链接访问时,会返回 403 Forbidden。必须配合签名或临时密钥(STS)才能访问,后续访问权限部分会详细说明。
  • 腾讯云支持自定义域名绑定,如果你有自己的域名并完成了 CNAME 解析,也可以使用自定义域名进行访问。

第3部分:预签名 URL

3.1 预签名 URL 的概念

预签名 URL(Presigned URL) 是指后端基于自己的「SecretId」和「SecretKey」对一个即将访问 COS 对象的 HTTP 请求进行签名,并且在其查询参数中附上签名和有效期等信息,从而生成一个可直接访问 COS 的临时链接。

  • 这个链接在指定的过期时间内是有效的(可以使用多次,一直到过期为止),任何拿到此链接的人都能直接对对应的对象执行指定操作(GET/PUT 等),而不需要额外的鉴权或临时密钥。
  • 超过过期时间则变为无效,再访问时会返回 403 或者签名过期的错误。
  • 如果你想要限制只能下载或者只能上传,就要在生成预签名 URL 时指定对应的 HTTP 方法(GETPUT)。
  • 适用于私有读写的场景:当我们不想让对象长期公开,但又想给某人或前端临时访问/上传的权限,就可以使用预签名 URL

3.2 预签名 URL 的生成:Java 后端示例

腾讯云官方 Java SDK 提供了一个方法 generatePresignedUrl 来生成该 URL。

3.2.1 代码示例:生成“下载”用的预签名 URL

java">@Service
public class CosPresignedUrlService {
    
    @Autowired
    private COSClient cosClient;
    
    @Value("${tencent.cos.bucket-name}")
    private String bucketName;

    /**
     * 生成预签名的下载链接(GET)
     * @param key               对象在 COS 上的 key
     * @param expirationSeconds 有效时间(秒),例如 300 表示 5 分钟
     * @return 预签名 URL
     */
    public URL generatePresignedDownloadUrl(String key, int expirationSeconds) {
        // 1. 计算过期时间
        long currentTimeMillis = System.currentTimeMillis();
        Date expirationDate = new Date(currentTimeMillis + expirationSeconds * 1000L);

        // 2. 构造预签名请求
        GeneratePresignedUrlRequest request =
                new GeneratePresignedUrlRequest(bucketName, key, HttpMethodName.GET);

        // 可以指定过期时间
        request.setExpiration(expirationDate);

        // 可选:如果要限制 Content-Type、Content-Disposition 等,也可在这里设置
        // request.setContentType("image/jpeg");

        // 3. 生成 URL
        URL presignedUrl = cosClient.generatePresignedUrl(request);

        // 4. 返回给调用方
        return presignedUrl;
    }
}
  • 参数说明
    • key: 需要访问的对象路径,如 images/test.jpg
    • expirationSeconds: 此链接在多长时间内有效,单位秒。
  • 生成的 URL 可以通过 url.toString() 转成字符串返回给前端。

如果是私有读写的桶,普通情况下通过 https://bucket.cos.region.myqcloud.com/key 无法直接访问,会返回 403 Forbidden;但拿着这个预签名 URL 就可以在有效期内访问。

3.2.2 代码示例:生成“上传”用的预签名 URL

如果你希望前端直接往这个 Key 上传文件,而不是下载,那么只需要把 HttpMethodName.GET 改为 HttpMethodName.PUTHttpMethodName.POST
示例

java">public URL generatePresignedUploadUrl(String key, int expirationSeconds) {
    Date expirationDate = new Date(System.currentTimeMillis() + expirationSeconds * 1000L);
    GeneratePresignedUrlRequest request =
            new GeneratePresignedUrlRequest(bucketName, key, HttpMethodName.PUT);
    request.setExpiration(expirationDate);
    // 如果对 Content-Type 有要求,可以设置
    // request.setContentType("application/octet-stream");
    return cosClient.generatePresignedUrl(request);
}
  • 拿到该 URL 后,可以在前端用 PUT 方法直接上传文件到该对象。

3.3 前端如何使用预签名 URL

3.3.1 前端拿到“下载”用链接

  • 假设后端返回的是一个字符串形式的 URL: https://my-bucket-xxx.cos.ap-shanghai.myqcloud.com/my-object?sign=xxxxxx...
  • 前端拿到后,可以在浏览器地址栏直接访问,或者在代码里用 <a href="presignedUrl" download>下载</a>,或者在脚本里发起 GET 请求(axios.get(presignedUrl) / fetch(presignedUrl))都可以。
  • 有效期内,该链接可被重复访问多次;过期后再访问会返回 403。

3.3.2 前端拿到“上传”用链接

  • 前端可以使用 fetchaxios 等工具发送一个 PUT 请求,将文件二进制数据上传过去。
    java">const file = ...; // input type="file" 获得的 File 对象
    const presignedUrl = "后端返回的预签名URL"; 
    fetch(presignedUrl, {
      method: 'PUT',
      body: file,
      headers: {
        // 设置你希望的 Content-Type,若后端签名时指定了必须一致
        'Content-Type': file.type  
      }
    }).then(response => {
      if (response.ok) {
        console.log("Upload success!");
      } else {
        console.error("Upload failed!", response.status);
      }
    });
    
  • 同理,这个链接在有效期内可以使用多次,但同一个 Key被多次 PUT 会导致文件被覆盖;如果你只想让它上传一次,业务上可以在后端再做约束,比如生成后端记录并只允许调用一次等。

3.4 预签名 URL 常见问题解答

3.4.1 预签名 URL 是不是用一次就失效?

不是的。预签名 URL设定的有效期内是可多次使用的,无论上传或下载;只要不超时,都能成功发起请求。

  • 如果需要“只用一次就失效”的效果,需要在后端用逻辑来限制.
  • SDK 本身不会限制“只用一次”,它就是一个时间限制的签名。

3.4.2 预签名 URL 的过期时间是多长?

你可以自行设置,最短可以是几秒,最长可到七天(604800秒),这是 COS 的默认限制。如果你设置过长,有安全风险;太短则可能来不及执行操作。需要根据业务场景平衡。

3.4.3 如果文件很大,预签名 URL 能用来做分块上传吗?

  • 使用 PUT 的预签名 URL 只能一次性地上传对象,不支持自动多分片。对于小中型文件没有问题,但若文件真的很大(> 5GB)就不合适了。
  • 如果需要大文件分块上传 + 前端直传场景,官方推荐使用后端下发 STS 临时密钥,然后前端用 cos-js-sdk-v5 提供的分块上传能力。
  • 或者后端每一块都生成一个预签名 URL(对同一个 Key 的不同 partNumber),然后前端依次 PUT,但实现起来比较繁琐;因此STS 临时密钥方案会更常见、更灵活。

3.4.4 如果签名 URL 在有效期内泄露,怎么办?

  • 在有效期内,任何人拿到该 URL 都能访问/上传,所以敏感文件要慎重签发。
  • 如果有紧急情况,比如某个链接暴露,你可以立即删除对应的对象或者修改其权限(不过对签名 URL 的过期无效),最彻底的方法是删除对象更新对象,使原来的预签名失效(因为原 Key 对象不存在或已变更)。
  • 因此,预签名 URL适合做临时授权,有效期不要过长,并做好后端管理与审计。

3.4.5 预签名 URL 与临时密钥(STS)的区别

  • 预签名 URL
    • 只针对特定的对象(Key)和操作(GET/PUT)。
    • 仅在指定时间内有效,不需要前端再算签名。
    • 前端拿到后就可以直接发起请求,适用于简单的“下载一个文件”或“上传一个文件”。
  • STS 临时密钥
    • 颁发给前端一个包含 tmpSecretId/tmpSecretKey/Token 的鉴权凭证,一段时间内可对指定范围的 Key执行多种操作(上传、下载、删除、列举等)。
    • 前端可以用官方 JS SDK 进行分块上传、断点续传、批量操作等更高级的功能。
    • 控制更灵活,对应的策略也可以写得很精细。

总结

  • 如果只是一张图片或一个小文件,需要一次下载/上传,用预签名 URL非常方便。
  • 如果你需要大量、频繁、灵活的操作,以及分块上传等功能,则STS 临时密钥更适合。

 第四部分:在微信小程序前端使用预签名 URL 分片上传到腾讯云 COS

后端负责:

  • 调用 COS 的 InitiateMultipartUpload,拿到 uploadId
  • 计算分片数量/大小后,依次为每个 partNumber 生成一个 PUT 预签名 URL
  • 返回这些预签名 URL 给前端。
  • 前端所有分片上传完毕后,调用后端的“完成合并”接口,后端执行 CompleteMultipartUpload
  • 返回最终的访问地址给前端或存入数据库。

前端负责:

  • 读取文件并切分为多个分片,每个分片对应一个预签名 URL。
  • 通过 wx.request (PUT) 将分片数据直传到 COS(不经过后端)。
  • 失败后可重试单个分片,不用重新上传全部。
  • 全部分片上传成功后,调用后端“完成合并”接口,让后端调用 COS 进行合并。

4.1 后端:三个核心接口

我们要提供给前端三个主要 API

  • Init:初始化分片上传,获取 uploadId 和每个分块的预签名 URL
    • POST /initiate-upload?fileName=xxx&fileSize=xxx
  • Complete:完成分片上传
    • POST /complete-upload
  • (可选)Abort:如果需要中止上传,可以提供
    • POST /abort-upload (本文不做展开)

4.1.1 Init接口:InitiateMultipartUpload + 生成预签名链接

示例代码:

java">@RestController
@RequestMapping("/api/upload")
public class MultipartUploadController {

    @Autowired
    private COSClient cosClient;

    @Value("${tencent.cos.bucket-name}")
    private String bucketName;

    // ===================== 1. INITIATE UPLOAD ===================== //
    @PostMapping("/initiate-upload")
    public InitMultipartUploadResponse initiateUpload(
            @RequestParam String fileName,
            @RequestParam long fileSize
    ) {
        // 1. 计算对象在 COS 上的存储 key
        //    这里可以根据业务需求自定义生成,例如加上日期、用户ID等
        //    也可以直接使用 fileName。注意如果多个用户上传相同名字会覆盖,因此一般用UUID或别的规则
        String cosKey = "uploads/" + System.currentTimeMillis() + "_" + fileName;

        // 2. 发起分块上传请求,拿到 uploadId
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, cosKey);
        InitiateMultipartUploadResult result = cosClient.initiateMultipartUpload(request);
        String uploadId = result.getUploadId();

        // 3. 确定分片大小和分片数量(前后端都要一致)
        //    一般每片 5MB~10MB 较常见。此处举例 5MB
        long partSize = 5L * 1024L * 1024L; // 5MB
        int partCount = (int) (fileSize / partSize);
        if (fileSize % partSize != 0) {
            partCount++;
        }

        // 4. 为每个分块生成 PUT 预签名 URL(后端一次性生成并返回给前端)
        List<PartInfo> partList = new ArrayList<>();
        for (int i = 1; i <= partCount; i++) {
            // 分块序号必须从 1 开始递增
            int partNumber = i;

            // 过期时间,可自定义,这里示例设为1小时
            Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);

            // 构造预签名请求
            GeneratePresignedUrlRequest urlRequest = new GeneratePresignedUrlRequest(
                    bucketName,
                    cosKey,
                    HttpMethodName.PUT
            );
            urlRequest.setExpiration(expiration);
            // 这里要指定分块参数 partNumber & uploadId
            // COS 在接收分片上传时,需要这两个信息
            urlRequest.addRequestParameter("partNumber", String.valueOf(partNumber));
            urlRequest.addRequestParameter("uploadId", uploadId);

            // 生成预签名 URL
            URL presignedUrl = cosClient.generatePresignedUrl(urlRequest);

            // 封装返回给前端
            PartInfo partInfo = new PartInfo();
            partInfo.setPartNumber(partNumber);
            partInfo.setPresignedUrl(presignedUrl.toString());
            partList.add(partInfo);
        }

        // 5. 返回给前端
        InitMultipartUploadResponse resp = new InitMultipartUploadResponse();
        resp.setBucket(bucketName);
        resp.setKey(cosKey);
        resp.setUploadId(uploadId);
        resp.setPartSize(partSize);
        resp.setPartCount(partCount);
        resp.setPartInfos(partList);

        return resp;
    }

    // 响应体示例
    public static class InitMultipartUploadResponse {
        private String bucket;
        private String key;
        private String uploadId;
        private long partSize;
        private int partCount;
        private List<PartInfo> partInfos;
        // getters/setters ...
    }

    public static class PartInfo {
        private int partNumber;
        private String presignedUrl;
        // getters/setters ...
    }
}

解释

  • InitiateMultipartUploadRequest:向 COS 发起一个分块上传请求,会返回一个 uploadId。这是后续所有分块上传和最终合并都需要的。
  • partNumber:COS 要求分块编号从 1 开始连续递增,最多 10000 块
  • generatePresignedUrl:通过添加请求参数 partNumberuploadId,来告诉 COS “这是第几块,该块的 uploadId 是多少”。
  • 分片大小:示例里用 5MB,真实场景可根据文件大小、用户网络情况自行决定;COS 要求单块最小 1MB(最后一块可以小于 1MB)。
  • 返回给前端一个 JSON,包含:
    • uploadId
    • 每个分片对应的 PUT 预签名 URL
    • 分片大小 partSize 和分片总数 partCount(前后端要用同样的分片逻辑)

4.1.2 Complete接口:合并分片

分片全部上传成功后,前端会把每个分片的 ETag 上传结果告诉后端,然后后端调用 CompleteMultipartUpload 来完成合并。

java">@RestController
@RequestMapping("/api/upload")
public class MultipartUploadController {

    @PostMapping("/complete-upload")
    public CompleteMultipartUploadResponse completeUpload(
            @RequestParam String key,
            @RequestParam String uploadId,
            @RequestBody List<UploadedPart> parts
    ) {
        // 1. 构造 COS 需要的 ETag 列表
        // parts 中包含 (partNumber, eTag)
        List<PartETag> partETags = new ArrayList<>();
        for (UploadedPart p : parts) {
            // 顺序要保证 partNumber 从小到大,通常前端就按顺序传回来
            partETags.add(new PartETag(p.getPartNumber(), p.getETag()));
        }

        // 2. 发起 CompleteMultipartUpload
        CompleteMultipartUploadRequest completeReq =
                new CompleteMultipartUploadRequest(bucketName, key, uploadId, partETags);
        CompleteMultipartUploadResult compResult = cosClient.completeMultipartUpload(completeReq);

        // 3. 获取最终访问地址
        String finalUrl = compResult.getLocation();

        // 4. 返回给前端,或存数据库
        CompleteMultipartUploadResponse resp = new CompleteMultipartUploadResponse();
        resp.setLocation(finalUrl);
        resp.setKey(key);
        return resp;
    }

    public static class UploadedPart {
        private int partNumber;
        private String eTag;
        // getters/setters ...
    }

    public static class CompleteMultipartUploadResponse {
        private String key;
        private String location;
        // getters/setters ...
    }
}

解释

  • 前端在每次分块上传成功时,COS 会返回一个 ETag(或通过 wx.request 的响应头获取),前端要保存这些 ETag
  • 当所有分块都上传完成后,前端把所有 (partNumber, ETag) 的列表传给后端。
  • 后端调用 completeMultipartUpload,COS 会把所有已上传的分块合并为一个完整对象。
  • 合并后得到一个 location 字段,即对象最终的访问地址,比如: https://my-bucket-1250000000.cos.ap-shanghai.myqcloud.com/uploads/167575121231_myvideo.mp4
  • 你可以将这个地址存到数据库里,或者以 JSON 形式返回给前端。

4.2 微信小程序前端示例

下面是一个示例,演示怎么在微信小程序端实现分片、请求预签名 URL、逐片 PUT 上传、并在最后完成合并的流程。

注意:小程序里读取本地文件可以用 wx.chooseMessageFilewx.chooseMedia 来获得文件临时路径,然后用 wx.getFileSystemManager() 读取文件的 ArrayBuffer。请根据小程序 API 版本自行检查。

4.2.1 前端数据结构

java">// 用于记录每个分块的信息
class PartItem {
  constructor(partNumber, start, end, url) {
    this.partNumber = partNumber;    // 分块序号
    this.start = start;             // 文件分块起始位置(字节)
    this.end = end;                 // 文件分块结束位置(字节)
    this.url = url;                 // 对应的预签名 PUT URL
    this.etag = "";                 // 上传成功后,从响应中解析的 ETag
  }
}

4.2.2 核心流程

  1. 选择文件 -> 读取文件信息(大小、名称)
  2. 向后端发起 /initiate-upload 请求 -> 得到 uploadIdpartSizepartCount、以及每个 part 的 presignedUrl
  3. 本地切片 -> 用小程序的 FileSystemManager.readFile 读取 ArrayBuffer,并按 partSize 切分。
  4. 逐片/并行上传:对每个 part 调用 wx.request (method=PUT),上传到其对应的 presignedUrl
  5. 记录 ETag:COS 返回 ETag,存到 partItem.etag
  6. 所有分片成功后 -> 调用后端 /complete-upload,携带 uploadIdkey、以及所有 (partNumber, ETag)
  7. 后端执行合并,返回最终访问地址。前端提示成功,或做后续业务逻辑。

4.2.3 代码示例

下面是一段相对完整的示例(可根据自己项目需求来改写),演示在小程序端如何实现这些步骤。

假设

  • 服务器地址为 https://example.com
  • 我们在 initiateUpload()completeUpload() 接口里,分别对应 /api/upload/initiate-upload/api/upload/complete-upload
java">Page({
  data: {
    fileName: '',
    fileSize: 0,
    tempFilePath: '',     // 小程序选择的临时文件路径
    partSize: 5 * 1024 * 1024, // 后端约定的分片大小(从后端返回为准,这里只是演示)
    partList: [],         // PartItem数组
    key: '',
    uploadId: '',
    uploadProgress: 0,    // 总体上传进度
  },

  // 1. 选择文件
  chooseFile() {
    wx.chooseMessageFile({
      count: 1,
      type: 'file',
      success: (res) => {
        const file = res.tempFiles[0];
        this.setData({
          fileName: file.name,
          fileSize: file.size,
          tempFilePath: file.path,
        });
      }
    });
  },

  // 2. INIT接口:后端返回uploadId、partInfos
  initiateUpload() {
    if (!this.data.fileName) {
      wx.showToast({ title: '请先选择文件', icon: 'none' });
      return;
    }
    // 调用后端 /initiate-upload 接口
    wx.request({
      url: 'https://example.com/api/upload/initiate-upload',
      method: 'POST',
      data: {
        fileName: this.data.fileName,
        fileSize: this.data.fileSize,
      },
      header: { 'content-type': 'application/x-www-form-urlencoded' },
      success: (res) => {
        if (res.statusCode === 200) {
          const resp = res.data;
          // 拿到后台返回的 key, uploadId, partSize, partCount, partInfos(每块的 presignedUrl)
          this.setData({
            key: resp.key,
            uploadId: resp.uploadId,
            partSize: resp.partSize,
          });
          // 生成 partList 数组(也可以直接用后端返回的partInfos)
          const partList = [];
          resp.partInfos.forEach((info) => {
            const partItem = new PartItem(info.partNumber, 0, 0, info.presignedUrl);
            partList.push(partItem);
          });
          this.setData({ partList });
          wx.showToast({ title: 'Init 分片成功', icon: 'none' });
        } else {
          wx.showToast({ title: 'Init 分片失败', icon: 'none' });
        }
      },
      fail: () => {
        wx.showToast({ title: '网络错误', icon: 'none' });
      }
    });
  },

  // 3. 分片并上传
  uploadFileInParts() {
    const { tempFilePath, fileSize, partSize, partList } = this.data;
    if (!tempFilePath || !partList.length) {
      wx.showToast({ title: '请先 INIT', icon: 'none' });
      return;
    }

    // 读取整个文件为 ArrayBuffer
    const fs = wx.getFileSystemManager();
    fs.readFile({
      filePath: tempFilePath,
      success: (readRes) => {
        const arrayBuffer = readRes.data; // 全文件数据
        // 切片 & 上传
        this._uploadParts(arrayBuffer, fileSize, partSize, partList);
      },
      fail: (e) => {
        console.error(e);
        wx.showToast({ title: '读取文件失败', icon: 'none' });
      }
    });
  },

  // 内部方法:切分并上传每个块
  _uploadParts(arrayBuffer, fileSize, partSize, partList) {
    let offset = 0;
    for (let i = 0; i < partList.length; i++) {
      const partItem = partList[i];
      const start = offset;
      let end = start + partSize;
      if (end > fileSize) end = fileSize;
      offset = end;

      // 记录到 partItem,方便后续调试
      partItem.start = start;
      partItem.end = end;
    }

    // 这里示例用 "并行" or "串行" 均可,看需求
    this._uploadPartSerial(arrayBuffer, 0); 
    // 或使用 Promise.all 并行上传 _uploadPartParallel(...) 也行
  },

  // 串行上传(简单演示,可控错误处理;并行逻辑更复杂)
  _uploadPartSerial(arrayBuffer, index) {
    const { partList } = this.data;
    if (index >= partList.length) {
      // 全部分片上传结束
      wx.showToast({ title: '分片全部上传成功', icon: 'none' });
      return;
    }

    const partItem = partList[index];
    const sliceData = arrayBuffer.slice(partItem.start, partItem.end); // 截取分片
    const url = partItem.url;

    // PUT 上传
    wx.request({
      url: url,            // 预签名URL
      method: 'PUT',
      data: sliceData,
      header: {
        'Content-Type': 'application/octet-stream', // 必须是二进制流
      },
      success: (res) => {
        // COS 会在响应头中带 ETag
        const eTag = res.header['ETag'] || res.header['Etag'];
        partItem.etag = eTag;

        // 更新进度
        const progress = Math.floor(((index + 1) / partList.length) * 100);
        this.setData({ uploadProgress: progress });
        console.log(`Part ${partItem.partNumber} uploaded, ETag=${eTag}`);

        // 递归上传下一个分片
        this._uploadPartSerial(arrayBuffer, index + 1);
      },
      fail: (err) => {
        console.error(err);
        wx.showToast({ title: `Part ${partItem.partNumber} 上传失败`, icon: 'none' });
      }
    });
  },

  // 4. 全部分片上传完毕 -> 调用 /complete-upload
  completeUpload() {
    const { key, uploadId, partList } = this.data;
    if (!uploadId || !partList.length) {
      wx.showToast({ title: '还未init或上传', icon: 'none' });
      return;
    }

    // 构造后端需要的 ETag 信息
    const partsParam = partList.map(p => ({
      partNumber: p.partNumber,
      eTag: p.etag,
    }));

    wx.request({
      url: 'https://example.com/api/upload/complete-upload',
      method: 'POST',
      data: {
        key: key,
        uploadId: uploadId,
        parts: partsParam, 
      },
      header: {
        'content-type': 'application/json',
      },
      success: (res) => {
        if (res.statusCode === 200) {
          wx.showToast({ title: '合并成功', icon: 'none' });
          console.log('Final Location:', res.data.location);
        } else {
          wx.showToast({ title: '合并失败', icon: 'none' });
        }
      },
      fail: (err) => {
        console.error(err);
        wx.showToast({ title: '请求失败', icon: 'none' });
      }
    });
  }
});

几点说明

  • 分片读取
    • arrayBuffer.slice(partItem.start, partItem.end) 得到对应字节区间的数据。
    • 注意要把 Content-Type 设为 'application/octet-stream' 或与后端一致的类型。
  • 并行与串行
    • 样例里是串行上传,每个分块上传成功后再传下一个,逻辑简单但速度较慢。
    • 可以改造成并行(如 Promise.all),同时发起多个请求来加快速度。但要做好失败重试、并发控制等。
  • ETag
    • COS 返回的 ETag 一般包含引号,如 "9f6203fa3e9f472b89d9c39e4b0736e7", 你可以保存原样,或者去掉引号再传后端都行(后端合并时不区分引号)。
  • partSizepartCount
    • 与后端协商一致,后台 initiateUpload() 返回的 partSize 应该与前端实际分割文件的大小匹配,否则上传可能出错。
  • 失败重试
    • 如果某个分块上传失败,可再次调用 PUT 同一个 URL(在预签名 URL 过期前)进行重试。
    • 如果预签名 URL 过期或 uploadId 因故失效,就需要从头再执行 initiate-upload

第五部分:从腾讯云 COS 使用预签名 URL 来进行分片下载到微信小程序端 

5.1 后端生成预签名 URL 的接口

思路

  • 通常只需要一个带 GET 权限的预签名 URL,就可以对同一个对象多次发起请求(包括带有 Range 头的部分下载)。
  • 只要在有效期内、且请求方法(GET)一致,就能成功下载任何 Range的数据分段。

在后端中可以提供一个简单的 API,例如:GET /api/download/presign?key=xxxx 用来返回一个带签名的临时下载链接。

java">@RestController
@RequestMapping("/api/download")
public class PresignedDownloadController {

    @Autowired
    private COSClient cosClient;

    @Value("${tencent.cos.bucket-name}")
    private String bucketName;

    /**
     * 生成预签名的下载URL (GET),可支持 Range 分块下载
     */
    @GetMapping("/presign")
    public Map<String, String> generatePresignedDownloadUrl(@RequestParam String key) {
        // 1. 过期时间,比如 1 小时 (3600秒)
        Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);

        // 2. 构造预签名请求
        GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest(bucketName, key, HttpMethodName.GET);
        req.setExpiration(expiration);

        // 3. 生成 URL
        URL presignedUrl = cosClient.generatePresignedUrl(req);

        // 4. 返回给前端
        Map<String, String> resp = new HashMap<>();
        resp.put("presignedUrl", presignedUrl.toString());
        return resp;
    }
}

说明

  • 这个 presignedUrl有效期内可多次使用,用于下载同一个 Key 对象的任何部分(或整个文件)。
  • 如果你的存储桶是“私有读”,那用户想直接下载这个对象必须通过签名访问——这个预签名 URL就提供了临时可访问的能力。
  • 如果文件较大,前端可以带 Range 头分段下载;也可以直接一次性下载整个文件(不分段)——都共用这一个 URL。

5.2 前端(微信小程序)实现分片下载并合并

5.2.1 整体思路

  • 获取文件信息:知道文件大小 (可以由后端告知,也可以前端先通过 HEAD 请求获取 Content-Length)。
  • 确定分块大小(chunkSize),比如 2MB、5MB 等。
  • 循环/并行对同一个 presignedUrl 发送带有 Range 头的 GET 请求,获取第 i 块数据。
    • 请求示例:Range: bytes=start-end
  • 拼接每块返回的二进制数据,形成一个完整的 ArrayBuffer。
    • 小程序中可把分段数据存到 ArrayBuffer 数组,然后最后合并。
    • 或者逐片写入小程序的临时文件,再在所有块结束后得到一个完整文件。
  • 保存或预览
    • 在小程序里可以把合并后的 ArrayBuffer 写入临时文件,然后打开分享等后续操作。

5.2.2 示例:单个预签名 URL + 多次 Range 请求

小程序页面脚本

示例代码如下,演示一个最小可行的流程(串行下载)。你可以根据需要改成并行、多线程等。

javascript">Page({
  data: {
    presignedUrl: '',
    fileKey: 'videos/test.mp4', // 需要下载的对象key
    fileSize: 0,
    chunkSize: 2 * 1024 * 1024, // 2MB分片大小
    totalChunks: 0,
    downloadedChunks: 0,
    downloadProgress: 0,
    fileBufferArray: [],  // 存放每块 ArrayBuffer
  },

  // 1. 向后端请求预签名URL
  getPresignedUrl() {
    wx.request({
      url: 'https://example.com/api/download/presign?key=' + this.data.fileKey,
      method: 'GET',
      success: (res) => {
        if (res.statusCode === 200) {
          this.setData({
            presignedUrl: res.data.presignedUrl
          });
          wx.showToast({ title: '获取预签名URL成功' });
        } else {
          wx.showToast({ title: '请求失败', icon: 'none' });
        }
      },
      fail: () => {
        wx.showToast({ title: '网络错误', icon: 'none' });
      }
    });
  },

  // 2. 获取文件大小 (可由后端返回,也可以自己HEAD一下)
  getFileSize() {
    if (!this.data.presignedUrl) {
      wx.showToast({ title: '请先获取预签名链接', icon: 'none' });
      return;
    }
    // 发起 HEAD 请求获取 Content-Length
    wx.request({
      url: this.data.presignedUrl,
      method: 'HEAD',
      success: (res) => {
        if (res.statusCode === 200) {
          // 可能在 res.header["Content-Length"] 里
          const lengthStr = res.header['Content-Length'];
          const size = parseInt(lengthStr, 10);
          if (!isNaN(size)) {
            this.setData({
              fileSize: size
            });
            wx.showToast({ title: '文件大小: ' + size });
          } else {
            wx.showToast({ title: '无法获取文件大小', icon: 'none' });
          }
        } else {
          wx.showToast({ title: 'HEAD请求失败', icon: 'none' });
        }
      },
      fail: () => {
        wx.showToast({ title: '网络错误', icon: 'none' });
      }
    });
  },

  // 3. 分段下载
  startChunkDownload() {
    const { fileSize, chunkSize } = this.data;
    if (fileSize <= 0) {
      wx.showToast({ title: '文件大小未知', icon: 'none' });
      return;
    }

    // 计算分块数量
    let totalChunks = Math.ceil(fileSize / chunkSize);
    this.setData({
      totalChunks,
      downloadedChunks: 0,
      downloadProgress: 0,
      fileBufferArray: [],
    });

    // 串行下载 (从第0块到最后一块)
    this.downloadChunkSerial(0);
  },

  // 串行下载逐块
  downloadChunkSerial(index) {
    const { totalChunks } = this.data;
    if (index >= totalChunks) {
      // 全部分块都下载完毕
      this.onAllChunksDownloaded();
      return;
    }

    // 计算Range区间
    const start = index * this.data.chunkSize;
    let end = start + this.data.chunkSize - 1;
    if (end >= this.data.fileSize) {
      end = this.data.fileSize - 1;
    }

    const rangeHeader = `bytes=${start}-${end}`;
    // 发起请求
    wx.request({
      url: this.data.presignedUrl,
      method: 'GET',
      header: {
        'Range': rangeHeader,
      },
      responseType: 'arraybuffer', // 重要:表示返回二进制
      success: (res) => {
        if (res.statusCode === 206 || res.statusCode === 200) {
          // 206 Partial Content | 200 (如果请求的是整个文件)
          const chunkData = res.data; // arraybuffer
          // 将这块数据存起来
          let newFileBufferArray = this.data.fileBufferArray;
          newFileBufferArray[index] = chunkData;

          // 更新进度
          const downloadedChunks = index + 1;
          const progress = Math.floor((downloadedChunks / totalChunks) * 100);
          this.setData({
            fileBufferArray: newFileBufferArray,
            downloadedChunks,
            downloadProgress: progress
          });

          // 继续下载下一块
          this.downloadChunkSerial(index + 1);
        } else {
          wx.showToast({ title: '分块下载失败:' + res.statusCode, icon: 'none' });
        }
      },
      fail: (err) => {
        wx.showToast({ title: '网络错误:' + err.errMsg, icon: 'none' });
      }
    });
  },

  // 4. 所有分块都下载完毕,合并为完整文件
  onAllChunksDownloaded() {
    // 4.1 合并 ArrayBuffer
    const { fileBufferArray, fileSize, totalChunks } = this.data;

    // 先计算所有分片总长度
    let totalLength = 0;
    fileBufferArray.forEach(buf => {
      totalLength += buf.byteLength;
    });

    // 创建一个总长度的 ArrayBuffer
    const fullArrayBuffer = new ArrayBuffer(totalLength);
    const fullView = new Uint8Array(fullArrayBuffer);

    // 依次拷贝
    let offset = 0;
    for (let i = 0; i < totalChunks; i++) {
      const chunk = new Uint8Array(fileBufferArray[i]);
      fullView.set(chunk, offset);
      offset += chunk.byteLength;
    }

    // 4.2 写入小程序临时文件
    const fs = wx.getFileSystemManager();
    const tempFilePath = `${wx.env.USER_DATA_PATH}/downloaded_${Date.now()}.bin`; 
    // 你可以带扩展名,比如 .mp4/.jpg等

    fs.writeFile({
      filePath: tempFilePath,
      data: fullArrayBuffer,
      encoding: 'binary',
      success: () => {
        wx.showToast({ title: '下载合并完成' });
        // 这里可以做:打开文件、预览、或保存到相册(如果是图片/视频)
        console.log('File saved to:', tempFilePath);
      },
      fail: (err) => {
        console.error(err);
        wx.showToast({ title: '写文件失败', icon: 'none' });
      }
    });
  },
});

代码说明

  • getPresignedUrl(): 向后端请求一个可 GET 的预签名 URL,存到 this.data.presignedUrl
  • getFileSize(): 用 wx.request({ method: 'HEAD' }) 来获取文件大小 (Content-Length) 并保存在 fileSize。你也可以在后端一起返回文件大小信息,这样就不需要额外的 HEAD 步骤。
  • startChunkDownload():
    • 计算 totalChunks = ceil(fileSize / chunkSize)
    • 初始化进度状态;
    • 开始执行 downloadChunkSerial(0)
  • downloadChunkSerial(index):
    • 计算本块的 Range(bytes=start-end),发起请求。
    • 由于是串行,每次块下载完成后再进入下一块。
    • 若需要并行,可以用类似于分块上传那样的并行池控制,这里就不展开。
  • 响应状态码 206 表示分块下载成功 (Partial Content),如果下载的是整个文件(从头到尾只请求一次),可能就是 200
  • onAllChunksDownloaded():
    • 先把所有分块的 ArrayBuffer 依次拼接成一个新的 ArrayBuffer
    • 再调用 fs.writeFile 写入到小程序的 USER_DATA_PATH 下生成一个临时文件;你也可以手动再改后缀名(比如 .mp4)方便后续使用。
    • 还可以使用 wx.openDocument(只支持部分格式)或 wx.previewMedia(如果是视频/图片)来做预览,也可以让用户另存。

关于 Range 请求的说明

  • HTTP Range 头格式:bytes=start-end
  • 如果 end 超过文件实际大小,服务器可能返回到文件末尾,并给出 206 或 200 等状态。
  • COS 对 Range 请求的支持:一般默认支持,但要注意你生成预签名 URL 时,如果你在签名里限制了特定的请求头,则必须保持一致,否则签名会无效。
  • 在常见用法下(generatePresignedUrl 未对 Range 做特殊限定),同一个预签名 URL 对任何 Range 请求都是合法的。

 第六部分:关键文件备份为低频存储

6.1 腾讯云 COS 存储类别概览

存储类型适用场景价格访问时效读取方式
STANDARD(标准存储)适合频繁访问的数据,如网站资源最高即时访问直接 GET
STANDARD_IA(低频存储)适合不经常访问但仍需要快速取回的文件,如备份数据较低即时访问直接 GET
ARCHIVE(归档存储)适合长期不访问的数据,如日志存档最低需解冻,最快1分钟restoreObject() 解冻,再 GET

STANDARD_IA(低频访问)与 STANDARD(标准)相比,存储费用更低,但每次读取会额外产生请求费用
ARCHIVE(归档)存储费用最低,但读取前必须先解冻(冷数据恢复),适用于长期存档的数据。

6.2 代码示例

java">import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.CopyObjectRequest;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.StorageClass;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class CosStorageService {

    @Autowired
    private COSClient cosClient;

    @Value("${tencent.cos.bucket-name}")
    private String bucketName;

    /**
     * 迁移文件到低频存储(STANDARD_IA)
     * @param key 文件的对象键(路径),例如 "images/sample.jpg"
     */
    public void migrateToStandardIA(String key) {
        try {
            // 1. 创建 CopyObjectRequest (源桶和目标桶都是同一个)
            CopyObjectRequest copyRequest = new CopyObjectRequest(bucketName, key, bucketName, key);

            // 2. 设置新的存储类别为 STANDARD_IA
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setHeader("x-cos-storage-class", StorageClass.Standard_IA.toString()); 
            copyRequest.setNewObjectMetadata(metadata);

            // 3. 执行存储类别转换
            cosClient.copyObject(copyRequest);

            System.out.println("成功迁移文件:" + key + " 到 STANDARD_IA 存储");
        } catch (Exception e) {
            System.err.println("迁移失败:" + e.getMessage());
        }
    }
}
  • 创建 CopyObjectRequest

    java">CopyObjectRequest copyRequest = new CopyObjectRequest(bucketName, key, bucketName, key);
    
    • 源桶目标桶 设置为同一个(即 bucketName, key 复制到 bucketName, key
    • 只改变文件存储类别,不改动其他文件属性
  • 设置新存储类别

    java">ObjectMetadata metadata = new ObjectMetadata();
    metadata.setHeader("x-cos-storage-class", StorageClass.Standard_IA.toString());
    
    • x-cos-storage-class 设置为 STANDARD_IA
    • Tencent COS 服务器会自动处理存储类别变更
  • 执行 copyObject()

    java">cosClient.copyObject(copyRequest);
    
    • COS API 直接修改存储类别,不会影响文件路径
    • 迁移后文件仍然在 相同 Key 下,但存储类别已经变更

http://www.niftyadmin.cn/n/5845021.html

相关文章

Axure设计教程:动态排名图(中继器实现)

一、开篇 在Axure原型设计中&#xff0c;动态图表是展示数据和交互效果的重要元素。今天&#xff0c;我们将学习如何使用中继器来创建一个动态的排名图&#xff0c;该图表不仅支持自动轮播&#xff0c;还可以手动切换&#xff0c;极大地增强了用户交互体验。此教程旨在提供一个…

Macbook ToDesk 无法连接网络

描述 网络连接的是 Wi-Fi&#xff0c;打开浏览器能跟正常浏览内容&#xff0c;说明 Wi-Fi 是正常的。 现象&#xff1a;显示网络连接失败&#xff0c;一直无法登陆&#xff01; 检查防火墙是没有阻止ToDesk 的任何连接&#xff0c;说明防火墙也是正常的。 解决 检查登录项&a…

idea整合deepseek实现AI辅助编程

1.File->Settings 2.安装插件codegpt 3.注册deepseek开发者账号&#xff0c;DeepSeek开放平台 4.按下图指示创建API KEY 5.回到idea配置api信息&#xff0c;File->Settings->Tools->CodeGPT->Providers->Custom OpenAI API key填写deepseek的api key Chat…

DeepSeek 和 ChatGPT 的商业化发展前景对比

在大语言模型商业化的赛道上&#xff0c;DeepSeek 和 ChatGPT 都展现出了独特的潜力。这两款模型由于技术特点、市场定位和发展策略的不同&#xff0c;在商业化发展前景上各有千秋。 市场定位与应用场景 ChatGPT 定位为通用性的大语言模型&#xff0c;旨在为全球用户提供广泛…

01什么是DevOps

在日常开发中&#xff0c;运维人员主要负责跟生产环境打交道&#xff0c;开发和测试&#xff0c;不去操作生产环境的内容&#xff0c;生产环境由运维人员操作&#xff0c;这里面包含了环境的搭建、系统监控、故障的转移&#xff0c;还有软件的维护等内容。 当一个项目开发完毕&…

【Block总结】PSA,金字塔挤压注意力,解决传统注意力机制在捕获多尺度特征时的局限性

论文信息 标题: EPSANet: An Efficient Pyramid Squeeze Attention Block on Convolutional Neural Network论文链接: arXivGitHub链接: https://github.com/murufeng/EPSANet 创新点 EPSANet提出了一种新颖的金字塔挤压注意力&#xff08;PSA&#xff09;模块&#xff0c;旨…

【学习总结|DAY036】Vue工程化+ElementPlus

引言 在前端开发领域&#xff0c;Vue 作为一款流行的 JavaScript 框架&#xff0c;结合 ElementPlus 组件库&#xff0c;为开发者提供了强大的构建用户界面的能力。本文将结合学习内容&#xff0c;详细介绍 Vue 工程化开发流程以及 ElementPlus 的使用&#xff0c;助力开发者快…

Python3+Request+Pytest+Allure+Jenkins 接口自动化测试[手动写的和AI写的对比]

我手动写的参考 总篇:Python3+Request+Pytest+Allure+Jenkins接口自动化框架设计思路_jenkins python3+request-CSDN博客 https://blog.csdn.net/fen_fen/article/details/144269072 下面是AI写的:Python3+Request+Pytest+Allure+Jenkins 接口自动化测试[AI文章框架] 在软…