Android整合搭建RxJava+Retrofit+LiveData+OkHttp框架实现MVVM模式开发

一、工程项目

包名解释:

base:基类库。

BaseDto类为服务器返回公共实体;BaseHttpSubscriber类自定义请求服务器被观察者;BaseRepository类请求网络数据基类。

exception:异常类模块。

ApiException类前端自定义Exception;ServerException类服务器返回的Exception;ExceptionEngine类拦截各种异常处理。

https:Retrofit+OkHttp封装网络请求模块。

ApiService接口API的Retrofit注解;RequetRetrofit类的网络请求用到的Retrofit+OkHttp。

interceptor:自定义网络请求拦截器。

model:数据模型层。定义实体类。

repository:数据仓库。包括网络数据获取,sqlite小型数据库,文件File,SharedPreferences数据存储。

view:视图层。主要包含Activity+Fragment实体类。

viewmodel:VM视图模型层。

二、引用类库

implementation 'com.alibaba:fastjson:1.1.70.android'
implementation 'com.google.code.gson:gson:2.8.4'
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
//Rxlifecycle
implementation 'com.trello:rxlifecycle:0.3.1'
implementation 'com.trello:rxlifecycle-components:0.3.1'
implementation 'com.github.bumptech.glide:glide:4.8.0'
implementation 'com.squareup.retrofit2:converter-gson:2.+'
//必须使用
implementation 'com.lzy.net:okgo:3.0.4'
implementation 'com.squareup.okio:okio:1.5.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.+'
implementation 'com.squareup.retrofit2:retrofit:2.+'
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
implementation 'com.squareup.retrofit2:converter-gson:2.+'
implementation 'com.squareup.retrofit2:converter-scalars:2.0.0'
implementation 'com.facebook.stetho:stetho:1.4.2'
implementation 'com.facebook.stetho:stetho-okhttp3:1.4.2'
// 依赖以下两个库,会自动引用基础库与Android库
implementation 'com.trello.rxlifecycle2:rxlifecycle-components:2.1.0'
implementation 'com.trello.rxlifecycle2:rxlifecycle-navi:2.1.0'
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.7.0@aar'
implementation 'com.jakewharton.rxbinding:rxbinding:1.+'
// Lifecycles, LiveData 和 ViewModel
implementation "android.arch.lifecycle:extensions:1.1.0"
// 对 RxJava 的支持
implementation "android.arch.persistence.room:rxjava2:1.0.0-alpha5"

三、网络请求封装

1、BaseDto类

package com.ylink.frameworkdemo.base;

import java.io.Serializable;

/**
 * 服务器返回公共实体
 *
 * @param <T>
 * @author twilight
 * @Time 2019-07-21
 */
public class BaseDto<T> implements Serializable {
    private String statusCode;
    private String statusDesc;
    private T data;

    public String getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(String statusCode) {
        this.statusCode = statusCode;
    }

    public String getStatusDesc() {
        return statusDesc;
    }

    public void setStatusDesc(String statusDesc) {
        this.statusDesc = statusDesc;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

2、BaseHttpSubscriber类

package com.ylink.frameworkdemo.base;

import androidx.lifecycle.MutableLiveData;

import com.ylink.frameworkdemo.Constant;
import com.ylink.frameworkdemo.exception.ApiException;
import com.ylink.frameworkdemo.exception.ExceptionEngine;
import com.ylink.frameworkdemo.exception.ServerException;

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

/**
 * 自定义请服务器被观察者
 *
 * @author twilight
 * @Time 2019-07-21
 */
public class BaseHttpSubscriber<T> implements Subscriber<BaseDto<T>> {

    //异常类
    private ApiException ex;

    public BaseHttpSubscriber() {
        data = new MutableLiveData();
    }

    private MutableLiveData<BaseDto<T>> data;

    public MutableLiveData<BaseDto<T>> get() {
        return data;
    }

    public void set(BaseDto<T> t) {
        this.data.setValue(t);
    }

    public void onFinish(BaseDto<T> t) {
        set(t);
    }

    @Override
    public void onSubscribe(Subscription s) {
        // 观察者接收事件 = 1个
        s.request(1);
    }

    @Override
    public void onNext(BaseDto<T> t) {
        if (t.getStatusCode().equals(Constant.RespCode.R000)) {
            onFinish(t);
        } else{
            ex = ExceptionEngine.handleException(new ServerException(t.getStatusCode(), t.getStatusDesc()));
            getErrorDto(ex);
        }
    }

    @Override
    public void onError(Throwable t) {
        ex = ExceptionEngine.handleException(t);
        getErrorDto(ex);
    }

    /**
     * 初始化错误的dto
     *
     * @param ex
     */
    private void getErrorDto(ApiException ex) {
        BaseDto dto = new BaseDto();
        dto.setStatusCode(ex.getStatusCode());
        dto.setStatusDesc(ex.getStatusDesc());
        onFinish((BaseDto<T>) dto);
    }

    @Override
    public void onComplete() {
    }

}

3、BaseRepository类

package com.ylink.frameworkdemo.base;


import io.reactivex.Flowable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;

/**
 * Repository基类
 *
 * @author twilight
 * @Time 2019-07-21
 */

public class BaseRepository {

    /**
     * 请求网络
     * @param flowable
     * @param <T>
     * @return
     */
    public <T> BaseHttpSubscriber<T> request(Flowable<BaseDto<T>> flowable){
        BaseHttpSubscriber<T> baseHttpSubscriber = new BaseHttpSubscriber<>(); //RxJava Subscriber回调
        flowable.subscribeOn(Schedulers.io()) //解决背压
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(baseHttpSubscriber);
        return baseHttpSubscriber;
    }
}

4、ApiException类

package com.ylink.frameworkdemo.exception;

/**
 * 前端自定义Exception
 */
public class ApiException extends Exception {
    private String statusCode;//错误码
    private String statusDesc;//错误信息

    public ApiException(Throwable throwable, String statusCode) {
        super(throwable);
        this.statusCode = statusCode;
    }

    public ApiException(String statusCode, String statusDesc) {
        this.statusCode = statusCode;
        this.statusDesc = statusDesc;
    }

    public String getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(String statusCode) {
        this.statusCode = statusCode;
    }

    public String getStatusDesc() {
        return statusDesc;
    }

    public void setStatusDesc(String statusDesc) {
        this.statusDesc = statusDesc;
    }
}

5、ServerException类

package com.ylink.frameworkdemo.exception;

/**
 * 服务器返回的Exception
 */
public class ServerException extends RuntimeException {
    private String statusCode;//错误码
    private String statusDesc;//错误信息

    public ServerException(String statusCode, String statusDesc) {
        this.statusCode = statusCode;
        this.statusDesc = statusDesc;
    }

    public String getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(String statusCode) {
        this.statusCode = statusCode;
    }

    public String getStatusDesc() {
        return statusDesc;
    }

    public void setStatusDesc(String statusDesc) {
        this.statusDesc = statusDesc;
    }
}

6、ExceptionEngine类

package com.ylink.frameworkdemo.exception;

import android.net.ParseException;
import android.util.MalformedJsonException;

import com.google.gson.JsonParseException;

import org.json.JSONException;

import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

import retrofit2.HttpException;

public class ExceptionEngine {
    //客户端报错
    public static final int UN_KNOWN_ERROR = 9000;//未知错误
    public static final int ANALYTIC_SERVER_DATA_ERROR = 9001;//解析(服务器)数据错误
    public static final int ANALYTIC_CLIENT_DATA_ERROR = 9002;//解析(客户端)数据错误
    public static final int CONNECT_ERROR = 9003;//网络连接错误
    public static final int TIME_OUT_ERROR = 9004;//网络连接超时
    public static final int UNKNOWNHOSTEXCEPTION = 9005;//网络连接超时

    public static ApiException handleException(Throwable e) {
        ApiException ex;
        if (e instanceof HttpException) {             //HTTP错误
            HttpException httpExc = (HttpException) e;
            ex = new ApiException(e, String.valueOf(httpExc.code()));
            ex.setStatusDesc("网络错误,请稍后再试");  //均视为网络错误
            return ex;
        } else if (e instanceof ServerException) {    //服务器返回的错误
           ServerException serverExc = (ServerException) e;
            ex = new ApiException(serverExc, serverExc.getStatusCode());
            ex.setStatusDesc(serverExc.getStatusDesc());
            return ex;
        } else if (e instanceof JsonParseException
                || e instanceof JSONException
                || e instanceof ParseException || e instanceof MalformedJsonException) {  //解析数据错误
            ex = new ApiException(e, String.valueOf(ANALYTIC_SERVER_DATA_ERROR));
            ex.setStatusDesc("客户端异常,请稍后再试");
            return ex;
        } else if (e instanceof ConnectException) {//连接网络错误
            ex = new ApiException(e, String.valueOf(CONNECT_ERROR));
            ex.setStatusDesc("网络连接错误,请稍后再试");
            return ex;
        } else if (e instanceof SocketTimeoutException) {//网络超时
            ex = new ApiException(e, String.valueOf(TIME_OUT_ERROR));
            ex.setStatusDesc("网络连接超时,请稍后再试");
            return ex;
        }
        else if (e instanceof UnknownHostException) {//网络异常
            ex = new ApiException(e, String.valueOf(UNKNOWNHOSTEXCEPTION));
            ex.setStatusDesc("网络异常,请检查您的网络连接");
            return ex;
        }
        else {  //未知错误
            ex = new ApiException(e, String.valueOf(UN_KNOWN_ERROR));
            ex.setStatusDesc("系统异常,请稍后再试");
            return ex;
        }
    }

}

7、ApiService接口

package com.ylink.frameworkdemo.https;

import com.ylink.frameworkdemo.Constant;
import com.ylink.frameworkdemo.base.BaseDto;
import com.ylink.frameworkdemo.model.dto.LoginDto;
import com.ylink.frameworkdemo.model.vo.LoginVo;

import io.reactivex.Flowable;
import retrofit2.http.Body;
import retrofit2.http.POST;

/**
 * api接口
 *
 * @author twilight
 * @Time 2019-07-21
 *
 * retrofit的注解学习https://blog.csdn.net/qiang_xi/article/details/53959437
 */
public interface ApiService {

    /**
     * 登录
     * @param loginVo
     * @return
     */
    @POST(Constant.Server.LOGIN)
    Flowable<BaseDto<LoginDto>> login(@Body LoginVo loginVo);

}

8、RequestRetrofit类

package com.ylink.frameworkdemo.https;

import android.util.Log;

import com.facebook.stetho.okhttp3.StethoInterceptor;
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import com.ylink.frameworkdemo.Constant;
import com.ylink.frameworkdemo.interceptor.AddCookiesInterceptor;
import com.ylink.frameworkdemo.interceptor.ReceivedCookiesInterceptor;

import java.util.concurrent.TimeUnit;

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

/**
 * 普通的网络请求用到的Retrofit
 */

public class RequetRetrofit {
    private static final String TAG = "RequetRetrofit";
     /**
     * 创建okhttp相关对象
     */
    private static OkHttpClient okHttpClient;
    /**
     * 创建Retrofit相关对象
     */
    private static Retrofit retrofit;

    public static <T> T getInstance(final Class<T> service) {
        if (okHttpClient == null) {
            synchronized (RequetRetrofit.class) {
                if(okHttpClient == null) {
                    /**
                     * 创建okhttp相关对象
                     */
                    okHttpClient = new OkHttpClient.Builder()

                            .addInterceptor(new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
                                @Override
                                public void log(String message) {   //访问网络请求,和服务端响应请求时。将数据拦截并输出
                                    Log.d(TAG, "log: " + message);
                                }
                            }).setLevel(HttpLoggingInterceptor.Level.BODY))     //Log等级
                            .connectTimeout(Constant.Server.TIME_OUT, TimeUnit.SECONDS)       //超时时间
                            .readTimeout(Constant.Server.TIME_OUT, TimeUnit.SECONDS)
                            .writeTimeout(Constant.Server.TIME_OUT, TimeUnit.SECONDS)
                            .addNetworkInterceptor(new StethoInterceptor())
                            .addInterceptor(new AddCookiesInterceptor()) //
                            .addInterceptor(new ReceivedCookiesInterceptor())
                            .build();
                }
            }
        }

        if (retrofit == null) {
            synchronized (RequetRetrofit.class) {
                if(retrofit == null) {
                    retrofit = new Retrofit.Builder()
                            .baseUrl(Constant.Server.ROOT_URL)         //BaseUrl
                            .client(okHttpClient)                       //请求的网络框架
                            .addConverterFactory(GsonConverterFactory.create())     //解析数据格式
                            .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // 使用RxJava作为回调适配器
                            .build();
                }
            }
        }
        return retrofit.create(service);
    }



}

9、AddCookiesInterceptor类

package com.ylink.frameworkdemo.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.ylink.frameworkdemo.Constant;
import com.ylink.frameworkdemo.utils.SPUtil;

import java.io.IOException;
import java.util.List;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
 * 自定义拦截器刷新sessionId  非首次请求的处理
 * @author twilight
 * @Time 2019-07-21
 */
public class AddCookiesInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request.Builder builder = chain.request().newBuilder();
        String cookieStr = SPUtil.getData(Constant.SP.SP, Constant.SP.SESSION_ID, String.class, null);
        List<String> cookies = JSONObject.parseArray(cookieStr, String.class);
        if (cookies != null) {
            for (String cookie : cookies) {
                builder.addHeader("Cookie", cookie);
            }
        }
        return chain.proceed(builder.build());
    }
}

10、ReceivedCookiesInterceptor类

package com.ylink.frameworkdemo.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.ylink.frameworkdemo.Constant;
import com.ylink.frameworkdemo.utils.SPUtil;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import okhttp3.Interceptor;
import okhttp3.Response;

/**
 * 自定义拦截器刷新sessionId 首次请求的处理
 * @author twilight
 * @Time 2019-07-21
 */
public class ReceivedCookiesInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {

        Response originalResponse = chain.proceed(chain.request());
        if (!originalResponse.headers("Set-Cookie").isEmpty()) {
            List<String> cookies = new ArrayList<>();
            for (String header : originalResponse.headers("Set-Cookie")) {
                cookies.add(header);
            }
            String cookieStr = JSONObject.toJSONString(cookies);
            SPUtil.putData(Constant.SP.SP, Constant.SP.SESSION_ID, cookieStr);
        }

        return originalResponse;
    }
}

四、创建model层

1、登录Vo

package com.ylink.frameworkdemo.model.vo;


import java.io.Serializable;

/**
 * 登陆vo
 *
 * @author twilight
 * @Time 2020-01-14
 */

public class LoginVo implements Serializable {
    private String password;
    private String username;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

2、登录Dto

package com.ylink.frameworkdemo.model.dto;
import java.io.Serializable;
/**
 * 登陆返回实体
 *
 * @author twilight
 * @Time 2020-01-14
 */
public class LoginDto implements Serializable {

    private String id;
    private String userName;
    private String userAccount;
    private String sex;
    private String age;
    private String telephone;

    public String getId() {
        return id;
    }

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

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserAccount() {
        return userAccount;
    }

    public void setUserAccount(String userAccount) {
        this.userAccount = userAccount;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    public String getTelephone() {
        return telephone;
    }

    public void setTelephone(String telephone) {
        this.telephone = telephone;
    }
}

五、创建Repository

1、ILoginRepository接口类

package com.ylink.frameworkdemo.repository.network.impl;

import androidx.lifecycle.LiveData;

import com.ylink.frameworkdemo.base.BaseDto;
import com.ylink.frameworkdemo.model.dto.LoginDto;
import com.ylink.frameworkdemo.model.vo.LoginVo;

/**
 * 登录
 */
public interface ILoginRepository {

    /**
     * 登录
     * @param loginVo
     * @return
     */
    LiveData<BaseDto<LoginDto>> login(LoginVo loginVo);
}

2、LoginRepository接口实现类

package com.ylink.frameworkdemo.repository.network;

import androidx.lifecycle.LiveData;

import com.ylink.frameworkdemo.base.BaseDto;
import com.ylink.frameworkdemo.base.BaseRepository;
import com.ylink.frameworkdemo.https.ApiService;
import com.ylink.frameworkdemo.https.RequetRetrofit;
import com.ylink.frameworkdemo.model.dto.LoginDto;
import com.ylink.frameworkdemo.model.vo.LoginVo;
import com.ylink.frameworkdemo.repository.network.impl.ILoginRepository;

/**
 * 登录
 */
public class LoginRepository extends BaseRepository implements ILoginRepository {

    /**
     * 登录
     * @param loginVo
     * @return
     */
    @Override
    public LiveData<BaseDto<LoginDto>> login(LoginVo loginVo) {
        return request(RequetRetrofit.getInstance(ApiService.class).login(loginVo)).get();
    }
}

六、创建View层

1、创建activity_login.xml文件

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".view.login.LoginActivity">

    <EditText
        android:id="@+id/username"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="96dp"
        android:layout_marginEnd="24dp"

        android:hint="用户名"
        android:inputType="textEmailAddress"
        android:selectAllOnFocus="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/password"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="24dp"
        android:hint="密码"
        android:imeOptions="actionDone"
        android:inputType="textPassword"
        android:selectAllOnFocus="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/username" />

    <Button
        android:id="@+id/login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="start"
        android:layout_marginStart="48dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="48dp"
        android:layout_marginBottom="64dp"
        android:enabled="true"
        android:text="登录"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/password"
        app:layout_constraintVertical_bias="0.2" />
</androidx.constraintlayout.widget.ConstraintLayout>

2、创建LoginActivity类

package com.ylink.frameworkdemo.view.login;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;

import com.ylink.frameworkdemo.Constant;
import com.ylink.frameworkdemo.R;
import com.ylink.frameworkdemo.base.BaseDto;
import com.ylink.frameworkdemo.model.dto.LoginDto;
import com.ylink.frameworkdemo.model.vo.LoginVo;
import com.ylink.frameworkdemo.viewmodel.login.LoginViewmodel;

public class LoginActivity extends AppCompatActivity {

    EditText usernameEditText;
    EditText passwordEditText;
    Button loginButton;

    private LoginViewmodel viewmodel;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        viewmodel = ViewModelProviders.of(this).get(LoginViewmodel.class);

        usernameEditText = findViewById(R.id.username);
        passwordEditText = findViewById(R.id.password);
        loginButton = findViewById(R.id.login);

        loginButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                login();
            }
        });
    }

    /**
     * 登录
     */
    private void login(){
        String username = usernameEditText.getText().toString().trim();
        String password = passwordEditText.getText().toString().trim();
        LoginVo loginVo = new LoginVo();
        loginVo.setUsername(username);
        loginVo.setPassword(password);
        viewmodel.login(loginVo).observe(this, new Observer<BaseDto<LoginDto>>() {
            @Override
            public void onChanged(@Nullable BaseDto<LoginDto> loginDtoBaseDto) {
                if(loginDtoBaseDto.getStatusCode().equals(Constant.RespCode.R000)){
                    Toast.makeText(LoginActivity.this,"登录成功",Toast.LENGTH_LONG).show();
                }else{
                    Toast.makeText(LoginActivity.this,loginDtoBaseDto.getStatusDesc(),Toast.LENGTH_LONG).show();
                }
            }
        });
    }
}

七、创建ViewModel层

package com.ylink.frameworkdemo.viewmodel.login;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;

import com.ylink.frameworkdemo.base.BaseDto;
import com.ylink.frameworkdemo.model.dto.LoginDto;
import com.ylink.frameworkdemo.model.vo.LoginVo;
import com.ylink.frameworkdemo.repository.network.LoginRepository;
import com.ylink.frameworkdemo.repository.network.impl.ILoginRepository;

/**
 * 登录页面viewmodel
 */
public class LoginViewmodel extends ViewModel {

    public LiveData<BaseDto<LoginDto>> login(LoginVo loginVo){
        ILoginRepository loginRepository = new LoginRepository();
        return loginRepository.login(loginVo);
    }
}
发布了1 篇原创文章 · 获赞 0 · 访问量 113

猜你喜欢

转载自blog.csdn.net/longlonghaohao/article/details/103975307
今日推荐