Android / IOS 推送的java服務端這幾種設計你知道嗎?


 前幾個月研究推送,小記一下,網上資料亂糟糟還沒有文檔說的明白

裏面有很多其他功能,比如定時,比如別名,比如拉數據等等這些是後續的動作暫且不說,對於推送本身先多理解完善,後續好辦

除了推送其他我就不說了,看文檔接入即可,錯誤碼去文檔中找,說一說具體推送設計和注意點,我用的是如下推送 

  • 個推
  • 小米推送
  • 華爲推送
  • oppo推送
  • vivo推送

個推對於IOS來說是可以的,用的是IOS本身的APN進行推送,app內外都會收到消息;Android的適配場景比較多比較煩,不知道什麼時候能夠“統一全國”啊哈哈,個推打開app其到達率還是可以的,如果關閉app就收不到消息了,所以就有了後續的補充推送,用其本身的推送提高到達率,

不過四個廠商推送還是有不足的地方,有的推送並不能很好的滿足用戶的需求,比如透傳,下面我會說到

 

模板:可以建立推送模板,由數據庫控制,可以借鑑微信的結構,一個表-結構,一個表-數據,或者結構和數據放在一起進行控制,自己可以設計一下表,導出需要推送的數據後根據模板進行推送即可


個推

官方文檔:http://docs.getui.com/getui/server/java/start/

內容比較豐富,比較常用的模板是NotificationTemplate、LinkTemplate和TransmissionTemplate

  • NotificationTemplate:(點擊通知模板)在通知欄顯示一條含圖標、標題等的通知,用戶點擊後激活您的應用(此模板需要點擊纔可以將透傳信息傳遞給前端進行下一步動作)
  • LinkTemplate:(網頁模板)在通知欄顯示一條含圖標、標題等的通知,用戶點擊可打開您指定的網頁。
  • TransmissionTemplate:(透傳模板)透傳消息是指消息傳遞到客戶端只有消息內容,展現形式由客戶端自行定義。客戶端可自定義通知的展現形式,也可自定義通知到達之後的動作,或者不做任何展現。iOS推送也使用該模板。

相關業務場景文檔中也有說明,一般用NotificationTemplate和LinkTemplate就可以做到,我用的是TransmissionTemplate,需要推送到達後客戶端接收透傳信息自動語音播報

/*
    *   Android - ios透傳模板 (不用點擊直接播報)
    */
    public static TransmissionTemplate getTemplate(String appId, String appKey, PushDTO pushDTO) {
        TransmissionTemplate template = new TransmissionTemplate();
        template.setAppId(appId);
        template.setAppkey(appKey);
        //透傳信息
        template.setTransmissionContent(JSON.toJSONString(pushDTO.getTransmissionDTO()));
        template.setTransmissionType(2);
        APNPayload payload = new APNPayload();
        payload.setAutoBadge("+1");
        payload.setContentAvailable(1);
        payload.setSound("default");
        payload.setCategory("$由客戶端定義");
        //字典模式使用下者
        payload.setAlertMsg(getDictionaryAlertMsg(pushDTO.getTitle(), pushDTO.getText()));
        payload.addCustomMsg("contentJsonStr", JSON.toJSONString(pushDTO.getTransmissionDTO()));
        template.setAPNInfo(payload);
        return template;
    }

    //apn消息
    private static APNPayload.DictionaryAlertMsg getDictionaryAlertMsg(String title, String text) {
        APNPayload.DictionaryAlertMsg alertMsg = new APNPayload.DictionaryAlertMsg();
        alertMsg.setBody("");
        alertMsg.setActionLocKey("");
        alertMsg.setLocKey("");
        alertMsg.addLocArg("");
        alertMsg.setLaunchImage("");
        // IOS8.2以上版本支持
        alertMsg.setTitle(title);
        alertMsg.setSubtitle(text);
        alertMsg.setTitleLocKey("");
        alertMsg.addTitleLocArg("");
        return alertMsg;
    }

模板我是這樣的設置,文檔中也有,下面參照文檔根據cid推送-單推,Android和IOS都可用這個接口

private Map<String, Object> pushToSingle(String url, String appKey, String masterSecret, String appId,
                                             PushDTO pushDTO) {
        try {
            IGtPush push = new IGtPush(url, appKey, masterSecret);
            TransmissionTemplate template = getTemplate(appId, appKey, pushDTO);

            SingleMessage message = new SingleMessage();
            message.setOffline(true);
            // 離線有效時間,單位爲毫秒,可選
            message.setOfflineExpireTime(24 * 3600 * 1000);
            message.setData(template);
            // 可選,1爲wifi,0爲不限制網絡環境。根據手機處於的網絡情況,決定是否下發
            message.setPushNetWorkType(0);
            Target target = new Target();
            target.setAppId(appId);
            target.setClientId(pushDTO.getCid());
            IPushResult ret = null;
            try {
                ret = push.pushMessageToSingle(message, target);
            } catch (RequestException e) {
                log.info("推送失敗,重新推送:cid:{}", pushDTO.getCid());
                e.printStackTrace();
                //下面是重推接口
                ret = push.pushMessageToSingle(message, target, e.getRequestId());
            }
            if (ret != null) {
                log.info("android/ios cid:{}對單個用戶推送消息返回:{}", pushDTO.getCid(), ret.getResponse());
                return ret.getResponse();
            }
            return null;
        } catch (Exception e) {
            log.error("cid:{}對單個用戶推送消息錯誤:{}", pushDTO.getCid(), e);
            return null;
        }
    }

返回形式:{taskId=OSS-0212_60001748948e9d3349048409ca699882, result=ok, status=successed_offline}

羣推

private Map<String, Object> pushToList(String url, String appKey, String masterSecret, String appId,
                                           PushDTO pushDTO) {
        try {
            // 配置返回對應cid的用戶狀態,可選
            System.setProperty("gexin_pushList_needDetails", "true");
            IGtPush push = new IGtPush(url, appKey, masterSecret);
            // 通知透傳模板
            TransmissionTemplate template = getTemplate(appId, appKey, pushDTO);
            ListMessage message = new ListMessage();
            message.setData(template);
            // 設置消息離線,並設置離線時間
            message.setOffline(true);
            // 離線有效時間,單位爲毫秒,可選
            message.setOfflineExpireTime(24 * 1000 * 3600);
            // 配置推送目標
            List targets = new ArrayList();
            for (String str : pushDTO.getList()) {
                Target target = new Target();
                target.setAppId(appId);
                target.setClientId(str);
                targets.add(target);
            }

            // taskId用於在推送時去查找對應的message
            String taskId = push.getContentId(message);
            IPushResult ret = push.pushMessageToList(taskId, targets);
            log.info("android/ios - cid對指定列表用戶推送消息pushMessageToList返回:{}", ret.getResponse());
            return ret.getResponse();
        } catch (Exception e) {
            log.info("cid們{},推送消息pushMessageToList錯誤:{}", pushDTO.getList(), e);
            return null;
        }
    }

返回形式:

{
    "result":"ok",
    "details":{
        "c281a495eb69d4fbd4f9d795bd003ea9":"successed_offline",
        "c281a495eb69d4fbd4f9d79555555555":"TokenMD5Error"
    },
    "contentId":"OSL-0212_73lrrQZSBP9sPJmEY30Eg8"
}

推送給所有app我就不貼了

這裏是Android常見問題,瞭解一下cid爲什麼會變等等:http://docs.getui.com/question/getui/android/

個推可以分環境測試,有幾個環境就建幾個app包


小米推送

官方文檔:https://dev.mi.com/console/doc/detail?pId=1278

小米推送比較簡單也比較完善,直接貼碼構建信息

private Message buildMessageForAndroid(String title, String description, String messagePayload) {
        Message message;
        Message.Builder builder = new Message.Builder()
                .title(title)//標題(注意16字真言限制長度,這段畫上重點考)
                .description(description).payload(messagePayload)//描述(注意128限制長度,這段畫上重點考,這個描述,我理解爲副標題,而且在手機客戶端呈現的也是標題+描述,內容不會自己顯示出來,如果只是爲了通知用戶信息,我們可以將信息內容放在此處,顯示效果比較明顯。但是三個文字區域都不可空。需要補充文字方可使用)
                .restrictedPackageName("com.mo.dr")//APP包名
                .passThrough(0)//是否透傳
                .notifyType(1)//設置震動,響鈴等等
                .notifyId((int)(Math.random()*90+10))
//                .extra("extend_content", extendContent);//這裏要注意下,你可以通過自定義的key傳給客戶端一段透傳參數
                .extra(Constants.EXTRA_PARAM_NOTIFY_EFFECT, Constants.NOTIFY_LAUNCHER_ACTIVITY);
        message = builder.build();
        return message;
    }

根據regid推送消息,regId是app在客戶端向小米推送服務註冊時, 小米推送服務端根據設備標識和appId以及當前時間戳生成, 因此能夠保證每個設備上每個app對應的regId都是不同的, 可以作爲每臺設備上app的唯一標識;和個推的clientid一個道理

注意自定義key   .extra,預定義通知欄通知的點擊行爲,一共有三種點擊方式,參照文檔設置一下

1.打開當前app對應的activity

2.打開當前app內任意一個activity

3.打開網頁

單推

public Result sendMessageToRegId(String title, String description,
                                     String messagePayload, String regId) {
        Constants.useOfficial();//這裏要注意,這是正式-啓動方式,支持IOS跟Android,Constants.useSandbox();這是測試-啓動方式,不支持Android,儘量申請正式APP,利用正式環境測試
        Sender sender = new Sender("你的secret");
        Message message = buildMessageForAndroid(title, description, messagePayload);
        Result result = null;
        try {
            result = sender.send(message, regId, 3);
            /**
             * 發送消息返回的錯誤碼,如果返回ErrorCode.Success表示發送成功,其他表示發送失敗。
             */
            ErrorCode errorMessage = result.getErrorCode();
            /**
             * 如果消息發送成功,服務器返回消息的ID;如果發送失敗,返回null。
             */
            String platformMessageId = result.getMessageId();

            /**
             *  如果消息發送失敗,服務器返回錯誤原因的文字描述;如果發送成功,返回null。
             */
            String reason = result.getReason();
            log.debug("MI錯誤碼:{},服務器返回消息的ID-如果發送成功不是null:{}-->錯誤原因 成功爲null:{}",
                    JSON.toJSONString(errorMessage), platformMessageId, reason);
        } catch (Exception e) {
            log.error("MI推送異常:{}",e.toString());
        }
        return result;
    }

羣推

public Result sendMessageToRegIdList(String title, String description,
                                         String messagePayload, List<String> list) {
        log.debug("小米推送list:{}", list);
        Constants.useOfficial();//這裏要注意,這是正式-啓動方式,支持IOS跟Android,Constants.useSandbox();這是測試-啓動方式,不支持Android,儘量申請正式APP,利用正式環境測試
        Sender sender = new Sender("你的secret");
        Message message = buildMessageForAndroid(title, description, messagePayload);
        Result result = null;
        try {
            result = sender.send(message, list, 3);
            log.debug("小米推送list-錯誤碼:{}服務器返回消息的ID成功不是null:{}-->錯誤原因成功不是null:{}", JSON.toJSONString(result.getErrorCode()), result.getMessageId(), result.getReason());
        } catch (Exception e) {
            log.error("MI推送異常:{}",e.toString());
        }
        return result;
    }

注意:根據regids, 發送消息到指定的一組設備上, regids的個數不得超過1000個

 


華爲推送

官方文檔:https://developer.huawei.com/consumer/cn/service/hms/catalog/huaweipush_agent.html?page=hmssdk_huaweipush_devguide_server_agent

分通知欄消息、透傳消息,官方說盡量用通知欄消息,降低功耗,提高到達率,且提供了推送流程兩者之間的對比,可以看到要先獲取token

代碼我就不貼了,官方有示例代碼,基本也都是用這一套,具體設置參照文檔設置滿足自己的需要

華爲推送因爲同一包名不可以建立多個app,所以無法分環境推送,這裏記得測試環境要做好白名單(當然了,如果你有辦法做到分環境推送最好不過)

官方注意:注意:目前access_token的有效期通過返回的expires_in來傳達,access_token的有效時間可能會在未來有調整。access_token在有效期內儘量複用,業務要根據這個有效時間提前去申請新access_token即可。如果業務頻繁申請access_token,可能會被流控。業務在API調用獲知access_token已經超過的情況下(NSP_STATUS=6),可以觸發access_token的申請流程。

我試驗了一下token可以做到分環境控制,token都是3600s過期,且第二次請求獲取的tokenB與第一次的tokenA都有效,各自有各自的失效時間,剛開始我沒發現還以爲第一個會失效,這樣就可以做到token放入緩存中,測試用測試的token,生產用生產的token了

注意每個羣推循環也是不要超過1000個


oppo推送

oppo推送我記得有一個比較坑的一點是oppo安裝了app後推送提醒默認是關閉的…… 透傳消息也做不到,後來都不想用了…… 還是先整理一下吧

官方文檔:http://storepic.oppomobile.com/openplat/resource/201812/03/OPPO%E6%8E%A8%E9%80%81%E5%B9%B3%E5%8F%B0%E6%9C%8D%E5%8A%A1%E7%AB%AFAPI-V1.3.pdf

oppo和華爲一樣也是需要鑑權token,然後攜帶token進行動作

獲取token

private String tokenMake() {
        try {
            long times = System.currentTimeMillis();
            String sign = appKey + times + masterSecret;
            String shaSign = SHA256Util.getSHA256StrJava(sign);
            String data = "app_key=" + appKey + "&timestamp=" + times + "&sign=" + shaSign;
            JSONObject jsonReturn = httpRequest(url + "/server/v1/auth", "POST", data, null);
            log.debug("oppo獲取token返回:{}", jsonReturn);
            if (jsonReturn != null && jsonReturn.getString("code").compareTo("0") == 0) {
                String jsonData = jsonReturn.getString("data");
                String token = (JSONObject.parseObject(jsonData)).getString("auth_token");
                return token;
            }
            log.error("oppo-token沒有獲取到");
            return null;
        } catch (Exception e) {
            log.error("oppo獲取token錯誤:{}", e.toString());
            return null;
        }
    }

appkey和masterSecret自己去平臺找,SHA256加密自己去網上找一下返回形式如下

{
    "code":0,
    "message":"sucess",
    "data":{
        "auth_token":"58ad47319e8d725350a5afd5",
        "create_time":"時間毫秒數"
    }
}

token權限令牌,推送消息時,需要提供 auth_token,有效期默認爲 24 小時,過期後無法使用,這裏有意思了,和華爲不一樣,oppo的token有效時間24小時,24小時內你請求多少次都是這個token,並沒有變,所以也可以不分環境,大家都用這個token就行

消息設置

private JSONObject noticeMessage(TransmissionDTO transmissionDTO) {
        JSONObject notice = new JSONObject();
        notice.put("app_message_id", System.currentTimeMillis());
        notice.put("title", transmissionDTO.getTitleStr());
        notice.put("sub_title"," ");
        notice.put("content", transmissionDTO.getContent());
        notice.put("click_action_type", 0);// 0,啓動應用;1,打開應 用內頁 ;2, 打開網頁; 4,打開應用內頁(activity); 【非必填,默認值爲 0】
        notice.put("show_time_type", 0);//展示類型0即使,1定時
//        notice.put("show_start_time", );//展示開始時間
//        notice.put("show_end_time",);
        notice.put("off_line_ttl", 259200);// 離線消息的存活時間 (單位:秒), 【off_line 值爲 true 時, 必填,最長 3 天259200秒】
        notice.put("push_time_type", 0);//定時推送 (0, “即時”),(1, “定 時”), 【只對全部用戶推送生效】
//        notice.put("push_start_time", )//推送開始時間
        notice.put("network_type", 0);//0不限聯網
        return notice;
    }

單推

public JSONObject oppoPushToSingle(TransmissionDTO transmissionDTO, String regId) {
        try {
            String token = getAuthToken();
            if (token == null) {
                return null;
            }
            JSONObject notice = noticeMessage(transmissionDTO);

            JSONObject param = new JSONObject();
            param.put("target_type", 2);
            param.put("target_value", regId);
            param.put("notification", notice);
            String body = "message=" + JSON.toJSONString(param);

            JSONObject jsonObject = httpRequest(url + "/server/v1/message/notification/unicast",
                    "POST", body, token);
            log.debug("oppo-single推送返回:{}", jsonObject);
            return jsonObject;
        } catch (Exception e) {
            log.error("oppo-single推送失敗:{}");
            return null;
        }
    }

transmissionDTO是透傳信息,regid是設備id,返回形式如下,兩種格式,錯誤情況和成功情況

{"message":"Invalid RegistrationId","code":10000}
{
    "message":"Success",
    "data":{
        "status":"call_success",
        "messageId":"5bf3ad4cfaad176240df4985"
    },
    "code":0
}

羣推

public JSONObject oppoPushToList(TransmissionDTO transmissionDTO, List<String> regIds) {
        log.debug("OPPO羣推接收:{},----:{}", transmissionDTO, regIds);
        try {
            String token = getAuthToken();
            if (token == null) {
                return null;
            }

            JSONObject jsonObject = pushList(transmissionDTO, regIds, token);
            return jsonObject;
        } catch (Exception e) {
            log.error("oppo羣推異常:{}", e.toString());
            return null;
        }
    }

返回形式

{
        "message":"Success",
        "data":[
            {
                "registrationId":"CN_05337b7d20819041980c19955182cdb1",
                "messageId":"5c4c0a7872bc86259df5f557"
            },
            {
                "errorMessage":"Invalid RegistrationId",
                "registrationId":"CN_u2o3hkj54hkj5h3kj4h52k3jh54k3j55",
                "errorCode":10000
            }
        ],
        "code":0
        }

成功的格式和失敗的格式如上所示

http請求方法

private JSONObject httpRequest(String reqUrl, String reqMethod, String jsonDataStr, String authToken) {
        JSONObject jsonObject = null;
        StringBuffer buffer = new StringBuffer();
        try {
            // 建立連接
            URL url = new URL(reqUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoOutput(true);
            connection.setDoInput(true);
            connection.setUseCaches(false);
            connection.setRequestMethod(reqMethod);
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencode");
            if (StringUtils.isNotBlank(authToken)) {
                connection.setRequestProperty("auth_token", authToken);
            }
            if (jsonDataStr != null) {
                OutputStream out = connection.getOutputStream();
                out.write(jsonDataStr.getBytes("UTF-8"));
                out.close();
            }
            // 流處理
            // read response
            InputStream input = null;
            if (connection.getResponseCode() < 400) {
                input = connection.getInputStream();
            } else {
                input = connection.getErrorStream();
            }
            InputStreamReader inputReader = new InputStreamReader(input, "UTF-8");
            BufferedReader reader = new BufferedReader(inputReader);
            String line;
            while ((line = reader.readLine()) != null) {
                buffer.append(line);
            }
            // 關閉連接、釋放資源
            reader.close();
            inputReader.close();
            input.close();
            input = null;
            connection.disconnect();
            jsonObject = JSONObject.parseObject(buffer.toString());
        } catch (Exception e) {
            log.error("vivo請求失敗嘍");
            return null;
        }
        return jsonObject;
    }

還是要注意:regid每次不可超過1000

 


vivo推送

官方文檔:https://swsdl.vivo.com.cn/appstore/developer/uploadfile/20190129/20190129110835218.pdf

獲取鑑權token,有效時間24小時,一天限制調用不超過10000次,注意用緩存來做

private String tokenMake() {
        try {
            long times = System.currentTimeMillis();
            String sign = appid + appkey + times + secret;
            String md5Sign = MD5Util.string2MD5(sign);
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("appId", appid);
            jsonObject.put("appKey", appkey);
            jsonObject.put("timestamp", times);
            jsonObject.put("sign", md5Sign);
            JSONObject jsonReturn = httpRequest(url + "/message/auth", "POST", JSON.toJSONString(jsonObject), null);
            log.debug("vivo獲取authToken返回:{}", jsonReturn);
            if (jsonReturn != null && jsonReturn.getString("result").compareTo("0") == 0) {
                String token = jsonReturn.getString("authToken");
                redisTemplate.opsForValue().set(Constant.VIVO_PUSH_TOKEN, token, 86300, TimeUnit.SECONDS);
                return token;
            }
            log.error("token沒有獲取到");
            return null;
        } catch (Exception e) {
            log.error("vivo獲取token錯誤:{}", e.toString());
            return null;
        }
    }

消息體設置

private static void requestBody(JSONObject body, TransmissionDTO transmissionDTO) {
        body.put("title", transmissionDTO.getTitleStr());//消息標題
        body.put("content", transmissionDTO.getContent());//消息內容體
        body.put("notifyType", "4");
        body.put("timeToLive", "86400");
        body.put("skipType", "1");//點擊跳轉類型 1.打開app首頁,2.打開鏈接 3.自定義 4.打開app內指定頁面
//        body.put("skipContent", "http://www.vivo.com");
        body.put("networkType", "-1");
        body.put("requestId", String.valueOf(System.currentTimeMillis()));
        Map<String, String> map = new HashMap<String, String>();
        map.put("transmissionDTO", JSON.toJSONString(transmissionDTO));
        body.put("clientCustomMap", map);
    }

獲取taskid

private String getTaskId(TransmissionDTO transmissionDTO, String authToken) {
        try {
            JSONObject body = new JSONObject();
            body.put("isBusinessMsg", "0");
            requestBody(body, transmissionDTO);
            JSONObject jsonObject = httpRequest(url + "/message/saveListPayload", "POST", JSON.toJSONString(body), authToken);
            log.debug("取得taskid返回:{}", jsonObject);
            if (jsonObject != null && jsonObject.getString("result").compareTo("0") == 0) {
                return jsonObject.getString("taskId");
            }
            return null;
        } catch (Exception e) {
            log.error("獲取taskid錯誤哦:{}", e.toString());
            return null;
        }
    }

單推

public JSONObject vivoPushToSingle(TransmissionDTO transmissionDTO, String regId) {
        try {
            String token = getAuthToken();
            log.debug("VIVO-token-----》{}", token);
            if (token == null) {
                return null;
            }
            JSONObject param = new JSONObject();
            param.put("callback", "http://www.vivo.com");
            param.put("callback.param", "vivo");

            JSONObject body = new JSONObject();//僅通知欄消息需要設置標題和內容,透傳消息key和value爲用戶自定義
            body.put("regId", regId);

            body.put("extra", param);
            requestBody(body, transmissionDTO);
            log.debug("vivo發送消息體:{}", body);

            JSONObject jsonObject = httpRequest(url + "/message/send", "POST", JSON.toJSONString(body), token);
            log.debug("vivo-single推送返回:{}", jsonObject);
            return jsonObject;
        } catch (Exception e) {
            log.error("vivo-single推送失敗:{}", e.toString());
            return null;
        }
    }

羣推

public JSONObject vivoPushToList(List<String> regIdList, TransmissionDTO transmissionDTO) {
        try {
            log.debug("批量推送接收:regIdList:{}", regIdList);
            log.debug("transmissionDTO---》》{}", transmissionDTO);
            String authToken = getAuthToken();
            if (authToken == null) {
                return null;
            }
            String taskId = getTaskId(transmissionDTO, authToken);
            if (taskId == null) {
                log.error("獲取taskid失敗");
                return null;
            }

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("regIds", regIdList);
            jsonObject.put("taskId", taskId);
            jsonObject.put("requestId", String.valueOf(System.currentTimeMillis()));
            JSONObject jsonReturn = httpRequest(url + "/message/pushToList", "POST", JSON.toJSONString(jsonObject), authToken);
            log.debug("vivo-list推送返回:{}", jsonReturn);
            if (jsonReturn != null && jsonReturn.getString("result").compareTo("0") == 0) {
                return jsonReturn;
            }
            return null;
        } catch (Exception e) {
            log.error("vivo-list推送失敗:{}", e.toString());
            return null;
        }
    }

注意regid一個推送循環不要超過1000個

vivo推送我記得也是沒法透傳,然後手機適配型號還多,有些型號還推送不了有些坑……大家可以再瞭解瞭解再考慮是否用吧


對接好每一個推送,設計好模板,記錄好成功/失敗的記錄,原因等等,單推還好說,羣推要記錄每一條數據的是否推送成功失敗原因等等,並且要測一下推送的每次推送吞吐量是多少,會不會崩潰等等

白名單要做好,推送對於用戶來說最忌諱的就是真實用戶收到了測試的亂七八糟的推送那就尷尬了,甚至一條推送循環推送多次,用戶體驗將會降到冰點,一定一定一定要注意檢查這個,沒發不要緊,發錯了,重複發纔是致命的

good luck

 

 

 

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