Web網(wǎng)絡(luò)服務(wù)模塊——程序自動升級

強制升級機制

使用某些軟件時經(jīng)常遇到“發(fā)現(xiàn)新版本昆雀,馬上升級”的提示辱志。對于一些程序,可以選擇忽略提示狞膘,不進行升級揩懒。但有時,程序只給用戶提供升級按鈕挽封,無法選擇忽略提示的升級信息已球,這就是強制升級的機制。

強制升級主要出于兩個方面的原因:1. 發(fā)現(xiàn)當(dāng)前程序的主要bug辅愿,必須升級補丁智亮,否則程序運行會出現(xiàn)問題;2. 程序某些關(guān)鍵功能模塊需要進行更新点待,如果不升級阔蛉,則軟件的某些功能將不能使用,或者使用出錯癞埠。

本系列筆記中books項目使用了強制自動升級的策略状原,我們也可以將其改為可選的升級策略。

實現(xiàn)強制升級機制需要以下3個方面的內(nèi)容:

  1. 程序啟動時要從網(wǎng)絡(luò)讀取程序新版本信息苗踪。
  2. 讀出的新版本信息要與本地程序版本號進行對比颠区,并根據(jù)版本對比結(jié)果判斷是否需要升級,如果需要則進入下一步通铲。
  3. 鎖定其他程序按鈕毕莱,只允許用戶點擊升級按鈕。單擊后程序進入升級程序颅夺。

系統(tǒng)實現(xiàn)

讀取INI文件中的版本信息

books項目中使用INI文件記錄程序的版本信息及相關(guān)的配置狀態(tài)央串。更多關(guān)于INI文件的信息可參考INI配置文件的格式

讀取版本信息包含兩方面內(nèi)容:1. 從網(wǎng)絡(luò)中讀取是否存在新版本程序碗啄,若有則讀取新版本號质和;2. 讀取本地版本信息。

從網(wǎng)絡(luò)中讀取新版本程序時稚字,可以在指定的某處網(wǎng)站保存一個INI文件饲宿,該文件內(nèi)部記錄了新程序版本號,程序升級區(qū)域限制及最新程序下載地址等胆描。例如GitHub—initxuan::books/updateversion.ini文件瘫想,其關(guān)鍵部分如下:

[update]
updateZone = ALL
updateVersion = 2.0

updateZone用于指定升級區(qū)域,不同區(qū)域用戶可以根據(jù)自身情況選擇是否允許升級昌讲,或者升級不同區(qū)域新版本程序国夜,這樣便于進行區(qū)域控制。此處可以通過配置參數(shù)來指定區(qū)域進行升級短绸,如不同區(qū)域可通過設(shè)置區(qū)號進行區(qū)別车吹,不同區(qū)域的區(qū)號用空格分隔筹裕;如果參數(shù)設(shè)置為“ALL”則表示所有區(qū)域均可升級。updateVersion用于指定當(dāng)前最新程序版本號窄驹。應(yīng)用程序會讀取updateVersion的數(shù)值朝卒,只有當(dāng)它比用戶當(dāng)前程序的版本號大時,升級程序才會啟動乐埠。

當(dāng)我們使用INI文件進行版本信息的記錄時抗斤,需要將INI文件放置在某網(wǎng)站目錄下,共程序下載后讀取丈咐。

設(shè)計本地信息INI文件

在本地我們也需要使用INI文件來記錄程序的相關(guān)信息包括當(dāng)前程序的版本號瑞眼、要從哪個網(wǎng)址下載升級程序等信息。例如GitHub—initxuan::books/config.ini棵逊,其關(guān)鍵部分如下:

[program]
userZone = 
username =
userID =
version = 1.0
installedDir = 

[update]
baseUrl = http://xxx.cn/release/
exeFileName = _update_setup_
versionFileName = updateversion.ini
updateLocalDir = update

[program]區(qū)塊指定了程序相關(guān)的所有信息伤疙,包括用戶的區(qū)域、用戶名歹河、用戶ID掩浙、當(dāng)前程序版本號等。[update]區(qū)塊定義了從哪里下載升級INI信息文件秸歧,versionFileName指定了要下載的INI文件名厨姚,updateLocalDir指定了下載后的文件要放在哪個目錄下(注:該值要作為參數(shù)傳給上一篇筆記中記述的mydir.mkdir(LOCALUPDATEDIR)函數(shù),在目錄中創(chuàng)建新的update目錄键菱,并將升級的EXE文件下載到這里谬墙。)最后exeFileName指定了要下載的EXE文件名,按照程序規(guī)則经备,會在這個文件名前加上用戶區(qū)域信息段拭抬,在文件名后加上版本號和“.exe”后綴,形成類似于“ALL_update_setup_2.0.exe”的文件名侵蒙,然后在baseUrl指定的網(wǎng)址目錄下下載這個文件造虎。

讀寫本地目錄下的INI文件

本地信息INI文件可以由程序設(shè)計者自由選擇目錄存放,按照慣例纷闺,這種信息文件一般放在兩個位置:1. 應(yīng)用程序所在目錄算凿。2. Windows常用的系統(tǒng)目錄,如My Documents目錄犁功。books項目使用后者氓轰,將其放在My Documents下面的一個子目錄,供程序讀取浸卦。Qt程序提取Windows系統(tǒng)的目錄My Documents時分為兩步:1. 使用QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation)得到目錄My Documents的字符串列表署鸡;2. 從列表頭部提取出目錄的字符串。代碼如下:

// 0.1尋找系統(tǒng)INI // my documents/x/config.ini
QStringList slist = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);    // 第一步
QDir documentsDir = slist.at(0);    //第二步
QString configIni = "/x/config.ini";
QString configIniWhole = documentsDir.path() + configIni;

因為讀出的目錄My Documents尾部沒有“/”,因此要在“/x/config.ini”頭部加一個“/”靴庆,形成完整的目錄文件地址时捌。

使用Qt提取其他Windows系統(tǒng)目錄時同樣采用這兩個函數(shù),具體內(nèi)容參見QStandardPaths Class | Qt Core 5.11

讀出INI文件之后撒穷,就可以對INI文件進行讀取解析了匣椰。Qt為讀取INI文件提供了非常方便使用的類:QSettings裆熙,使用時僅需將INI文件名作為參數(shù)實例化該類端礼,便可進行信息讀取工作。參考代碼如下:

// 0.2讀入INI
QSettings configIniRead(configIniWhole, QSettings::IniFormat);
// 0.2.1 config
curUserName = configIniRead.value("/program/username").toString();
if(curUserName.isEmpty()) isLogin = false;
else isLogin = true;
curVersion = configIniRead.value("/program/version").toDouble();
curInstalledDir = configIniRead.value("/program/installedDir").toString();
QSettings configIniWrite(configIniWhole, QSettings::IniFormat);
configIniWrite.setValue("/program/installedDir", QDir::currentPath().trimmed());

//0.2.2 update
baseUrl = configIniRead.value("/update/baseUrl").toString();
exeFileName = configIniRead.value("/update/exeFileName").toString();
versionFileName = configIniRead.value("/update/versionFileName").toString();
updateDir = configIniRead.value("/update/updateLocalDir").toString();
isThereNewUpdate = false;    // 先設(shè)置為false入录,后面有比較版本號蛤奥,如果大于當(dāng)前版本,則設(shè)置為true
qDebug() << "---" << curVersion << "==" << curUserName << "==" << isLogin;

用戶登錄后就將用戶信息記錄在本地INI文件中僚稿,如果INI文件中沒有用戶信息凡桥,則表示用戶未登錄。程序先讀取INI文件中用戶信息蚀同,判斷用戶是否登錄缅刽;然后讀取升級相關(guān)地址、升級文件名蠢络、升級信息文件名等衰猛;最后設(shè)定isThereNewUpdate為false,等待后續(xù)如果有新版本程序刹孔,再將其設(shè)為true啡省。

向INI文件寫入信息可參考上述代碼中:

QSettings configIniWrite(configIniWhole, QSettings::IniFormat);
configIniWrite.setValue("/program/installedDir", QDir::currentPath().trimmed());

通過定義另一個QSettings實例,使用函數(shù)setValue對指定的INI區(qū)塊寫入信息即可髓霞。

邏輯判斷

接下來程序需要到指定網(wǎng)址下載升級的INI文件卦睹,下載的代碼使用本項目即books項目中的下載類實現(xiàn),然后進行讀取分析方库,進行版本號比較的邏輯判斷结序。

當(dāng)某個功能被頻繁使用時,應(yīng)將該功能封裝成類纵潦。

下載后的INI文件開始進行信息讀取徐鹤,進行版本比較,相關(guān)代碼如下:

QString filename = updateDir + "/" + versionFileName;
QFile file(filename);
if(file.size() == 0){
    QMessageBox::information(this, "升級", "升級文件檢測失敗酪穿,請檢查您的網(wǎng)絡(luò)是否正常凳干。");
}
else{
    // 打開文件,讀版本號
    QSettings updateIniRead(filename, QSettings::IniFormat);
    QString updateZone = updateIniRead.value("/update/updateZone").toString();
    double updateVersion = updateIniRead.value("/update/updateVersion").toDouble();
    QString strUpdateVersion = updateIniRead.value("/update/updateVersion").toString();
    //qDebug() << "here" << updateVersion << updateZone << curUserZone;
    if(!curUserZone.isEmpty() && (updateZone.contains(curUserZone)) ||
      updateZone.contains("ALL") && (updateVersion > curVersion)){ // 同一區(qū)域被济,版本號不同
        isThereNewUpdate = true;    // 不同INI文件救赐,升級后由升級程序直接復(fù)制my documents/config.ini
        QString tmpZone = updateZone.contains("ALL") ? "ALL" : curUserZone;
        exeFileName = tmpZone + tmpExeFileName + strUpdateVersion + ".exe";
    }
}
file.remove();

只有當(dāng)新版本大于當(dāng)前版本的版本號,且用戶已經(jīng)登錄、用戶區(qū)域符合要求時经磅,isThereNewUpdate才設(shè)置為true泌绣。根據(jù)上述信息得到下一步要下載的EXE文件名為exeFileName,然后根據(jù)這個文件名從網(wǎng)絡(luò)中下載指定升級文件预厌,開始升級阿迈。程序最后使用File.remove()將升級信息INI文件刪除,防止非法用戶竊取相關(guān)信息轧叽。

開始下載

如果獲得了指定升級文件的相關(guān)信息苗沧,那么就允許用戶單擊“升級”按鈕實現(xiàn)升級,這需要程序GUI界面的配合炭晒。如果無新版本升級信息待逞,則“升級”按鈕變灰;如果有升級版本网严,則“升級”按鈕點亮识樱,其他按鈕變灰不可用。具體代碼如下:

if(isThereNewUpdate){
    ui->lable_Update->setText("發(fā)現(xiàn)新版本震束,請單擊升級程序按鈕怜庸,否則無法正常使用該程序");
    ui->pushButton->setEnabled(false);
    ui->progressBar->hide();
}
else{
    ui->label_Update->setText("未發(fā)現(xiàn)新版本程序垢村。");
    ui->pushButtonUpdate->setEnabled(false);
    ui->progressBar->hide();

ui->label_Update->setText控制一個Label模塊割疾,用于顯示提示文字信息。ui->pushButton->setEnabled函數(shù)控制“升級”按鈕肝断,如果參數(shù)為false杈曲,則按鈕灰;如果參數(shù)為true胸懈,則按鈕點亮担扑。ui->progressBar->hide()函數(shù)隱藏進度條。因為只有單擊“升級”按鈕開始正式升級程序時進度條才亮趣钱,并通過Qt網(wǎng)絡(luò)模塊connect函數(shù)與下載升級文件的百分比關(guān)聯(lián)涌献。代碼如下:

void Dialog::on_pushButtonUpdate_clicked()
{
    // 顯示進度條
    ui->progressBar->setValue(0);
    ui->progressBar->show();
    ui->label_Update->setText("正在下載升級程序,請稍后...");
    
    // 升級程序
    UpdateByNetwork *pUpdateExeFile = new UpdateByNetwork();
    pUpdateExeFile->setBaseAddress(baseUrl);
    pUpdateExeFile->setDownloadFileName(exeFileName);
    pUpdateExeFile->setlocalUpdateDir(updateDir);
    pUpdateExeFile->startDownload();
    connect(pUpdateExeFile->pReply, QNetworkReply::downloadProgress, this, myDownloadProgress);
    while(!pUpdateExeFile->isDownloaded())
    {
        QCoreApplication::processEvents();
        //QThread::currentThread()->msleep(300);
        //QThread::currentThread()->yieldCurrentThread();  // 不放棄當(dāng)前線程首有,讓它執(zhí)行myFinished
    }

    QString exe = updateDir + "/" + exeFileName;
    QFile exeFile(exe);
    if(!exeFile.open(QIODevice::ReadOnly)){
        QMessageBox::information(this, "升級", "下載升級程序失敗燕垃,請檢查網(wǎng)絡(luò)是否連通。");
        exeFile.close();
        delete pUpdateExeFile;
        return;
    }
    else if(exeFile.size() < 4096){
        if(exeFile.size() == 0)  // 未連接網(wǎng)絡(luò)井联,下載的程序大小為0
            QMessageBox::information(this, "升級", "下載升級程序失敗卜壕,請檢查網(wǎng)絡(luò)是否連通。");
        if(exeFile.size() < 4096)  // 連接了網(wǎng)絡(luò)烙常,但下載源錯誤轴捎,下載了2KB大小的文件(內(nèi)容為tomcat錯誤代碼)
            QMessageBox::information(this, "升級", "下載升級程序失敗,下載源不存在。請聯(lián)系客服侦副。");
        exeFile.close();
        delete pUpdateExeFile;
        return;
    }
}

上述代碼中循環(huán)while(!pUpdateExeFile->isDownloaded()){ }很重要侦锯,如果沒有這段代碼,程序?qū)o法正常運行秦驯。常見的情況是升級程序還沒下載完尺碰,主程序已經(jīng)執(zhí)行完,兩者之間沒有實現(xiàn)同步译隘。

執(zhí)行上述下載代碼后亲桥,將升級EXE文件下載到指定目錄,然后判斷下載的EXE文件是否有效细燎。常用的方法是通過下載文件的大小是否為0來判斷下載是否成功两曼。但有時在tomcat服務(wù)器下載文件時皂甘,如果文件不存在玻驻,將返回一個大小約為2KB的信息文件,因此需要補充判斷else if(exeFile.size() < 4096)偿枕,其中4096可以是任意大于2KB的值璧瞬。

需要特別注意,有時根據(jù)網(wǎng)絡(luò)傳輸情況渐夸,一個EXE文件還沒有完全下載嗤锉,Qt程序就給出完成下載的信息。整個過程完全合法墓塌,沒有錯誤瘟忱,但是下載的EXE文件不完整,無法使用苫幢。這是Qt本身的bug访诱,如果要修改,則需要對封裝的下載類代碼進行補充:一種方法是利用多線程技術(shù)分塊下載韩肝,在較差的環(huán)境下仍能以較快的速度進行下載触菜;第二種方法是使用多次檢驗技術(shù),反復(fù)判斷Qt函數(shù)返回的數(shù)據(jù)是否完整哀峻、準(zhǔn)確涡相,如反復(fù)調(diào)用void myDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)函數(shù),確定獲取的下載文件字節(jié)數(shù)是否正確剩蟀。

啟動進程外EXE文件完成升級

下載的升級程序是EXE格式的可執(zhí)行文件催蝗,要完成升級還需要兩個步驟:1. 啟動這個EXE文件;2. 關(guān)閉當(dāng)前運行的應(yīng)用程序育特,等待文件覆蓋丙号,完成升級。代碼如下:

// 已下載:執(zhí)行升級程序,關(guān)閉當(dāng)前程序
qDebug() << "EXE已下載";
delete pUpdateExeFile;
ui->label_Update->setText("升級程序下載完畢槽袄。");
ui->progressBar->hide();
ui->pushButtonUpdate->setEnabled(false);
QMessageBox::information(this, "升級", "下載升級程序成功烙无,單擊確定按鈕開始升級。\n\n單擊確定按鈕后遍尺,將關(guān)閉當(dāng)前程序并啟動升級程序截酷。");
QProcess::startDetached(updateDir + "/" + exeFileName);
// QProcess::startDetached("update/xjx_jxs_setup.exe");
qApp->quit();

升級程序下載完成后,首先有一些附加的工作需要處理乾戏,如隱藏升級進度條迂苛、Label字段顯示提示信息等;然后會顯示一個對話框鼓择,該對話框不僅是提示用戶程序下載完畢三幻,還可以使用戶單擊后,執(zhí)行程序外的EXE升級文件以完成升級呐能。

執(zhí)行進程外的EXE進程使用Qt提供的QProcess::startDetached函數(shù)念搬,該函數(shù)只需要將要執(zhí)行的EXE文件名作為參數(shù)輸入即可完成。startDetached在執(zhí)行EXE進程后將與之解除關(guān)聯(lián)摆出,這正是本程序需要的功能朗徊。相關(guān)的函數(shù)還有QProcess::start等,可以通過參數(shù)配置使當(dāng)前應(yīng)用程序與執(zhí)行的進程外程序保持進程間通信偎漫。

最后使用qApp->quit();退出當(dāng)前應(yīng)用程序爷恳,讓升級程序覆蓋舊文件,更新新文件象踊。如果不退出當(dāng)前應(yīng)用温亲,使升級程序接管升級過程也是可行的,但存在一定隱患杯矩,如:某些文件因為被當(dāng)前應(yīng)用程序鎖住而無法被覆蓋更新栈虚,會導(dǎo)致升級失敗。這些內(nèi)容主要取決于升級程序的能力菊碟,設(shè)計良好的升級程序可提示重新啟動节芥,完成這些被鎖定文件的覆蓋更更新。

Qt程序體系的完成基于事件驅(qū)動逆害,任何一個Qt程序头镊,無論大小都有一個事件驅(qū)動的函數(shù)驅(qū)動消息傳遞、事件運行魄幕。當(dāng)我們打開任何一個Qt程序main.cpp文件都可以看到相艇,return a.exec();函數(shù)就是一個事件驅(qū)動,它驅(qū)動這個程序依序執(zhí)行纯陨。

當(dāng)我們自定義類坛芽,且這個類要執(zhí)行的是一個占用CPU較長的工作時留储,Qt主程序和這個類的子程序之間就需要有一個同步過程,即主程序要等待子程序執(zhí)行完后再執(zhí)行其他主程序代碼咙轩。這就需要我們自定義一個事件驅(qū)動函數(shù)获讳,使程序間實現(xiàn)同步,常用的為QCoreApplication::processEvents()函數(shù)活喊。實際上丐膝,這個函數(shù)一般情況下可以實現(xiàn)事件驅(qū)動和保持同步的功能,但在外面加一個循環(huán)更能保證程序的有效性钾菊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帅矗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子煞烫,更是在濱河造成了極大的恐慌浑此,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滞详,死亡現(xiàn)場離奇詭異凛俱,居然都是意外死亡,警方通過查閱死者的電腦和手機茵宪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門最冰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人稀火,你說我怎么就攤上這事《呐螅” “怎么了凰狞?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長沛慢。 經(jīng)常有香客問我赡若,道長,這世上最難降的妖魔是什么团甲? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任逾冬,我火速辦了婚禮,結(jié)果婚禮上躺苦,老公的妹妹穿的比我還像新娘身腻。我一直安慰自己,他們只是感情好匹厘,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布嘀趟。 她就那樣靜靜地躺著,像睡著了一般愈诚。 火紅的嫁衣襯著肌膚如雪她按。 梳的紋絲不亂的頭發(fā)上牛隅,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音酌泰,去河邊找鬼媒佣。 笑死,一個胖子當(dāng)著我的面吹牛陵刹,可吹牛的內(nèi)容都是我干的丈攒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼授霸,長吁一口氣:“原來是場噩夢啊……” “哼巡验!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起碘耳,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤显设,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辛辨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捕捂,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年斗搞,在試婚紗的時候發(fā)現(xiàn)自己被綠了指攒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡僻焚,死狀恐怖允悦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情虑啤,我是刑警寧澤隙弛,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站狞山,受9級特大地震影響全闷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜萍启,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一总珠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧勘纯,春花似錦局服、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至超埋,卻和暖如春搏讶,著一層夾襖步出監(jiān)牢的瞬間佳鳖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工媒惕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留系吩,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓妒蔚,卻偏偏與公主長得像穿挨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子肴盏,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容

  • .bat腳本基本命令語法 目錄 批處理的常見命令(未列舉的命令還比較多科盛,請查閱幫助信息) 1、REM 和 :: 2...
    慶慶慶慶慶閱讀 8,062評論 1 19
  • Ubuntu的發(fā)音 Ubuntu榨崩,源于非洲祖魯人和科薩人的語言,發(fā)作 oo-boon-too 的音章母。了解發(fā)音是有意...
    螢火蟲de夢閱讀 99,197評論 9 467
  • 個人學(xué)習(xí)批處理的初衷來源于實際工作母蛛;在某個迭代版本有個BS(安卓手游模擬器)大需求,從而在測試過程中就重復(fù)涉及到...
    Luckykailiu閱讀 4,702評論 0 11
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理乳怎,服務(wù)發(fā)現(xiàn)彩郊,斷路器,智...
    卡卡羅2017閱讀 134,626評論 18 139
  • 誰能不留戀春夜的氣息呢舞肆? 微涼而清新焦辅; 那遍野的草綠, 滿山的花香椿胯, 似露珠掛在純真的笑臉上。 夢中的人哪剃根, 你在...
    雪夜花開閱讀 182評論 2 9