您的位置:首页 > 理论基础 > 计算机网络

Retrofit2.0通俗易懂的学习姿势,Retrofit2.0 + OkHttp3 + Gson + RxJava

2017-12-11 17:12 483 查看

Retrofit2.0通俗易懂的学习姿势,Retrofit2.0 + OkHttp3 + Gson + RxJava

Retrofit,因为其简单与出色的性能,也是受到很多人的青睐,但是他和以往的通信框架还是有点区别,不过放心,因为他本身还是挺简单的,所有我相信你看完这篇文章,对基本的请求是没什么问题的,其实现在网上这样的文章也有很多了,好了,那我们直接开车吧!

一.相关资料

Github:https://github.com/square/retrofit

官网文档:http://square.github.io/retrofit/



二.square

square这家公司开源了很多非常优秀的项目,所有拿出来表示一家对他们这帮有开源精神的工程师表达

一下敬意,比如有这些项目:

OkHttp:https://github.com/square/okhttp

Picasso:https://github.com/square/picasso

更多项目可以自己去他们的组织里官网下:https://github.com/square

三.准备工作

我们要使用Retrofit2.0,肯定要先集成进去,把项目这么一新建——RetrofitSample,然后我们看他的Github上,支持三种使用方式,这就没必要多说,各位看官想怎么添加就怎么添加

Jar: 点我下载

Maven:

<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.1.0</version>
</dependency>
1
2
3
4
5
6
[/code]

Gradle:

compile 'com.squareup.retrofit2:retrofit:2.1.0'
1
2
[/code]

这里我们就直接用Gradle配置了,毕竟这样最简单,当然,我们配合OkHttp3更加的好用,所以添加源

compile 'com.squareup.okhttp3:okhttp:3.3.1'
1
[/code]

这里可别忘了添加网络权限哦!

<uses-permission android:name="android.permission.INTERNET"/>
1
[/code]

四.定义接口

我们要想学会使用,最佳的选择肯定是阅读官方的文档,第一句就是

“Retrofit turns your HTTP API into a Java interface”

需要我们定义一个接口,好的,那我们就依葫芦画瓢,写一个接口, 这里一定要记住哦,这个接口写起来是有一定的规范的,我们先看接口

http://gank.io/api/data/Android/10/1

这是Gank的接口,那我们应该怎么去写我们本地的接口呢,这是我们的Json



我们来定义一下我们的接口类,我们取名为GnakApi

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;

public interface GnakApi {

@GET("api/data/Android/10/1")
Call<ResponseBody> getAndroidInfo();
}
1
2
3
4
5
6
7
8
9
[/code]

仔细分析这个接口的定义,其实,这个接口我们可用这样去拆分



前面的baseUrl我们等一下直接定义,后面的,我们才是定义接口,我现在不需要返回值,所以我直接传了一个ResponseBody,而上面,我用GET请求,直接请求了api/data/Android/10/1,这样就能和我们的baseUrl拼接了,而后面的参数如果我们要改变的话,那接口方法就要传参了,这个我们等下来讲,再看下我们文档上的接口定义

public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
1
2
3
4
[/code]

这个是文档上提供的例子,我们可用发现,他同样的是GET请求,只不过他的返回值是一个List,类型是repo,repo就是他的实体类,传了一个path是一个参数,user的参数,这样也同样的可以和他的baseUrl拼接了他的baseUrl是什么呢,我们等下再说

五.简单请求

接口定义好了之后,我就开始请求了,这里我在布局里面写一个button做点击事件,再写一个textview做返回结果的呈现

<Button
android:id="@+id/btn_requet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/button_bg"
android:text="请求"
android:textAllCaps="false"
android:textColor="@android:color/white"/>

<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
[/code]

好,那我们要怎么做呢?继续看文档

The Retrofit class generates an implementation of the GitHubService interface.

这里说直接把Retrofit的接口改造成一个类,那需要怎么做呢?首先,我们要创建一个Retrofit

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://gank.io/")
.build();
1
2
3
[/code]

这里大家就看到我们的baseUrl了,就是我们Gank接口的前缀,我们现在有了retrofit,通过他的create方法就可以创建我们的接口对象了

GnakApi api = retrofit.create(GnakApi.class);
1
[/code]

这里虽然是返回一个GnakApi,但是通过源码我们知道,这里的create实际上是通过代理的方式拿到的,可以看下

public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();

@Override public Object invoke(Object proxy, Method method, Object... args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[/code]

当然,这里不做深入的了解,我们现在就直接去调用我们的接口方法,他返回的就是一个Call

Call<ResponseBody> call = api.getAndroidInfo();
1
[/code]

到这里,你会很奇怪,我去,怎么和okHttp这么像啊,如果单纯从简单请求来看,确实有一丢丢像,但是别急,Retrofit可没这么简单,我们又了call之后就直接请求了,一般我们都是异步请求

call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
try {
String result = response.body().string();
Log.i(TAG, result);
tv_result.setText(result);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {

}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[/code]

那我们直接点,运行一下



恩,可以看到,已经成功的请求到了结果了,这就是Retrofit的无参简单请求了,但是这里我们其实还可以做点文章,Retrofit + OkHttp + Gson更配哦,那我们添加Gson的Jar之后我们写一个实体类,也就是我们这个接口的javaBean,这里我使用的解析插件是Gsonformat



GankBean

import java.util.List;

public class GankBean {

private boolean error;
private List<ResultsBean> results;

public boolean isError() {
return error;
}

public void setError(boolean error) {
this.error = error;
}

public List<ResultsBean> getResults() {
return results;
}

public void setResults(List<ResultsBean> results) {
this.results = results;
}

public static class ResultsBean {
/**
* _id : 5827f41b421aa911d3bb7ecb
* createdAt : 2016-11-13T13:03:23.38Z
* desc : 独立全端开发的开源小作:简诗 2.0
* images : ["http://img.gank.io/b6be7a85-4035-437f-9521-65593fdbc48e"]
* publishedAt : 2016-11-14T11:36:49.680Z
* source : web
* type : Android
* url : https://www.v2ex.com/t/320154 * used : true
* who : wingjay
*/

private String _id;
private String createdAt;
private String desc;
private String publishedAt;
private String source;
private String type;
private String url;
private boolean used;
private String who;
private List<String> images;

public String get_id() {
return _id;
}

public void set_id(String _id) {
this._id = _id;
}

public String getCreatedAt() {
return createdAt;
}

public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

public String getPublishedAt() {
return publishedAt;
}

public void setPublishedAt(String publishedAt) {
this.publishedAt = publishedAt;
}

public String getSource() {
return source;
}

public void setSource(String source) {
this.source = source;
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public boolean isUsed() {
return used;
}

public void setUsed(boolean used) {
this.used = used;
}

public String getWho() {
return who;
}

public void setWho(String who) {
this.who = who;
}

public List<String> getImages() {
return images;
}

public void setImages(List<String> images) {
this.images = images;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
[/code]

用Gson是一键生成的,所以还是很方便

六.正常请求

我们做一个和官网一样的例子,实体类GankBean已经有了,那我们重新修改一下接口,让他返回这个实体类

import retrofit2.Call;
import retrofit2.http.GET;

public interface GnakApi {

@GET("api/data/Android/10/1")
Call<GankBean> getAndroidInfo();
}
1
2
3
4
5
6
7
8
[/code]

去用的时候前面没有什么变化,只是返回的是实体类罢了

Call<GankBean> call = api.getAndroidInfo();
1
[/code]

那我们异步的话,就很简单了

//异步
call.enqueue(new Callback<GankBean>() {
@Override
public void onResponse(Call<GankBean> call, Response<GankBean> response) {
GankBean.ResultsBean bean = response.body().getResults().get(0);
tv_result.setText(
"_id:" + bean.get_id() + "\n"
+ "createdAt:" + bean.getCreatedAt() + "\n"
+ "desc:" + bean.getDesc() + "\n"
+ "images:" + bean.getImages() + "\n"
+ "publishedAt:" + bean.getPublishedAt() + "\n"
+ "source" + bean.getSource() + "\n"
+ "type:" + bean.getType() + "\n"
+ "url: " + bean.getUrl() + "\n"
+ "who:" + bean.getWho());
}

@Override
public void onFailure(Call<GankBean> call, Throwable t) {

}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[/code]

这里就可以直接get出我们想要的值了吗?答案是否定的,我们运行之后可以看到这样一行错误

IllegalArgumentException: Unable to create converter for class com.liuguilin.retrofitsample.GankBean
1
[/code]

下面该有一条更加重要的信息

Could not locate ResponseBody converter for class com.liuguilin.retrofitsample.GankBean.
1
[/code]

这里很直观的说明,不能创建一个转换器,我擦,那怎么办?通过看文档,我们知道,我们需要去配置一个Gson,并不是我们的google.gson,我们添加源

compile 'com.squareup.retrofit2:converter-gson:2.1.0'
1
[/code]

单独说这个错误是因为我的学生很多都碰到过,所以这里提一下,然后我们配置一行

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://gank.io/")
.addConverterFactory(GsonConverterFactory.create())
.build();
1
2
3
4
[/code]

现在运行就没问题了



恩,到这里,我们的基本请求就是这个样子,是不是很简单,我们只要前期配置好,并且根据api定义好接口,其他的都变得很方便了,配合Gson,那请求完了就可以直接拿值了,加上比较火的Dagger2注解,代表会变得十分的感人的,简洁,高效。

其实讲到这里,大家对他就已经有了一定的了解了,我们现在继续跟着文档走,来说下他的一些特性

七.Get的动态参数

大家都知道,我们的url大多数是拼接的,就像我们查询天气的接口,肯定是动态传一个城市的名字,对吧,这里我们换一个接口,就用天气的接口

官网:https://www.juhe.cn/docs/api/id/73

通过官网我们可用得知我们的接口是这样的

http://op.juhe.cn/onebox/weather/query?cityname=深圳&key=您申请的KEY

这里我们做一个简单的案例,默认cityname是深圳,我们就去拼接我们的key,那我们接口要怎么定义呢,我们写一个WeatherApi:

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;

public interface WeatherApi {

@GET("onebox/weather/query?cityname=深圳")
Call<WeatherDataBean> getWeather(@Query("key") String key);
}
1
2
3
4
5
6
7
8
9
10
[/code]

这里我们可以看到,我们Get还是把我们的连接后半段传进去,但是这里最后拼接的时一个key,所以在传参的前面加上Query,如果你想两个参数,还有一个城市的话,你就多加一个参数咯,我们现在初始化一下

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://op.juhe.cn/")
.addConverterFactory(GsonConverterFactory.create())
.build();
api = retrofit.create(WeatherApi.class);
1
2
3
4
5
[/code]

好的,拿到我们的接口对象就去用吧

Call<WeatherDataBean> call = api.getWeather("4ea58de8a7573377cec0046f5e2469d5");
//异步
call.enqueue(new Callback<WeatherDataBean>() {
@Override
public void onResponse(Call<WeatherDataBean> call, Response<WeatherDataBean> response) {
String info = response.body().getResult().getData().getRealtime().getWeather().getInfo();
tv_result.setText("深圳天气:" + info);
}

@Override
public void onFailure(Call<WeatherDataBean> call, Throwable t) {

}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]

这里的WeatherDataBean实在是数据太多了,所以我就没有显示出来,有需要的我会在本文的最后提供Sample的,这里我们请求,数据太多,我就直接请求一个了,来看运行之后的结果:



八.Get参数请求

上面的Get方法是一般的接口会这样做,但是有些请求,是有问题的,比如我们最上面的这个接口

http://gank.io/api/data/Android/10/1

他并没有像cityname或者key这样的名字,而是直接传参,其实他的参数含义是

//后面三个参数
//Android可接受参数  | Android | iOS | 休息视频 | 福利 | 前端 | App
//count 最大 50
//page  是页数
1
2
3
4
[/code]

这样的类型接口,我们一般是怎么去定义接口的呢?我们改造一下GankApi,这里我就直接传page了

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;

public interface GnakApi {

@GET("api/data/Android/10/{page}")
Call<GankBean> getAndroidInfo(@Path("page") int page);
}
1
2
3
4
5
6
7
8
9
[/code]

可以看到,这里我们用大括号做占位符,然后用path关键字,一定要对应哦,其他的都差不多,紧接着我们请求

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://gank.io/")
.addConverterFactory(GsonConverterFactory.create())
.build();
api = retrofit.create(GankApi.class);
1
2
3
4
5
[/code]

这样拿到我们的接口对象后直接请求

api.getAndroidInfo(1).enqueue(new Callback<GankBean>() {
@Override
public void onResponse(Call<GankBean> call, Response<GankBean> response) {
tv_result.setText(response.body().getResults().get(0).getDesc());
}

@Override
public void onFailure(Call<GankBean> call, Throwable t) {

}
});
1
2
3
4
5
6
7
8
9
10
11
[/code]

可以运行看到,依旧是可以拿到我们想要的值的,说明我们这个也是可以的



九.Get参数拼接

这个又是什么呢,我们把天气的api改一下你就知道了

import java.util.Map;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.QueryMap;

public interface WeatherApi {

@GET("onebox/weather/query?")
Call<WeatherDataBean> getWeather(@QueryMap Map<String, String> params);
}
1
2
3
4
5
6
7
8
9
10
11
[/code]

仔细看我这里GET后面传的并没有像cityname之类的参数,但是我这里有一个QueryMap ,传的是键值对,这样我们怎么去用呢?可以这样:

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://op.juhe.cn/")
.addConverterFactory(GsonConverterFactory.create())
.build();
api = retrofit.create(WeatherApi.class);
1
2
3
4
5
[/code]

这里倒是没什么改变的,我们主要看接口传递的对象:

Map<String, String> params = new HashMap<>();
params.put("cityname", "深圳");
params.put("key", "4ea58de8a7573377cec0046f5e2469d5");
api.getWeather(params).enqueue(new Callback<WeatherDataBean>() {
@Override
public void onResponse(Call<WeatherDataBean> call, Response<WeatherDataBean> response) {

}

@Override
public void onFailure(Call<WeatherDataBean> call, Throwable t) {

}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]

这里我们可以看到传map,然后map去put参数,这样最后请求的url就是

http://op.juhe.cn/onebox/weather/query?cityname=深圳&key=您申请的KEY

十.Post

POST请求的话,因为没有接口,所以我简单说一下,我也相信,你看到这里,get了解了post也相应的知道点,不过别急,我们还是会很详细的讲

import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;

public interface PostApi {

@POST("user/new")
Call<Result> postUser(@Body User user);
}
1
2
3
4
5
6
7
8
9
[/code]

这里POST的地址和之前的get也是一样的,这里返回一个Result是我们自家定义的结果,Body是表示参数,我需要一个User,那我们的User就是

public class User {

private int id;
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[/code]

而我们的Result:

public class Result {

private int yes;
private int no;

public int getYes() {
return yes;
}

public void setYes(int yes) {
this.yes = yes;
}

public int getNo() {
return no;
}

public void setNo(int no) {
this.no = no;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[/code]

这两个都是没什么东西的,然后我们就可以直接去看怎么调用了

User user = new User();
user.setId(1);
user.setName("lgl");
api.postUser(user).enqueue(new Callback<Result>() {
@Override
public void onResponse(Call<Result> call, Response<Result> response) {
if (response.body().getYes() == 0) {
Toast.makeText(MainActivity.this, "成功", Toast.LENGTH_SHORT).show();
}
}

@Override
public void onFailure(Call<Result> call, Throwable t) {

}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[/code]

我再这里传了一个user进去,细节不为过就是id和name,如果请求成功,那就返回0,失败就是1,这里服务端定义,这样我们就POST完成了

十一.Post提交表单

根据官网上的例子,还有一个更新用户的方法,用的就是提交表单

@POST("user/edit")
Call<Result> editUser(@Field("id") int id, @Field("name") String name);
1
2
[/code]

OK,我们用到的关键字是Field,这样去定义,然后直接调用这个方法

api.editUser(1, "liuguilin").enqueue(new Callback<Result>() {
@Override
public void onResponse(Call<Result> call, Response<Result> response) {
if (response.body().getYes() == 0) {
Toast.makeText(MainActivity.this, "成功", Toast.LENGTH_SHORT).show();
}
}

@Override
public void onFailure(Call<Result> call, Throwable t) {

}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
[/code]

这样就把表单提交了,是不是很方便呢

最后还有一个PUT,他是多种文件类型上传,比如文件,图片,这里大家参考下官方文档

当然还有修改我们的Headers,这个很简单,看例子

@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();
1
2
3
[/code]

这是官网的例子,但是你只需要添加Headers参数就可以穿了,而且因为他的参数时一个数组,你可以穿多个

按道理讲到这里算是完成了,我这里稍微的带点例子,讲下RxJava,因为我自己也不是很熟,所以我就讲我了解的这一块就好了

十二 Retrofit2.0+ RxJava

RxJava使用起来会让我们的代码更加的简洁,高效,那他和Retrofit2.0又会碰到什么样的激情呢?欢迎大家收看今天的人与自然……额 …跑题了,我们先做一些准备,主要是什么呢,根据官网来看,需要添加

//适配器
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
//RxJava
compile 'io.reactivex:rxjava:1.1.6'
//RxAndroid
compile 'io.reactivex:rxandroid:1.2.1'
1
2
3
4
5
6
[/code]

地址

RxJava:https://github.com/ReactiveX/RxJava

RxAndroid:https://github.com/ReactiveX/RxAndroid

这里我举一个最常见的例子来说明。那就是登录,登录成功后获取到user_id,再去请求用户信息,这里应该是两个请求对吧,我们先去写好接口,这里我们先用常规的方法去获取:

@POST("user/login")
Call<User> login(@Field("username") String user, @Field("password") String password);

@GET("user/info")
Call<User> getUser(@Query("id") String id);
1
2
3
4
5
[/code]

这里的两个接口,一个是登录,传参用户名和密码,还有一个是用id去查找用户信息的,那我们继续

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("baseUrl")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
api = retrofit.create(PostApi.class);
1
2
3
4
5
6
[/code]

这里我们需要增加addCallAdapterFactory为我们后面的Rx做准备,然后我们调用两次

api.login("liuguilin", "748778890").enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
if (response.isSuccessful()) {
String id = response.body().getUser_id();
api.getUser(id).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {

Toast.makeText(MainActivity.this, "id:" +
response.body().getId()
+ "name:" + response.body().getName(),
Toast.LENGTH_SHORT).show();
}

@Override
public void onFailure(Call<User> call, Throwable t) {

}
});
}
}

@Override
public void onFailure(Call<User> call, Throwable t) {

}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[/code]

这样写代码是不是有点臃肿,别急,我们看看RxJava的写法,我们这里需要重新定义两个接口了

@POST("user/login")
rx.Observable<User> loginForRX(@Body User user);

@GET("user/info")
rx.Observable<User> getUserForRX(@Query("id") String id);
1
2
3
4
5
[/code]

这里的都是伪代码,注意看使用方法

api.loginForRX(new User("liuguilin", "748778890")).flatMap(new Func1<User, Observable<User>>() {
@Override
public Observable<User> call(User user) {
return api.getUser(user.getUser_id());
}
}).subscribe(new Action1<User>() {
@Override
public void call(User user) {
Toast.makeText(MainActivity.this, "name:" + user.getName(), Toast.LENGTH_SHORT).show();
}
});
1
2
3
4
5
6
7
8
9
10
11
[/code]

这就是比较简洁的写法了,RxJava作为链式的表达式,响应式的操作还是很不错的,我会在我后面的博客继续深入的剖析,这里就不做太深入的了解了,因为感觉自己还没有到家吧,所以只是提一提,好了,到这里算是Ok了,你的Retrofit2.0学会了吗?

群:555974449

Sample:http://download.csdn.net/detail/qq_26787115/9683939

转自:http://blog.csdn.net/qq_26787115/article/details/53034267
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐