FastDFS-Client Java源码改造为适用Nacos的方式

FastDFS-Client 源码的gitHub 地址:
https://github.com/happyfish100/fastdfs-client-java
这个Client的简单用法可自行去看博客:FastDFSClient集成SpringBoot 基础用法
简单用法主要是根据fdfs_client.conf 配置文件的配置来进行的,这样在打包发布后,配置文件不方便修改,对维护成本也有所增加。所以在这根据FastDFS-Client的源码进行改造,修改为使用application.yml配置文件就行fastdfs的配置,同时将配置文件存在Nacos中,以便后续生产改动服务器地址提供便利。

先看看改造后的工程(这里是做成了微服务的组件,通过加入依赖就可以使用):
在这里插入图片描述
工程的pom.xml如下(最主要的就是fastdfs-client-java 和 commons-pool2)另外的是工程的核心组件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<parent>
		<artifactId>syiti-components</artifactId>
		<groupId>com.syiti.dev</groupId>
		<version>2.0.0</version>
	</parent>
	<modelVersion>4.0.0</modelVersion>

	<artifactId>component-fastdfs</artifactId>

	<dependencies>
		<dependency>
			<groupId>org.csource</groupId>
			<artifactId>fastdfs-client-java</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.slf4j</groupId>
					<artifactId>slf4j-log4j12</artifactId>
				</exclusion>
			</exclusions>
			<version>1.27</version>
		</dependency>
		<dependency>
			<groupId>com.syiti.dev</groupId>
			<artifactId>component-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.6.0</version>
		</dependency>
	</dependencies>

</project>

然后在properties包下创建FastdfsProperties配置文件实体类

package com.syiti.dev.component.fastdfs.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;


/**
 * @author LinYoufeng
 * @date 2020-01-03 下午4:05
 */
@Data
@ConfigurationProperties(prefix = "fastdfs")
public class FastdfsProperties {

	/**
	 * 连接超时时间
	 */
	private String connectTimeout = "5";
	/**
	 * 网络超时时间
	 */
	private String networkTimeout = "30";
	/**
	 * 字符集编码
	 */
	private String charset = "UTF-8";
	/**
	 * 是否使用Token
	 */
	private String httpAntiStealToken = "false";
	/**
	 * Token加密密钥
	 */
	private String httpSecretKey = "";
	/**
	 * 跟踪器IP地址,多个使用分号隔开
	 */
	private String httpTrackerHttpPort = "";
	/**
	 * 连接池的连接对象最大个数
	 */
	private String trackerServers = "";
	/**
	 * 连接池的最大空闲对象个数
	 */
	private String connectionPoolMaxTotal = "18";
	/**
	 * 连接池的最小空闲对象个数
	 */
	private String connectionPoolMaxIdle = "18";
	/**
	 * Nginx服务器IP,多个使用分号分割
 	 */
	private String connectionPoolMinIdle = "2";
	/**
	 *  获取连接对象时可忍受的等待时长(毫秒)
	 */
	private String nginxServers = "";

}

创建连接对象工厂类,在factory包下创建StorageClientFactory

package com.syiti.dev.component.fastdfs.factory;

import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.csource.fastdfs.StorageClient;
import org.csource.fastdfs.TrackerClient;
import org.csource.fastdfs.TrackerServer;

/**
 * 用于创建连接对象的工厂类
 * @author LinYoufeng
 * @date 2020-01-03 下午4:02
 */
public class StorageClientFactory implements PooledObjectFactory<StorageClient> {

	@Override
	public PooledObject<StorageClient> makeObject() throws Exception {
		TrackerClient client = new TrackerClient();
		TrackerServer server = client.getConnection();
		return new DefaultPooledObject<>(new StorageClient(server, null));
	}

	@Override
	public void destroyObject(PooledObject<StorageClient> p) throws Exception {
		p.getObject().getTrackerServer().close();
	}

	@Override
	public boolean validateObject(PooledObject<StorageClient> p) {
		return false;
	}

	@Override
	public void activateObject(PooledObject<StorageClient> p) throws Exception {

	}

	@Override
	public void passivateObject(PooledObject<StorageClient> p) throws Exception {

	}
}

创建配置类

package com.syiti.dev.component.fastdfs.autoconfiguration;

import com.syiti.dev.component.fastdfs.properties.FastdfsProperties;
import com.syiti.dev.component.fastdfs.service.FastdfsClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author LinYoufeng
 * @date 2020-01-03 下午4:02
 */

@Configuration
@EnableConfigurationProperties(FastdfsProperties.class)
public class FastdfsAutoConfiguration {
	@Autowired
	private FastdfsProperties fastdfsProperties;

	@Bean
	@ConditionalOnMissingBean(FastdfsClientService.class)
	public FastdfsClientService fastdfsClientService() throws Exception {
		return new FastdfsClientService(fastdfsProperties);
	}
}

然后在service包下创建FastdfsClientService类,包括具体的上传操作

package com.syiti.dev.component.fastdfs.service;

import com.syiti.dev.component.fastdfs.factory.StorageClientFactory;
import com.syiti.dev.component.fastdfs.properties.FastdfsProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.ClientGlobal;
import org.csource.fastdfs.FileInfo;
import org.csource.fastdfs.ProtoCommon;
import org.csource.fastdfs.StorageClient;

import java.util.Arrays;
import java.util.Properties;

import static java.lang.Math.abs;

/**
 * @author LinYoufeng
 * @date 2020-01-03 下午4:14
 */
@Slf4j
public class FastdfsClientService {

	/**
	 * SpringBoot加载的配置文件
	 */
	private FastdfsProperties fdfsProp;
	/**
	 * 连接池配置项
	 */
	private GenericObjectPoolConfig config;
	/**
	 * 转换后的配置条目
 	 */
	private Properties prop;
	/**
	 * 连接池
	 */
	private GenericObjectPool<StorageClient> pool;
	/**
	 * Nginx服务器地址
 	 */
	private String[] nginxServers;


	public FastdfsClientService(FastdfsProperties fdfsProp) throws Exception {
		this.fdfsProp = fdfsProp;

		init();
		create();
		info();
	}

	/**
	 * 初始化全局客户端
	 */
	private void init() throws Exception {
		this.prop = new Properties();
		log.info("FastDFS: reading config file...");
		log.info("FastDFS: fastdfs.connect_timeout_in_seconds=" + this.fdfsProp.getConnectTimeout());
		log.info("FastDFS: fastdfs.network_timeout_in_seconds=" + this.fdfsProp.getNetworkTimeout());
		log.info("FastDFS: fastdfs.charset=" + this.fdfsProp.getCharset());
		log.info("FastDFS: fastdfs.http_anti_steal_token=" + this.fdfsProp.getHttpAntiStealToken());
		log.info("FastDFS: fastdfs.http_secret_key=" + this.fdfsProp.getHttpSecretKey());
		log.info("FastDFS: fastdfs.http_tracker_http_port=" + this.fdfsProp.getHttpTrackerHttpPort());
		log.info("FastDFS: fastdfs.tracker_servers=" + this.fdfsProp.getTrackerServers());
		log.info("FastDFS: fastdfs.connection_pool_max_total=" + this.fdfsProp.getConnectionPoolMaxTotal());
		log.info("FastDFS: fastdfs.connection_pool_max_idle=" + this.fdfsProp.getConnectionPoolMaxIdle());
		log.info("FastDFS: fastdfs.connection_pool_min_idle=" + this.fdfsProp.getConnectionPoolMinIdle());
		log.info("FastDFS: fastdfs.nginx_servers=" + this.fdfsProp.getNginxServers());

		this.prop.put("fastdfs.connect_timeout_in_seconds", this.fdfsProp.getConnectTimeout());
		this.prop.put("fastdfs.network_timeout_in_seconds", this.fdfsProp.getNetworkTimeout());
		this.prop.put("fastdfs.charset", this.fdfsProp.getCharset());
		this.prop.put("fastdfs.http_anti_steal_token", this.fdfsProp.getHttpAntiStealToken());
		this.prop.put("fastdfs.http_secret_key", this.fdfsProp.getHttpSecretKey());
		this.prop.put("fastdfs.http_tracker_http_port", this.fdfsProp.getHttpTrackerHttpPort());
		this.prop.put("fastdfs.tracker_servers", this.fdfsProp.getTrackerServers());
		ClientGlobal.initByProperties(this.prop);
	}

	/**
	 * 显示初始化信息
	 */
	private void info() {
		log.info("FastDFS parameter: ConnectionPoolMaxTotal ==> " + this.pool.getMaxTotal());
		log.info("FastDFS parameter: ConnectionPoolMaxIdle ==> " + this.pool.getMaxIdle());
		log.info("FastDFS parameter: ConnectionPoolMinIdle ==> " + this.pool.getMinIdle());
		log.info("FastDFS parameter: NginxServer ==> " + Arrays.toString(this.nginxServers));
		log.info(ClientGlobal.configInfo());
	}

	/**
	 * 创建连接池
	 */
	private void create() {
		this.config = new GenericObjectPoolConfig();
		log.info("FastDFS Client: Creating connection pool...");
		this.config.setMaxTotal(Integer.parseInt(this.fdfsProp.getConnectionPoolMaxTotal()));
		this.config.setMaxIdle(Integer.parseInt(this.fdfsProp.getConnectionPoolMaxIdle()));
		this.config.setMinIdle(Integer.parseInt(this.fdfsProp.getConnectionPoolMinIdle()));
		StorageClientFactory factory = new StorageClientFactory();
		this.pool = new GenericObjectPool<StorageClient>(factory, this.config);
		this.nginxServers = this.fdfsProp.getNginxServers().split(",");
	}

	/**
	 * Nginx服务器负载均衡算法
	 *
	 * @param servers 服务器地址
	 * @param address 客户端IP地址
	 * @return 可用的服务器地址
	 */
	private String getNginxServer(String[] servers, String address) {
		int size = servers.length;
		int i = address.hashCode();
		int index = abs(i % size);
		return servers[index];
	}

	public String getNginxServer(){
		if (this.nginxServers.length > 0){
			return "http://"+this.nginxServers[0];
		}
		return null;
	}

	/**
	 * 上传文件,适合上传图片
	 *
	 * @param buffer 字节数组
	 * @param ext    扩展名
	 * @return 文件组名和ID
	 */
	public String[] autoUpload(byte[] buffer, String ext) throws Exception {
		String[] upload = this.upload(buffer, ext, null);
		return upload;
	}

	/**
	 * 带有防盗链的下载
	 *
	 * @param fileGroup       文件组名
	 * @param remoteFileName  远程文件名称
	 * @param clientIpAddress 客户端IP地址
	 * @return 完整的URL地址
	 */
	public String autoDownloadWithToken(String fileGroup, String remoteFileName, String clientIpAddress) throws Exception {
		int ts = (int) (System.currentTimeMillis() / 1000);
		String token = ProtoCommon.getToken(remoteFileName, ts, ClientGlobal.getG_secret_key());
		String nginx = this.getNginxServer(this.nginxServers, clientIpAddress);
		return "http://" + nginx + "/" + fileGroup + "/" + remoteFileName + "?token=" + token + "&ts=" + ts;
	}

	/**
	 * 不带防盗链的下载,如果开启防盗链会导致该方法抛出异常
	 *
	 * @param fileGroup       文件组名
	 * @param remoteFileName  远程文件ID
	 * @param clientIpAddress 客户端IP地址,根据客户端IP来分配Nginx服务器
	 * @return 完整的URL地址
	 */
	public String autoDownloadWithoutToken(String fileGroup, String remoteFileName, String clientIpAddress) throws Exception {
		if (ClientGlobal.getG_anti_steal_token()) {
			log.error("FastDFS Client: You've turned on Token authentication.");
			throw new Exception("You've turned on Token authentication.");
		}
		String nginx = this.getNginxServer(this.nginxServers, clientIpAddress);
		return "http://" + nginx + fileGroup + "/" + remoteFileName;
	}

	/**
	 * 通过本地文件上传
	 *
	 * @param localFileName 本地文件名称
	 * @param fileExtName   文件扩展名
	 * @param metadata      文件的元数据
	 * @return 文件组名和ID
	 */
	public String[] upload(String localFileName, String fileExtName, NameValuePair[] metadata) throws Exception {
		StorageClient client = this.pool.borrowObject();
		final String[] strings = client.upload_file(localFileName, fileExtName, metadata);
		this.pool.returnObject(client);
		return strings;
	}

	/**
	 * 通过字节数组上传
	 *
	 * @param fileBuff    文件的字节数组
	 * @param fileExtName 扩展名
	 * @param metadata    元数据
	 * @return 文件组名和ID
	 */
	public String[] upload(byte[] fileBuff, String fileExtName, NameValuePair[] metadata) throws Exception {
		StorageClient client = this.pool.borrowObject();
		String[] strings = client.upload_file(fileBuff, fileExtName, metadata);
		this.pool.returnObject(client);
		return strings;
	}



	/**
	 * 通过字节数组上传附加文件
	 *
	 * @param buff        字节数组
	 * @param offset      偏移量
	 * @param len         长度
	 * @param fileExtName 扩展名
	 * @param metadata    元数据
	 * @return 文件组名和ID
	 */
	public String[] uploadAppenderFile(byte[] buff, int offset, int len, String fileExtName, NameValuePair[] metadata) throws Exception {
		StorageClient client = this.pool.borrowObject();
		String[] strings = client.upload_appender_file(buff, offset, len, fileExtName, metadata);
		this.pool.returnObject(client);
		return strings;
	}

	/**
	 * 通过字节数组上传附加文件
	 *
	 * @param buff        字节数组
	 * @param fileExtName 扩展名
	 * @param metadata    元数据
	 * @return 文件组名和ID
	 */
	public String[] uploadAppenderFile(byte[] buff, String fileExtName, NameValuePair[] metadata) throws Exception {
		StorageClient client = this.pool.borrowObject();
		String[] strings = client.upload_appender_file(buff, fileExtName, metadata);
		this.pool.returnObject(client);
		return strings;
	}

	/**
	 * 追加文件
	 *
	 * @param groupName        组名
	 * @param appenderFileName 追加文件名称
	 * @param buff             文件字节数组
	 * @return 返回0表示成功
	 */
	public int appendFile(String groupName, String appenderFileName, byte[] buff) throws Exception {
		StorageClient client = this.pool.borrowObject();
		int i = client.append_file(groupName, appenderFileName, buff);
		this.pool.returnObject(client);
		return i;
	}



	/**
	 * 修改文件
	 *
	 * @param groupName        组名
	 * @param appenderFileName 追加文件名称
	 * @param fileOffset       偏移量
	 * @param buff             字节数组
	 * @return 返回0表示成功
	 */
	public int modify(String groupName, String appenderFileName, long fileOffset, byte[] buff) throws Exception {
		StorageClient client = this.pool.borrowObject();
		int i = client.modify_file(groupName, appenderFileName, fileOffset, buff);
		this.pool.returnObject(client);
		return i;
	}

	/**
	 * 修改文件
	 *
	 * @param groupName        组名
	 * @param appenderFileName 追加文件名称
	 * @param fileOffset       偏移量
	 * @param buff             字节数组
	 * @param waitTimeMillis   等待时间
	 * @return 返回0表示成功
	 */
	public int modify(String groupName, String appenderFileName, long fileOffset, byte[] buff, long waitTimeMillis) throws Exception {
		StorageClient client = this.pool.borrowObject(waitTimeMillis);
		int i = client.modify_file(groupName, appenderFileName, fileOffset, buff);
		this.pool.returnObject(client);
		return i;
	}

	/**
	 * 删除文件
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @return 返回0表示成功
	 */
	public int delete(String groupName, String remoteFileName) throws Exception {
		StorageClient client = this.pool.borrowObject();
		int i = client.delete_file(groupName, remoteFileName);
		this.pool.returnObject(client);
		return i;
	}


	/**
	 * 文件分片
	 *
	 * @param groupName    组名
	 * @param appenderName 输出源文件名称
	 * @return 返回0表示成功
	 */
	public int truncate(String groupName, String appenderName) throws Exception {
		StorageClient client = this.pool.borrowObject();
		int i = client.truncate_file(groupName, appenderName);
		this.pool.returnObject(client);
		return i;
	}


	/**
	 * 文件分片
	 *
	 * @param truncatedFileSize 分片大小
	 * @param groupName         组名
	 * @param appenderName      输出源文件名称
	 * @return 返回0表示成功
	 */
	public int truncate(long truncatedFileSize, String groupName, String appenderName) throws Exception {
		StorageClient client = this.pool.borrowObject();
		int i = client.truncate_file(groupName, appenderName, truncatedFileSize);
		this.pool.returnObject(client);
		return i;
	}


	/**
	 * 下载文件
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @return 返回0表示成功
	 */
	public byte[] download(String groupName, String remoteFileName) throws Exception {
		StorageClient client = this.pool.borrowObject();
		byte[] bytes = client.download_file(groupName, remoteFileName);
		this.pool.returnObject(client);
		return bytes;
	}

	/**
	 * 下载文件
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @param waitTimeMillis 等待时间
	 * @return 返回0表示成功
	 */
	public byte[] download(String groupName, String remoteFileName, long waitTimeMillis) throws Exception {
		StorageClient client = this.pool.borrowObject(waitTimeMillis);
		byte[] bytes = client.download_file(groupName, remoteFileName);
		this.pool.returnObject(client);
		return bytes;
	}



	/**
	 * 下载文件
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @param localFileName  本地文件名称
	 * @return 返回0表示成功
	 */
	public int download(String groupName, String remoteFileName, String localFileName) throws Exception {
		StorageClient client = this.pool.borrowObject();
		int i = client.download_file(groupName, remoteFileName, localFileName);
		this.pool.returnObject(client);
		return i;
	}

	/**
	 * 下载文件
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @param localFileName  本地文件名称
	 * @param waitTimeMillis 等待时间
	 * @return 返回0表示成功
	 */
	public int download(String groupName, String remoteFileName, String localFileName, long waitTimeMillis) throws Exception {
		StorageClient client = this.pool.borrowObject(waitTimeMillis);
		int i = client.download_file(groupName, remoteFileName, localFileName);
		this.pool.returnObject(client);
		return i;
	}

	/**
	 * 获取元数据
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @return 键值对数组
	 */
	public NameValuePair[] getMetadata(String groupName, String remoteFileName) throws Exception {
		StorageClient client = this.pool.borrowObject();
		NameValuePair[] metadata = client.get_metadata(groupName, remoteFileName);
		this.pool.returnObject(client);
		return metadata;
	}

	/**
	 * 获取元数据
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @param waitTimeMillis 等待时间
	 * @return 键值对数组
	 */
	public NameValuePair[] getMetadata(String groupName, String remoteFileName, long waitTimeMillis) throws Exception {
		StorageClient client = this.pool.borrowObject(waitTimeMillis);
		NameValuePair[] metadata = client.get_metadata(groupName, remoteFileName);
		this.pool.returnObject(client);
		return metadata;
	}

	/**
	 * 设置元数据
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @param metadata       元数据
	 * @param flag           更新设置:ProtoCommon.STORAGE_SET_METADATA_FLAG_OVERWRITE 表示重写所有; ProtoCommon.STORAGE_SET_METADATA_FLAG_MERGE 表示没有就插入,有就更新
	 * @return 返回0表示成功
	 */
	public int setMetadata(String groupName, String remoteFileName, NameValuePair[] metadata, byte flag) throws Exception {
		StorageClient client = this.pool.borrowObject();
		int i = client.set_metadata(groupName, remoteFileName, metadata, flag);
		this.pool.returnObject(client);
		return i;
	}


	/**
	 * 获取文件信息
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @return 返回文件信息
	 */
	public FileInfo getFileInfo(String groupName, String remoteFileName) throws Exception {
		StorageClient client = this.pool.borrowObject();
		FileInfo file_info = client.get_file_info(groupName, remoteFileName);
		this.pool.returnObject(client);
		return file_info;
	}

	/**
	 * 获取文件名称
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件名称
	 * @param waitTimeMillis 等待时长
	 * @return 返回文件信息
	 */
	public FileInfo getFileInfo(String groupName, String remoteFileName, long waitTimeMillis) throws Exception {
		StorageClient client = this.pool.borrowObject(waitTimeMillis);
		FileInfo file_info = client.get_file_info(groupName, remoteFileName);
		this.pool.returnObject(client);
		return file_info;
	}

	/**
	 * 查询文件信息
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件信息
	 * @return 返回文件信息
	 */
	public FileInfo queryFileInfo(String groupName, String remoteFileName) throws Exception {
		StorageClient client = this.pool.borrowObject();
		FileInfo fileInfo = client.query_file_info(groupName, remoteFileName);
		this.pool.returnObject(client);
		return fileInfo;
	}

	/**
	 * 查询文件信息
	 *
	 * @param groupName      组名
	 * @param remoteFileName 远程文件信息
	 * @param waitTimeMillis 等待时间
	 * @return 返回文件信息
	 */
	public FileInfo queryFileInfo(String groupName, String remoteFileName, long waitTimeMillis) throws Exception {
		StorageClient client = this.pool.borrowObject(waitTimeMillis);
		FileInfo fileInfo = client.query_file_info(groupName, remoteFileName);
		this.pool.returnObject(client);
		return fileInfo;
	}
}

最后通过创建注解类

package com.syiti.dev.component.fastdfs.annotation;

import com.syiti.dev.component.fastdfs.autoconfiguration.FastdfsAutoConfiguration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * @author LinYoufeng
 * @date 2020-01-03 下午4:01
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(FastdfsAutoConfiguration.class)
@Documented
public @interface EnableFastdfsClient {
}

在源码上包装后,使用这个组件说明:
FastDFS 组件使用说明:
使用FastDFS 组件分以下四步:
1.加依赖:

com.syiti.dev
component-fastdfs
2.0.0

2.在启动类上加入注解
@EnableDiscoveryClient

3.写配置文件 application.yml
fastdfs:
charset: UTF-8
connect-timeout: 5
network-timeout: 30
http-tracker-http-port: 1080
connection-pool-max-idle: 20
connection-pool-max-total: 20
connection-pool-min-idle: 2
nginx-servers: 192.168.0.192:1080
tracker-servers: 192.168.0.192:22122

4.在需要使用FastDFS 的具体业务实现类上加入 Service
@Autowired
private FastdfsClientService fastdfsClientService;

完成以上四步即可直接使用相关的上传、删除等功能。
fastdfsClientService.autoUpload(xx,xx)

发布了14 篇原创文章 · 获赞 1 · 访问量 2243
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 1024 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览