三分钟帮你集成极光推送——和那些你可能不知道的事

本文简介:本文前篇,可以帮助朋友们快速集成极光推送。本文后篇,是我自己项目实践的一些总结和心得,应该对读者们还是很有参考价值的,相信读完这篇文章,你会对极光推送有更加深入的理解,而不仅仅只是会集成而已。总之呢,集成第三方SDK,都不是很难的事情,仔细阅读文档,一步步来,遇到Bug,慢慢解决就行,实在解决不了,可以问问客服小哥哥或者小姐姐,重要的是,你得有着解决它的决心和耐心。

《一》JPush SDK的集成

简要介绍:
极光推送(JPush)是一个端到端的推送服务,使得服务器端消息能够及时地推送到终端用户手机上,让开发者积极地保持与用户的连接,从而提高用户活跃度、提高应用的留存率。

开发者集成 JPush Android SDK 到其应用里,JPush Android SDK 创建到 JPush Cloud 的长连接,为 App 提供永远在线的能力。 当开发者想要及时地推送消息到达 App 时,只需要调用 JPush API 推送,或者使用其他方便的智能推送工具,即可轻松与用户交流。

JPush Android SDK 是作为 Android Service 长期运行在后台的,从而创建并保持长连接,保持永远在线的能力。

假设你已经注册了极光的账号,登录进来了。点击立即体验,创建自己的应用。

在这里插入图片描述App端集成需要用到的AppKey。在这里插入图片描述在这里插入图片描述添加集成代码,此处使用jcenter的方式集成。 一、确认android studio的 Project 根目录的主 gradle 中配置了jcenter支持。(新建project默认配置就支持)

buildscript {
    repositories {
        jcenter()
    }
    ......
}

allprojets {
    repositories {
        jcenter()
    }
}

二、在 module 的 gradle 中添加依赖和AndroidManifest的替换变量。

android {
    ......
    defaultConfig {
        applicationId "com.xxx.xxx" //JPush上注册的包名.
        ......

        ndk {
            //选择要添加的对应cpu类型的.so库。
            abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
            // 还可以添加 'x86', 'x86_64', 'mips', 'mips64'
        }

        manifestPlaceholders = [
            JPUSH_PKGNAME : applicationId,
            JPUSH_APPKEY : "你的appkey", //JPush上注册的包名对应的appkey.
            JPUSH_CHANNEL : "developer-default", //暂时填写默认值即可.
        ]
        ......
    }
    ......
}

dependencies {
    ......

    compile 'cn.jiguang.sdk:jpush:3.1.1'  // 此处以JPush 3.1.1 版本为例。
    compile 'cn.jiguang.sdk:jcore:1.1.9'  // 此处以JCore 1.1.9 版本为例。
    ......
}

三、AndroidManifest.xml中添加

<!--Jpush配置 所需权限start-->
<!-- Required -->
<permission
    android:name="你的应用包名.permission.JPUSH_MESSAGE"
    android:protectionLevel="signature" />

<!-- Required  一些系统要求的权限,如访问网络等-->
<uses-permission android:name="你的应用包名.permission.JPUSH_MESSAGE" />
<uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
<!--<uses-permission android:name="android.permission.INTERNET" />-->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--<uses-permission android:name="android.permission.READ_PHONE_STATE" />-->
<!--<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />-->
<!--<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />-->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<!--<uses-permission android:name="android.permission.VIBRATE" />-->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<!--<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />-->
<!--<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />-->

<!-- Optional for location -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 用于开启 debug 版本的应用在6.0 系统上 层叠窗口权限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />-->
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />-->
<!--<uses-permission android:name="android.permission.GET_TASKS" />-->
<!--Jpush配置 所需权限end-->

<!--Jpush配置 start-->
<application
    <!-- Rich push 核心功能 since 2.0.6-->
    <activity
        android:name="cn.jpush.android.ui.PopWinActivity"
        android:theme="@style/MyDialogStyle"
        android:exported="false">
    </activity>

    <!-- Required SDK核心功能-->
    <activity
        android:name="cn.jpush.android.ui.PushActivity"
        android:configChanges="orientation|keyboardHidden"
        android:theme="@android:style/Theme.NoTitleBar"
        android:exported="false">
        <intent-filter>
            <action android:name="cn.jpush.android.ui.PushActivity" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="你的应用包名" />
        </intent-filter>
    </activity>

    <!-- Required SDK 核心功能-->
    <!-- 可配置android:process参数将PushService放在其他进程中 -->
    <service
        android:name="cn.jpush.android.service.PushService"
        android:enabled="true"
        android:exported="false">
        <intent-filter>
            <action android:name="cn.jpush.android.intent.REGISTER" />
            <action android:name="cn.jpush.android.intent.REPORT" />
            <action android:name="cn.jpush.android.intent.PushService" />
            <action android:name="cn.jpush.android.intent.PUSH_TIME" />
        </intent-filter>
    </service>
    <!-- since 3.0.9 Required SDK 核心功能-->
    <provider
        android:authorities="你的应用包名.DataProvider"
        android:name="cn.jpush.android.service.DataProvider"
        android:exported="false"
        />

    <!-- since 1.8.0 option 可选项。用于同一设备中不同应用的JPush服务相互拉起的功能。 -->
    <!-- 若不启用该功能可删除该组件,将不拉起其他应用也不能被其他应用拉起 -->
    <service
        android:name="cn.jpush.android.service.DaemonService"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="cn.jpush.android.intent.DaemonService" />
            <category android:name="你的应用包名" />
        </intent-filter>

    </service>
    <!-- since 3.1.0 Required SDK 核心功能-->
    <provider
        android:authorities="你的应用包名.DownloadProvider"
        android:name="cn.jpush.android.service.DownloadProvider"
        android:exported="true"
        />
    <!-- Required SDK核心功能-->
    <receiver
        android:name="cn.jpush.android.service.PushReceiver"
        android:enabled="true"
        android:exported="false">
        <intent-filter android:priority="1000">
            <action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED_PROXY" />   <!--Required  显示通知栏 -->
            <category android:name="你的应用包名" />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.USER_PRESENT" />
            <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
        </intent-filter>
        <!-- Optional -->
        <intent-filter>
            <action android:name="android.intent.action.PACKAGE_ADDED" />
            <action android:name="android.intent.action.PACKAGE_REMOVED" />

            <data android:scheme="package" />
        </intent-filter>
    </receiver>

    <!-- Required SDK核心功能-->
    <receiver android:name="cn.jpush.android.service.AlarmReceiver" android:exported="false"/>

    <!-- User defined.  For test only  MyReceiver为用户自定义的广播接收器-->
    <receiver
        android:name=".util.MyReceiver"
        android:exported="false"
        android:enabled="true">
        <intent-filter>
            <action android:name="cn.jpush.android.intent.REGISTRATION" /> <!--Required  用户注册SDK的intent-->
            <action android:name="cn.jpush.android.intent.MESSAGE_RECEIVED" /> <!--Required  用户接收SDK消息的intent-->
            <action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED" /> <!--Required  用户接收SDK通知栏信息的intent-->
            <action android:name="cn.jpush.android.intent.NOTIFICATION_OPENED" /> <!--Required  用户打开自定义通知栏的intent-->
            <action android:name="cn.jpush.android.intent.CONNECTION" /><!-- 接收网络变化 连接/断开 since 1.6.3 -->
            <category android:name="你的应用包名" />
        </intent-filter>
    </receiver>

    <!-- Required  . Enable it you can get statistics data with channel -->
    <meta-data android:name="JPUSH_CHANNEL" android:value="developer-default"/>
    <meta-data android:name="JPUSH_APPKEY" android:value="应用的Appkey" /> <!--  </>值来自开发者平台取得的AppKey-->
</application>
    <!--Jpush配置 end-->

四、自定义一个广播接收器MyReceiver(此处直接使用官方Demo中的MyReceiver)

/**
 * 自定义接收器
 * 
 * 如果不定义这个 Receiver,则:
 * 1) 默认用户会打开主界面
 * 2) 接收不到自定义消息
 */
public class MyReceiver extends BroadcastReceiver {
	private static final String TAG = "JIGUANG-Example";

	@Override
	public void onReceive(Context context, Intent intent) {
		try {
			Bundle bundle = intent.getExtras();
			Logger.d(TAG, "[MyReceiver] onReceive - " + intent.getAction() + ", extras: " + printBundle(bundle));

			if (JPushInterface.ACTION_REGISTRATION_ID.equals(intent.getAction())) {
				String regId = bundle.getString(JPushInterface.EXTRA_REGISTRATION_ID);
				Logger.d(TAG, "[MyReceiver] 接收Registration Id : " + regId);
				//send the Registration Id to your server...

			} else if (JPushInterface.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
				Logger.d(TAG, "[MyReceiver] 接收到推送下来的自定义消息: " + bundle.getString(JPushInterface.EXTRA_MESSAGE));
				processCustomMessage(context, bundle);

			} else if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) {
				Logger.d(TAG, "[MyReceiver] 接收到推送下来的通知");
				int notifactionId = bundle.getInt(JPushInterface.EXTRA_NOTIFICATION_ID);
				Logger.d(TAG, "[MyReceiver] 接收到推送下来的通知的ID: " + notifactionId);

			} else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) {
				Logger.d(TAG, "[MyReceiver] 用户点击打开了通知");

				//打开自定义的Activity
				Intent i = new Intent(context, TestActivity.class);
				i.putExtras(bundle);
				//i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
				i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP );
				context.startActivity(i);

			} else if (JPushInterface.ACTION_RICHPUSH_CALLBACK.equals(intent.getAction())) {
				Logger.d(TAG, "[MyReceiver] 用户收到到RICH PUSH CALLBACK: " + bundle.getString(JPushInterface.EXTRA_EXTRA));
				//在这里根据 JPushInterface.EXTRA_EXTRA 的内容处理代码,比如打开新的Activity, 打开一个网页等..

			} else if(JPushInterface.ACTION_CONNECTION_CHANGE.equals(intent.getAction())) {
				boolean connected = intent.getBooleanExtra(JPushInterface.EXTRA_CONNECTION_CHANGE, false);
				Logger.w(TAG, "[MyReceiver]" + intent.getAction() +" connected state change to "+connected);
			} else {
				Logger.d(TAG, "[MyReceiver] Unhandled intent - " + intent.getAction());
			}
		} catch (Exception e){

		}

	}

	// 打印所有的 intent extra 数据
	private static String printBundle(Bundle bundle) {
		StringBuilder sb = new StringBuilder();
		for (String key : bundle.keySet()) {
			if (key.equals(JPushInterface.EXTRA_NOTIFICATION_ID)) {
				sb.append("\nkey:" + key + ", value:" + bundle.getInt(key));
			}else if(key.equals(JPushInterface.EXTRA_CONNECTION_CHANGE)){
				sb.append("\nkey:" + key + ", value:" + bundle.getBoolean(key));
			} else if (key.equals(JPushInterface.EXTRA_EXTRA)) {
				if (TextUtils.isEmpty(bundle.getString(JPushInterface.EXTRA_EXTRA))) {
					Logger.i(TAG, "This message has no Extra data");
					continue;
				}

				try {
					JSONObject json = new JSONObject(bundle.getString(JPushInterface.EXTRA_EXTRA));
					Iterator<String> it =  json.keys();

					while (it.hasNext()) {
						String myKey = it.next();
						sb.append("\nkey:" + key + ", value: [" +
								myKey + " - " +json.optString(myKey) + "]");
					}
				} catch (JSONException e) {
					Logger.e(TAG, "Get message extra JSON error!");
				}

			} else {
				sb.append("\nkey:" + key + ", value:" + bundle.getString(key));
			}
		}
		return sb.toString();
	}
	
	//send msg to MainActivity
	private void processCustomMessage(Context context, Bundle bundle) {
		if (MainActivity.isForeground) {
			String message = bundle.getString(JPushInterface.EXTRA_MESSAGE);
			String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
			Intent msgIntent = new Intent(MainActivity.MESSAGE_RECEIVED_ACTION);
			msgIntent.putExtra(MainActivity.KEY_MESSAGE, message);
			if (!ExampleUtil.isEmpty(extras)) {
				try {
					JSONObject extraJson = new JSONObject(extras);
					if (extraJson.length() > 0) {
						msgIntent.putExtra(MainActivity.KEY_EXTRAS, extras);
					}
				} catch (JSONException e) {

				}

			}
			LocalBroadcastManager.getInstance(context).sendBroadcast(msgIntent);
		}
	}
}

五、在Application中的onCreate()方法中初始化极光推送

    // 初始化 JPush
    JPushInterface.init(this);
    //发布时关闭日志
    JPushInterface.setDebugMode(false);

六、最后不要忘了添加混淆代码哦!

请在工程的混淆文件proguard-rules.pro中添加以下配置:

-dontoptimize
-dontpreverify

-dontwarn cn.jpush.**
-keep class cn.jpush.** { *; }
-keep class * extends cn.jpush.android.helpers.JPushMessageReceiver { *; }

-dontwarn cn.jiguang.**
-keep class cn.jiguang.** { *; }

《二》推送方式分析

使用场景分析:推送消息,无疑就是两种情形,一种是全部推送,这种方式简单,群发就行了,Portal与API都支持向指定的 appKey 群发消息。但是一般实际的业务需求,都不仅仅是群发,还需要针对某一个人或者某一群人进行推送。例如:给会员推送一些新的内容,此时就需要针对会员这一群特定的人,来进行推送。简单的说,就是需要把JPush的注册用户与开发者App用户绑定起来,以达到精准推送的目的。

1.RegistrationID方式实现点对点的精准推送(把绑定关系保存到开发者应用服务器中)

集成了 JPush SDK 的应用程序在第一次成功注册到 JPush 服务器时,JPush 服务器会以广播的形式发送RegistrationID到应用程序,给客户端返回一个唯一的该设备的标识 - RegistrationID。首次注册成功,自定义的MyReceiver中会收到一条广播。

如下图,会在MyReceiver中收到RegistrationID的值。只要极光推送第一次注册成功了,后期不会再发 RegistrationID 的广播了。RegistrationID 就会被保留在手机内,下次即使你是无网状态进入APP,你也可以获取到这个RegistrationID。有了这个标识,App 编程可以把这个 RegistrationID 保存到自己的应用服务器上,然后就可以根据 RegistrationID 来向设备推送消息或者通知。所以建议可以在你的Application和MyReceiver中都对这个RegistrationID进行赋值。然后根据项目的业务需要,将RegistrationID和用户标识的对应关系,上传自己的服务端。

在这里插入图片描述

public class MyApplication extends Application{
    public static String registrationID;
     @Override
    public void onCreate() {
        // 初始化 JPush
        JPushInterface.init(this);
        registrationID = JPushInterface.getRegistrationID(this);
        Log.d("TAG", "接收Registration Id : " + registrationID);
    }
}

【注意】:
如果 App 不卸载,是直接覆盖安装,Android, iOS 上 RegistrationID 的值都不会变化。
如果 App 是卸载之后再次安装:Android 上 RegistrationID 基本不会变;
iOS 上如果启用了 IDFA 变化可能性不大,如果未启用 IDFA 则每次安装 RegistrationID 都会变;
参考:极光推送的设备唯一性标识 RegistrationID

如果使用此种推送方式,你可能会遇到的问题:假设在一个设备中登录不同的账号,那此时上传给服务器的都是同一个RegistrationID,因为设备没有变化。那么可能出现:本来服务器是根据A用户找到它的RegistrationID进行推送,但是B用户也是在A用户登录的设备登录的,RegistrationID跟A的一样,这样就导致B用户收到了推送给A用户的推送内容。

需要谨记:

使用 RegistrationID 推送的关键于,App 开发者需要在开发 App 时,获取到这个 RegistrationID,保存到 App 业务服务器上去,并且与自己的用户标识对应起来。建议 App 开发者尽可能做这个保存动作。因为这是最精确地定位到设备的。(RegistrationID 的方式,相对而言比较麻烦一点,因为需要自己的App服务端维护RegistrationID和用户的对应关系。暂时我还没有使用这种方式,真正投入到线上的项目,只是测试环境下调试过。)
值得一读:推送人群的选择 – 推送方式-技术篇

2.别名与标签推送(把绑定关系保存到 JPush 服务器端)

别名推送也是一种实现点对点推送的方式,用于给某特定用户推送消息。
功能介绍:
①为安装了应用程序的用户,取个别名来标识。以后给该用户 Push 消息时,就可以用此别名来指定。
②每个用户只能指定一个别名。
③同一个应用程序内,对不同的用户,建议取不同的别名。这样,尽可能根据别名来唯一确定用户。
④系统不限定一个别名只能指定一个用户。如果一个别名被指定到了多个用户,当给指定这个别名发消息时,服务器端API会同时给这多个用户发送消息。

举例:在一个用户要登录的游戏中,可能设置别名为 userid。游戏运营时,发现该用户 3 天没有玩游戏了,则根据 userid 调用服务器端API发通知到客户端提醒用户。

别名设置:
需要和自己的服务端协商好别名的规则,例如下面的代码中,是将用户ID通过MD5加密,作为别名,设置保存到JPush服务器。当然你也可以使用其他的拼接规则,具体根据每个公司的项目需要设置即可,只要满足别名的命名限制就行: 命名长度限制为 40 字节。(判断长度需采用UTF-8编码)
深入理解各种推送方式可以参考:推送人群的选择 – 推送方式-技术篇

下面是一个JPush设置别名和标签的辅助类。

public class JPushHelper {
    private String TAG = "JPushHelper";

    /**
     * 设置别名与标签
     *
     * @param UUID
     */
    private void setAlias(String UUID) {
        if (null != UUID) {
            //恢复接收推送
            JPushInterface.resumePush(MyApplication.getInstance());
            JPushInterface.setAliasAndTags(MyApplication.getInstance(), UUID, null, mAliasCallback);
        }
    }

    public void setAlias() {
        if (MyApplication.isLogin) { //必须登录
            UserInfo userBean = ACT_Login.getLoginUser();
            if (null != userBean) {
                 //根据具体业务需求,可以再拼接上其他相关字段
                //String alias = "";
                //StringBuilder stringBuffer = new StringBuilder();
                // stringBuffer.append(StringUtil.getString(userBean.user_id));
                Log.e(TAG, stringBuffer.toString());
                alias = MD5Util.string2MD5(userBean.user_id);
                //限制:alias 命名长度限制为 40 字节。(判断长度需采用UTF-8编码)
                Log.e(TAG, alias);
                //setAlias("");
                setAlias(alias);
            }
        }
    }

    /**
     * 停止接收推送
     */
    public void removeAlias() {
        JPushInterface.clearAllNotifications(MyApplication.getInstance());
        //setAlias("");//该句打开的话,如果退出登录后,用户就收不到离线的消息了。
        JPushInterface.stopPush(MyApplication.getInstance());
    }

    private final TagAliasCallback mAliasCallback = new TagAliasCallback() {

        @Override
        public void gotResult(int code, String alias, Set<String> tags) {
            String logs;
            switch (code) {
                case 0:
    // 建议这里往 SharePreference 里写一个成功设置的状态。成功设置一次后,以后不必再次设置了。
                    logs = "Set tag and alias success";
                    Log.e(TAG, logs);
                    break;
                case 6002:
                    logs = "Failed to set alias and tags due to timeout. Try again after 60s.";
                    // 延迟 60 秒来调用 Handler 设置别名
                    mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SET_ALIAS, alias), 1000 * 6);
                    Log.e(TAG, logs + AppDateUtil.getTimeStamp(System.currentTimeMillis(), AppDateUtil.MM_DD_HH_MM_SS));
                    break;
                default:
                    logs = "Failed with errorCode = " + code;
                    Log.e(TAG, logs);
            }

        }

    };

    private static final int MSG_SET_ALIAS = 1001;
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(android.os.Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MSG_SET_ALIAS:
                    Log.e(TAG, "Set alias in handler." + ((String) msg.obj));
                    // 调用 JPush 接口来设置别名。
                    JPushInterface.setAliasAndTags(MyApplication.getInstance(),
                            (String) msg.obj,
                            null,
                            mAliasCallback);
                    break;
                default:
                    Log.e(TAG, "Unhandled msg - " + msg.what);
            }
        }
    };

    // 校验Tag Alias 只能是数字,英文字母和中文
    public static boolean isValidTagAndAlias(String s) {
        Pattern p = Pattern.compile("^[\u4E00-\u9FA50-9a-zA-Z_!@#$&*+=.|]+$");
        Matcher m = p.matcher(s);
        return m.matches();
    }
}

3.关于后端服务器设置别名还是前端设置别名:

说实在的,以前没有遇到过这个选择题,因为之前做的极光推送都是在我们客户端设置,不过最近调试JPush的时候后台说他设置别名,我突然就有点蒙了?不是一般都是前端设置吗???
于是有个疑问:如果是服务端设置别名,那服务端也没有经过客户端,那极光服务器咋通过别名来匹配用户进行点对点推送呀???我一下子有点想不通了。然后查了一下文档和相关博客,确实极光服务器也给后端服务提供设置别名的API,又问了一下后端开发,他是不是只是单单设置了别名,还是在设备ID上也有做了处理,因为没有映射关系,极光服务器也不可能找到对应的用户进行推送呀。果不其然,他是在RegistrationID上设置的别名,这下我就明白了。
不过大部分的情况,应该还是前端设置别名吧,毕竟那些登入登出的操作在前端控制会比较方便。

小提醒:

实际应用场景,客户端一般都需要在登录到App成功后,设置别名,恢复接收推送。退出登录后,停止接收推送。
恢复接收推送:

 JPushInterface.resumePush(MyApplication.getInstance());

停止接收推送:

JPushInterface.stopPush(MyApplication.getInstance());

4.App杀死后还想要收到推送

有的公司由于业务需求,可能会要求你,App杀死后还想要收到推送。如果知道JPush Android SDK 是作为 Android Service 长期运行在后台的,从而创建并保持长连接,保持永远在线的能力…的童鞋们,那么必须要清楚一点的是:如果APP真的被杀死了,是不可能收到推送的,如果杀死了还能收到,那说明可能是以下几种情况:

①应用自启动了
②要么是其他方式将App拉起来了。
③你压根就没有杀死App,Service还在运行着。(有的手机清理App,并没有完全杀死进程的)

关于这个问题可以看看这位小姐姐的总结:
Android 关于App被杀死后,如何接收极光推送
极光推送相关阅读参考:
官网集成
极光推送Android端API
详解极光推送的 4 种消息形式 —— Android 篇
常见问题 - JPush 合集(持续更新)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章