当前位置:   article > 正文

这才是企业级的 oss-spring-boot-starter,真心牛x!

oss-spring-boot-starter

cc9dcabdcd9cca37d016a9fda42f406d.jpeg

程序员的成长之路

互联网/程序员/技术/资料共享 

关注

阅读本文大概需要 10 分钟。

来自:juejin.cn/post/7211828279430021180

本文主要讲解企业级OSS对象存储服务Spring Boot Starter制作,开箱即用,为项目进行赋能。基于AmazonS3协议,适配市面上的对象存储服务如:阿里云OSS、腾讯COS、七牛云OSS、MInio等等

什么是OSS?

OSS(Object Storage Service),对象存储服务,对象存储服务是一种使用HTTP API存储和检索对象的工具。就是将系统所要用的文件上传到云硬盘上,该云硬盘提供了文件下载、上传、预览等一系列服务,具备版本,权限控制能力,具备数据生命周期管理能力这样的服务以及技术可以统称为OSS

OSS在项目中的使用

OSS对象存储在目前大部分项目中必不可少的存在,如下图所示。

863d8109f681641343b583a4199d8018.jpeg

  1. 一般项目使用OSS对象存储服务,主要是对图片、文件、音频等对象集中式管理权限控制,管理数据生命周期等等,提供上传,下载,预览,删除等功能。

  2. 通过OSS部署前端项目。

什么是AmazonS3

  • https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/Welcome.html

Amazon Simple Storage Service(Amazon S3,Amazon简便存储服务)是 AWS 最早推出的云服务之一,经过多年的发展,S3 协议在对象存储行业事实上已经成为标准。

  1. 提供了统一的接口 REST/SOAP 来统一访问任何数据

  2. 对 S3 来说,存在里面的数据就是对象名(键),和数据(值)

  3. 不限量,单个文件最高可达 5TB,可动态扩容。

  4. 高速。每个 bucket 下每秒可达 3500 PUT/COPY/POST/DELETE 或 5500 GET/HEAD 请求。

  5. 具备版本,权限控制能力

  6. 具备数据生命周期管理能力

作为一个对象存储服务,S3 功能真的很完备,行业的标杆,目前市面上大部分OSS对象存储服务都支持AmazonS3,本文主要讲解的就是基于AmazonS3实现我们自己的 Spring Boot Starter。

阿里云OSS兼容S3

07893ea6522e01c32a798de56c55cd79.jpeg

七牛云对象存储兼容S3

1e46c302c4ace8607eb57afffc7617b5.jpeg

腾讯云COS兼容S3

3cd88f76c05273ce4e0cf46829a10e01.jpeg

Minio兼容S3

5558e73710d5d107058cd02a893cbd03.jpeg

我们为什么要基于AmazonS3实现 Spring Boot Starter

原因:市面上OSS对象存储服务基本都支持AmazonS3,我们封装我们的自己的starter那么就必须考虑适配,迁移,可扩展。比喻说我们今天使用的是阿里云OSS对接阿里云OSS的SDK,后天我们使用的是腾讯COS对接是腾讯云COS,我们何不直接对接AmazonS3实现呢,这样后续不需要调整代码,只需要去各个云服务商配置就好了。

创建一个SpringBoot项目

如下图所示:创建一个SpringBoot项目。

我们取名为oss-spring-boot-starter。

a9cd66e7a526d6b8a03e8ed67ec3c067.jpeg

如下图所示,创建成功,让我们进入制作的过程吧。

9b477ca6ab4e14ffa509a5ec16f7424d.jpeg

找到我们需要的依赖

打开maven仓库,搜索minio

地址

  • https://mvnrepository.com/

50e2498cdb988c23ca164c2fffaa46cb.jpeg

这里我们选择第一个,点进去后我们选择1.12.423版本,做演示。

3a58990e9c59fdd836eab97b0ee35091.jpeg

  1. <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
  2. <dependency>
  3.     <groupId>com.amazonaws</groupId>
  4.     <artifactId>aws-java-sdk-s3</artifactId>
  5.     <version>1.12.423</version>
  6. </dependency>
本项目的Pom文件
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4.     <modelVersion>4.0.0</modelVersion>
  5.     <parent>
  6.         <groupId>org.springframework.boot</groupId>
  7.         <artifactId>spring-boot-starter-parent</artifactId>
  8.         <version>2.7.9</version>
  9.         <relativePath/> <!-- lookup parent from repository -->
  10.     </parent>
  11.     <groupId>com.qing</groupId>
  12.     <artifactId>oss-spring-boot-starter</artifactId>
  13.     <version>0.0.1-SNAPSHOT</version>
  14.     <name>oss-spring-boot-starter</name>
  15.     <description>Demo oss-spring-boot-starter</description>
  16.     <properties>
  17.         <java.version>1.8</java.version>
  18.         <aws.version>1.12.423</aws.version>
  19.         <hutool.version>5.8.5</hutool.version>
  20.     </properties>
  21.     <dependencies>
  22.         <dependency>
  23.             <groupId>org.springframework.boot</groupId>
  24.             <artifactId>spring-boot-starter</artifactId>
  25.         </dependency>
  26.         <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
  27.         <dependency>
  28.             <groupId>com.amazonaws</groupId>
  29.             <artifactId>aws-java-sdk-s3</artifactId>
  30.             <version>${aws.version}</version>
  31.         </dependency>
  32.         <dependency>
  33.             <groupId>cn.hutool</groupId>
  34.             <artifactId>hutool-core</artifactId>
  35.             <version>${hutool.version}</version>
  36.         </dependency>
  37.         <dependency>
  38.             <groupId>org.springframework.boot</groupId>
  39.             <artifactId>spring-boot-starter-test</artifactId>
  40.             <scope>test</scope>
  41.         </dependency>
  42.         <dependency>
  43.             <groupId>org.projectlombok</groupId>
  44.             <artifactId>lombok</artifactId>
  45.             <version>RELEASE</version>
  46.             <scope>compile</scope>
  47.         </dependency>
  48.     </dependencies>
  49. </project>

编写OssProperties

代码如下,注释写的非常清楚了。 @ConfigurationProperties报红不用管后门会解决。

  • @Data:注解是lombok,生成get set方法的。

  • @ConfigurationProperties(prefix = "oss"):将配置文件中oss开头的属性绑定到此对象中

大概就是如果想要使用我们的jar他的配置文件有这些配置

  1. oss.endpoint=xxx
  2. oss.accessKey=xxx
  3. oss.secretKey=xxx
  1. /**
  2.  * @Author JiaQIng
  3.  * @Description Oss配置类
  4.  * @ClassName OssProperties
  5.  * @Date 2023/3/18 17:51
  6.  **/
  7. @Data
  8. @ConfigurationProperties(prefix = "oss")
  9. public class OssProperties {
  10.     /**
  11.      * 对象存储服务的URL
  12.      */
  13.     private String endpoint;
  14.     /**
  15.      * 区域
  16.      */
  17.     private String region;
  18.     /**
  19.      * true path-style nginx 反向代理和S3默认支持 pathStyle模式 {http://endpoint/bucketname}
  20.      * false supports virtual-hosted-style 阿里云等需要配置为 virtual-hosted-style 模式{http://bucketname.endpoint}
  21.      * 只是url的显示不一样
  22.      */
  23.     private Boolean pathStyleAccess = true;
  24.     /**
  25.      * Access key
  26.      */
  27.     private String accessKey;
  28.     /**
  29.      * Secret key
  30.      */
  31.     private String secretKey;
  32.     /**
  33.      * 最大线程数,默认: 100
  34.      */
  35.     private Integer maxConnections = 100;
  36. }

创建一个接口OssTemplate

OssTemplate:oss模板接口,此接口主要是对oss操作的方法的一个接口,定义为接口主要是满足可扩展原则,就是其他人使用了我们的jar包,实现此接口可以自定义相关操作。

如下面所示代码:定义了一些对oss操作的方法。

  1. /**
  2.  * @Author JiaQIng
  3.  * @Description oss操作模板
  4.  * @ClassName OssTemplate
  5.  * @Date 2023/3/18 18:15
  6.  **/
  7. public interface OssTemplate {
  8.     /**
  9.      * 创建bucket
  10.      * @param bucketName bucket名称
  11.      */
  12.     void createBucket(String bucketName);
  13.     /**
  14.      * 获取所有的bucket
  15.      * @return
  16.      */
  17.     List<Bucket> getAllBuckets();
  18.     /**
  19.      * 通过bucket名称删除bucket
  20.      * @param bucketName
  21.      */
  22.     void removeBucket(String bucketName);
  23.     /**
  24.      * 上传文件
  25.      * @param bucketName bucket名称
  26.      * @param objectName 文件名称
  27.      * @param stream 文件流
  28.      * @param contextType 文件类型
  29.      * @throws Exception
  30.      */
  31.     void putObject(String bucketName, String objectName, InputStream stream, String contextType) throws Exception;
  32.     /**
  33.      * 上传文件
  34.      * @param bucketName bucket名称
  35.      * @param objectName 文件名称
  36.      * @param stream 文件流
  37.      * @throws Exception
  38.      */
  39.     void putObject(String bucketName, String objectName, InputStream stream) throws Exception;
  40.     /**
  41.      * 获取文件
  42.      * @param bucketName bucket名称
  43.      * @param objectName 文件名称
  44.      * @return S3Object
  45.      */
  46.     S3Object getObject(String bucketName, String objectName);
  47.     /**
  48.      * 获取对象的url
  49.      * @param bucketName
  50.      * @param objectName
  51.      * @param expires
  52.      * @return
  53.      */
  54.     String getObjectURL(String bucketName, String objectName, Integer expires);
  55.     /**
  56.      * 通过bucketName和objectName删除对象
  57.      * @param bucketName
  58.      * @param objectName
  59.      * @throws Exception
  60.      */
  61.     void removeObject(String bucketName, String objectName) throws Exception;
  62.     /**
  63.      * 根据文件前置查询文件
  64.      * @param bucketName bucket名称
  65.      * @param prefix 前缀
  66.      * @param recursive 是否递归查询
  67.      * @return S3ObjectSummary 列表
  68.      */
  69.     List<S3ObjectSummary> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive);
  70. }

创建OssTemplate的实现类

如下图所示:实现OssTemplate里面的方法,调用AmazonS3JavaSDK的方法实现。

AmazonS3提供了众多的方法,这里就不写全部的了,公司要用到那些就写那些吧,后续扩展就行。

AmazonS3接口地址如下

  • https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html

此类解释:就是实现OssTemplate接口,引入AmazonS3客户端调用对应的接口。

使用的注解解释:

  • @RequiredArgsConstructor:lomnok的注解,替代@Autowired

  • @SneakyThrows:lomnok的注解,抛出异常。

  1. /**
  2.  * @Author JiaQIng
  3.  * @Description OssTemplate的实现类
  4.  * @ClassName OssTemplateImpl
  5.  * @Date 2023/3/18 19:02
  6.  **/
  7. @RequiredArgsConstructor
  8. public class OssTemplateImpl implements OssTemplate {
  9.     private final AmazonS3 amazonS3;
  10.     /**
  11.      * 创建Bucket
  12.      * AmazonS3:https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html
  13.      * @param bucketName bucket名称
  14.      */
  15.     @Override
  16.     @SneakyThrows
  17.     public void createBucket(String bucketName) {
  18.         if ( !amazonS3.doesBucketExistV2(bucketName) ) {
  19.             amazonS3.createBucket((bucketName));
  20.         }
  21.     }
  22.     /**
  23.      * 获取所有的buckets
  24.      * AmazonS3:https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html
  25.      * @return
  26.      */
  27.     @Override
  28.     @SneakyThrows
  29.     public List<Bucket> getAllBuckets() {
  30.         return amazonS3.listBuckets();
  31.     }
  32.     /**
  33.      * 通过Bucket名称删除Bucket
  34.      * AmazonS3:https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucket.html
  35.      * @param bucketName
  36.      */
  37.     @Override
  38.     @SneakyThrows
  39.     public void removeBucket(String bucketName) {
  40.         amazonS3.deleteBucket(bucketName);
  41.     }
  42.     /**
  43.      * 上传对象
  44.      * @param bucketName bucket名称
  45.      * @param objectName 文件名称
  46.      * @param stream 文件流
  47.      * @param contextType 文件类型
  48.      * AmazonS3:https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
  49.      */
  50.     @Override
  51.     @SneakyThrows
  52.     public void putObject(String bucketName, String objectName, InputStream stream, String contextType) {
  53.         putObject(bucketName, objectName, stream, stream.available(), contextType);
  54.     }
  55.     /**
  56.      * 上传对象
  57.      * @param bucketName bucket名称
  58.      * @param objectName 文件名称
  59.      * @param stream 文件流
  60.      * AmazonS3:https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
  61.      */
  62.     @Override
  63.     @SneakyThrows
  64.     public void putObject(String bucketName, String objectName, InputStream stream) {
  65.         putObject(bucketName, objectName, stream, stream.available(), "application/octet-stream");
  66.     }
  67.     /**
  68.      * 通过bucketName和objectName获取对象
  69.      * @param bucketName bucket名称
  70.      * @param objectName 文件名称
  71.      * @return
  72.      * AmazonS3:https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
  73.      */
  74.     @Override
  75.     @SneakyThrows
  76.     public S3Object getObject(String bucketName, String objectName) {
  77.         return amazonS3.getObject(bucketName, objectName);
  78.     }
  79.     /**
  80.      * 获取对象的url
  81.      * @param bucketName
  82.      * @param objectName
  83.      * @param expires
  84.      * @return
  85.      * AmazonS3:https://docs.aws.amazon.com/AmazonS3/latest/API/API_GeneratePresignedUrl.html
  86.      */
  87.     @Override
  88.     @SneakyThrows
  89.     public String getObjectURL(String bucketName, String objectName, Integer expires) {
  90.         Date date = new Date();
  91.         Calendar calendar = new GregorianCalendar();
  92.         calendar.setTime(date);
  93.         calendar.add(Calendar.DAY_OF_MONTH, expires);
  94.         URL url = amazonS3.generatePresignedUrl(bucketName, objectName, calendar.getTime());
  95.         return url.toString();
  96.     }
  97.     /**
  98.      * 通过bucketName和objectName删除对象
  99.      * @param bucketName
  100.      * @param objectName
  101.      * AmazonS3:https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html
  102.      */
  103.     @Override
  104.     @SneakyThrows
  105.     public void removeObject(String bucketName, String objectName) {
  106.         amazonS3.deleteObject(bucketName, objectName);
  107.     }
  108.     /**
  109.      * 根据bucketName和prefix获取对象集合
  110.      * @param bucketName bucket名称
  111.      * @param prefix 前缀
  112.      * @param recursive 是否递归查询
  113.      * @return
  114.      * AmazonS3:https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjects.html
  115.      */
  116.     @Override
  117.     @SneakyThrows
  118.     public List<S3ObjectSummary> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {
  119.         ObjectListing objectListing = amazonS3.listObjects(bucketName, prefix);
  120.         return objectListing.getObjectSummaries();
  121.     }
  122.     /**
  123.      *
  124.      * @param bucketName
  125.      * @param objectName
  126.      * @param stream
  127.      * @param size
  128.      * @param contextType
  129.      * @return
  130.      */
  131.     @SneakyThrows
  132.     private PutObjectResult putObject(String bucketName, String objectName, InputStream stream, long size,
  133.                                      String contextType)  {
  134.         byte[] bytes = IOUtils.toByteArray(stream);
  135.         ObjectMetadata objectMetadata = new ObjectMetadata();
  136.         objectMetadata.setContentLength(size);
  137.         objectMetadata.setContentType(contextType);
  138.         ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
  139.         // 上传
  140.         return amazonS3.putObject(bucketName, objectName, byteArrayInputStream, objectMetadata);
  141.     }
  142. }

创建OssAutoConfiguration

204d927fdaa2364db69f469cb8f443d8.jpeg

OssAutoConfiguration:自动装配配置类,自动装配的bean有AmazonS3OssTemplate

所使用的注解:

  • @RequiredArgsConstructor:lomnok的注解,替代@Autowired。

  • @EnableConfigurationProperties(OssProperties.class):自动装配我们的配置类

  • @Bean:声明式bean。

  • @ConditionalOnMissingBean:修饰bean的一个注解,当你的bean被注册之后,注册相同类型的bean,就不会成功,它会保证你的bean只有一个,即你的实例只有一个。多个会报错。

  • @ConditionalOnBean(AmazonS3.class):当给定的在bean存在时,则实例化当前Bean。

  1. /**
  2.  * @Author JiaQIng
  3.  * @Description oss配置bean
  4.  * @ClassName OssAConfiguration
  5.  * @Date 2023/3/18 18:23
  6.  **/
  7. @Configuration
  8. @RequiredArgsConstructor
  9. @EnableConfigurationProperties(OssProperties.class)
  10. public class OssAutoConfiguration {
  11.     @Bean
  12.     @ConditionalOnMissingBean
  13.     public AmazonS3 ossClient(OssProperties ossProperties) {
  14.         // 客户端配置,主要是全局的配置信息
  15.         ClientConfiguration clientConfiguration = new ClientConfiguration();
  16.         clientConfiguration.setMaxConnections(ossProperties.getMaxConnections());
  17.         // url以及region配置
  18.         AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(
  19.                 ossProperties.getEndpoint(), ossProperties.getRegion());
  20.         // 凭证配置
  21.         AWSCredentials awsCredentials = new BasicAWSCredentials(ossProperties.getAccessKey(),
  22.                 ossProperties.getSecretKey());
  23.         AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);
  24.         // build amazonS3Client客户端
  25.         return AmazonS3Client.builder().withEndpointConfiguration(endpointConfiguration)
  26.                 .withClientConfiguration(clientConfiguration).withCredentials(awsCredentialsProvider)
  27.                 .disableChunkedEncoding().withPathStyleAccessEnabled(ossProperties.getPathStyleAccess()).build();
  28.     }
  29.     @Bean
  30.     @ConditionalOnBean(AmazonS3.class)
  31.     public OssTemplate ossTemplate(AmazonS3 amazonS3){
  32.         return new OssTemplateImpl(amazonS3);
  33.     }
  34. }
ClientConfiguration对象

客户端配置,主要是全局的配置信息

看下图,有很多的配置,有的指定了默认值有的没有,可以到AmazonS3的官方文档熟悉相关配置,配置你所需要指定的配置信息等。

只有你真正的理解那些配置的作用才能避免线上的bug。 有兴趣的同学可以看一下。

2fe528f3f6a5f25a5ef256f3bf831e5d.jpeg

创建我们的spring.factories

在resources目录下新增META-INF包,下面新建spring.factories文件。

这种形式也是"约定大于配置"的体现。读过spring-boot源码的同学应该知道,这里就不给大家讲解了。

如下图所示:

  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2.   com.qing.oss.OssAutoConfiguration

执行install打包到我们的本地仓库

把springboot工程的启动类,配置文件干掉,干掉Test包。

最重要的是干掉pom文件的spring-boot-maven-plugin,要不然install报错。

cf2dfec49cbcc785538eca92ea40a3f3.jpeg

  1. <build>
  2.     <plugins>
  3.         <plugin>
  4.             <groupId>org.springframework.boot</groupId>
  5.             <artifactId>spring-boot-maven-plugin</artifactId>
  6.         </plugin>
  7.     </plugins>
  8. </build>

这样我们的一个oss-spring-boot-starter就完成了。

382bd5c1e4acbbbff05418775be7b512.jpeg

执行install打包成jar到我们的本地仓库。

96e736f7bcdf4991604d2832d44adc72.jpeg

到我们的本地仓库就能看到我们的oss-spring-boot-starter

f2ff8c88902cde9c17adc6a72d292f84.jpeg

测试

创建一个spring-boot工程当作我们的测试工程

这里就不演示怎么创建项目了。直接看下图吧

909658185562d82fa1c8b812d94a235f.jpeg

pom文件新增我们的oss-spring-boot-starter依赖

新增版本全局配置

  1. <properties>
  2.     <oss.version>0.0.1-SNAPSHOT</oss.version>
  3. </properties>

新增oss-spring-boot-starter依赖

  1. <dependency>
  2.     <groupId>com.qing</groupId>
  3.     <artifactId>oss-spring-boot-starter</artifactId>
  4.     <version>${oss.version}</version>
  5. </dependency>

刷新maven后可以看到我们依赖加进来了。

af20cb28d2893df696a0c4d3adfd878e.jpeg

解决打包没有注释的问题

可以发现我们的依赖没有注释没有Javadoc注释。

在我们的oss-string-boot-starter的pom文件下加入下面插件,重新install一下就好了。

  1. <build>
  2.     <plugins>
  3.         <!-- 在打好的jar包中保留javadoc注释,实际会另外生成一个xxxxx-sources.jar -->
  4.         <plugin>
  5.             <groupId>org.apache.maven.plugins</groupId>
  6.             <artifactId>maven-source-plugin</artifactId>
  7.             <executions>
  8.                 <execution>
  9.                     <id>attach-sources</id>
  10.                     <goals>
  11.                         <goal>jar</goal>
  12.                     </goals>
  13.                 </execution>
  14.             </executions>
  15.         </plugin>
  16.     </plugins>
  17. </build>

在我们的测试项目里面刷新一下maven可以看到已经带注释了。

a6d3b3efcb8353023695f7d85b0e53af.jpeg

配置文件添加oss-spring-boot-starter所需要的配置

这里填写你的阿里云,腾讯cos,七牛云,minio等等的配置。

下面我给大家演示的是Minio

  1. oss.endpoint=xxx
  2. oss.accessKey=xxx
  3. oss.secretKey=xxx
编写测试方法

如下图所示,编写测试方法,执行测试方法成功。

  1. @SpringBootTest
  2. class TestOssSpringBpptStarterApplicationTests {
  3.     @Autowired
  4.     private OssTemplate ossTemplate;
  5.     @Test
  6.     void contextLoads() {
  7.         ossTemplate.createBucket("oss02");
  8.     }
  9. }

df6c219ed3665324e3f42e2d9c0c9ca4.jpeg

到我的Minio中查看发现测试成功。

5b030b8c44db0aa45919c5f692261528.jpeg

后记

本文主要讲解企业级OSS对象存储服务Spring Boot Starter制作,开箱即用,为项目进行赋能。基于AmazonS3协议,适配市面上的大部分对象存储服务如:阿里云OSS、腾讯COS、七牛云OSS、MInio等等

如何制作spring-boot-starter,看这篇就够了。

源码

  • https://github.com/hujiaqing789/test-spring-boot-starter.git

<END>

推荐阅读:

三分钟教你如何用 Github 快速找到优秀的开源项目 !

SpringBoot + K8S 中的滚动发布、优雅停机、弹性伸缩、应用监控、配置分离

  1. 互联网初中高级大厂面试题(9个G)
  2. 内容包含Java基础、JavaWeb、MySQL性能优化、JVM、锁、百万并发、消息队列、高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper......等技术栈!
  3. ⬇戳阅读原文领取!                                  朕已阅
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/762387
推荐阅读
相关标签
  

闽ICP备14008679号