#include "updaterdialog.h" #include "ui_updaterdialog.h" #include #include #include #include #include #include #include #include #include #include QString CHECK_URL = QStringLiteral("https://gitea.tianzd.cn/TianZD/deploy/raw/branch/master/%1/updates.json"); //QString DOWNLOAD_URL = QStringLiteral("https://update.tianzd.cn/deploy/"); QString PLATFORM = "windows"; QString APPVERSION = ""; QString APPNAME = ""; QString APPDATE = "0"; QString MODIFYCNT = "0"; namespace { /** * @brief 比较两个 x.y.z 形式的版本号。 * @param left 左侧版本号。 * @param right 右侧版本号。 * @return left < right 返回 -1,left == right 返回 0,left > right 返回 1。 * * 设计意图:统一版本比较规则,缺失段按 0 处理,非数字段也按 0 处理并保持保守行为。 * 边界条件:空版本号小于任何非空版本号;两者均空则视为相等。 */ int compareVersionString(const QString &left, const QString &right) { const QStringList leftParts = left.trimmed().split(".", Qt::SkipEmptyParts); const QStringList rightParts = right.trimmed().split(".", Qt::SkipEmptyParts); const int partCount = qMax(leftParts.count(), rightParts.count()); for(int i = 0; i < partCount; ++i) { const uint leftValue = (i < leftParts.count()) ? leftParts.at(i).toUInt() : 0U; const uint rightValue = (i < rightParts.count()) ? rightParts.at(i).toUInt() : 0U; if(leftValue < rightValue) { return -1; } if(leftValue > rightValue) { return 1; } } return 0; } /** * @brief 拼接基础 URL 和文件相对路径。 * @param baseUrl 远程基础地址,允许带或不带末尾斜杠。 * @param appName 应用名目录。 * @param fileName 文件名。 * @return 标准化后的下载 URL 字符串。 */ QString buildDownloadUrl(QString baseUrl, const QString &appName, const QString &fileName) { if(!baseUrl.endsWith('/')) { baseUrl.append('/'); } return baseUrl + appName + "/" + fileName; } } UpdaterDialog::UpdaterDialog(QWidget *parent) : QDialog(parent), ui(new Ui::UpdaterDialog) { ui->setupUi(this); this->setWindowTitle("Updater"); setUpdateButtonsEnabled(false); m_totalSize = 0; m_version = APPVERSION; ui->versionInput->setText(m_version); ui->appNameInput->setText(APPNAME); ui->show->setEnabled(0); ui->label_2->setText(""); ui->label_3->setText(""); m_pDownloader = new Downloader(this); connect(m_pDownloader, &Downloader::doShowInfo, this, [this](const QString &s) { showStatus(s); }); connect(m_pDownloader, &Downloader::onError, this, [this](QNetworkReply::NetworkError e) { const QMetaEnum metaEnum = QMetaEnum::fromType(); const QString str = QString::fromLatin1(metaEnum.valueToKey(e)); ui->show->appendPlainText(str); setUpdateButtonsEnabled(true); }); connect(m_pDownloader, &Downloader::doFinished, this, [this]() { ui->show->appendPlainText("download finished"); onDownloadFinished(); }); connect(m_pDownloader, &Downloader::doProgress, this, [this](qint64 received, qint64 total) { m_totalSize = total > 0 ? static_cast(total) : 0; if(total > 0) { ui->progressBar->setMinimum(0); ui->progressBar->setMaximum(100); ui->progressBar->setValue(static_cast((received * 100) / total)); calculateSizes(received, total); calculateTimeRemaining(received, total); } else { ui->progressBar->setMinimum(0); ui->progressBar->setMaximum(0); ui->progressBar->setValue(-1); ui->label_2->setText(tr("Downloading updates")); } }); } UpdaterDialog::~UpdaterDialog() { delete m_UpdateProc; delete ui; } void UpdaterDialog::showStatus(const QString &s) { ui->show->appendPlainText(s); } void UpdaterDialog::on_btn_check_clicked() { APPNAME = ui->appNameInput->text().trimmed(); if(APPNAME.isEmpty()) { ui->show->appendPlainText("Please input app name"); return; } ui->show->clear(); setUpdateButtonsEnabled(false); m_version = ui->versionInput->text().trimmed(); if(!m_pManager) { m_pManager = new QNetworkAccessManager(this); connect(m_pManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(onCheckReply(QNetworkReply *))); } const QString url = CHECK_URL.arg(APPNAME); QNetworkRequest request{QUrl(url)}; m_pManager->get(request); } bool UpdaterDialog::checkVersion(const QString &_localVersion, const QString &_remoteVersion) { // 本地版本为空或为 0 时视为首次安装/未知版本,优先下载完整包降低补丁失败风险。 if(_localVersion.trimmed().isEmpty() || _localVersion.trimmed() == "0") { m_bDownloadFullExe = true; return true; } const int versionCompareResult = compareVersionString(_localVersion, _remoteVersion); if(versionCompareResult > 0) { return false; } bool isNotLatest = versionCompareResult < 0; if(!isNotLatest) { // 版本号一致时再比较构建日期和修改计数,兼容同版本热修复包。 if(m_lastChangeTime.toUInt() > APPDATE.toUInt()) { isNotLatest = true; } if(!m_modifyCnt.trimmed().isEmpty() && !MODIFYCNT.trimmed().isEmpty()) { if(m_modifyCnt.toUInt() > MODIFYCNT.toUInt()) { isNotLatest = true; } } } return isNotLatest; } void UpdaterDialog::onCheckReply(QNetworkReply *reply) { reply->deleteLater(); if(reply->error() != QNetworkReply::NoError) { showStatus("check error: " + reply->errorString()); return; } const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); if(document.isNull() || !document.isObject()) { showStatus("reply doc is null"); return; } const QJsonObject updates = document.object().value("updates").toObject(); const QJsonObject platform = updates.value(PLATFORM).toObject(); if(platform.isEmpty()) { showStatus(tr("no update information for platform: %1").arg(PLATFORM)); return; } m_changelog = platform.value("changelog").toString(); m_patchVersion = platform.value("patch-version").toString(); m_fullVersion = platform.value("full-version").toString(); m_lastChangeTime = platform.value("modify-time").toString(); m_modifyCnt = platform.value("modifyCnt").toString(); m_bDownloadFullExe = platform.value("download-full").toBool(); m_patchBaseUrl = platform.value("patch-url").toString(); m_fullBaseUrl = platform.value("full-url").toString(); if(m_version.isEmpty() || m_version == "0") { m_bDownloadFullExe = true; } rebuildDownloadInfo(); const QString remoteVersion = m_bDownloadFullExe ? m_fullVersion : m_patchVersion; const bool isNotLatest = checkVersion(m_version, remoteVersion); if(isNotLatest) { showStatus("======================================" + tr("\ncurrent version: v") + m_version + tr("\nmodify time: ") + APPDATE + tr("\nmodify cnt: ") + MODIFYCNT + "\n ------------------------------" + tr("\nlatest version: v") + remoteVersion + tr("\nmodify time: ") + m_lastChangeTime + tr("\nmodify cnt: ") + m_modifyCnt + tr("\nchange log: ") + m_changelog + "\n======================================"); if(m_bDownloadFullExe) { showStatus("need download full exe"); } showStatus(tr("click update button to update")); setUpdateButtonsEnabled(true); } else { showStatus("======================================" + tr("\ncurrent software is up-to-date") + tr("\nversion : v") + remoteVersion + tr("\nmodify time: ") + m_lastChangeTime + tr("\nmodify cnt: ") + m_modifyCnt + "\n======================================" + tr("\nno need to update") + tr("\nclick cancel button to quit")); setUpdateButtonsEnabled(false); } } void UpdaterDialog::calculateSizes(qint64 received, qint64 total) { const QString totalSize = formatBytes(total); const QString receivedSize = formatBytes(received); ui->label_2->setText(tr("Downloading updates") + " (" + receivedSize + " " + tr("of") + " " + totalSize + ")"); } void UpdaterDialog::calculateTimeRemaining(qint64 received, qint64 total) { if(total <= 0 || received <= 0) { return; } const qint64 currentMs = m_downloadTimer.elapsed(); const qint64 elapsedSinceLast = currentMs - m_lastProgressMs; double speed = 0.0; if(elapsedSinceLast > 0) { speed = (received - m_lastProgressBytes) / 1024.0 / elapsedSinceLast * 1000.0; } m_lastProgressMs = currentMs; m_lastProgressBytes = received; const qint64 elapsedSeconds = qMax(1, currentMs / 1000); qreal timeRemaining = (total - received) / (received / static_cast(elapsedSeconds)); QString timeString; if(timeRemaining > 7200) { timeRemaining /= 3600; const int hours = int(timeRemaining + 0.5); timeString = hours > 1 ? tr("about %1 hours").arg(hours) : tr("about one hour"); } else if(timeRemaining > 60) { timeRemaining /= 60; const int minutes = int(timeRemaining + 0.5); timeString = minutes > 1 ? tr("%1 minutes").arg(minutes) : tr("1 minute"); } else { const int seconds = int(timeRemaining + 0.5); timeString = seconds > 1 ? tr("%1 seconds").arg(seconds) : tr("1 second"); } ui->label_3->setText(tr("speed: %1 kb/s ").arg(speed, 0, 'f', 1) + tr("Time remaining") + ": " + timeString); } void UpdaterDialog::on_btn_update_clicked() { rebuildDownloadInfo(); m_pDownloader->setFileName(m_fileName); m_downloadTimer.restart(); m_lastProgressMs = 0; m_lastProgressBytes = 0; const QString url = m_bDownloadFullExe ? m_fullUrl : m_patchUrl; m_pDownloader->startDownload(url); } void UpdaterDialog::on_btn_cancel_clicked() { if(restartApplication()) { QApplication::quit(); } } void UpdaterDialog::onDownloadFinished() { const qreal totalSeconds = qMax(0.001, m_downloadTimer.elapsed() / 1000.0); if(m_totalSize > 0) { ui->label_3->setText("total time: " + QString::number(totalSeconds, 'f', 1) + "s average speed: " + QString::number((m_totalSize / 1024.0 / totalSeconds), 'g', 3) + "kb/s"); } const auto localInformation = QMessageBox::question(this, "update", "download finished\nstart update?"); if(localInformation != QMessageBox::Yes) { return; } QString appName; if(!m_bDownloadFullExe) { // 补丁包下载完成后在应用目录解压;解压失败时保留更新器,避免退出后主程序缺文件。 QFileInfo fileInfo(m_pDownloader->getFile()); QZipReader zipReader(fileInfo.absoluteFilePath()); if(!zipReader.extractAll(fileInfo.absolutePath())) { showStatus("extract patch failed"); return; } appName = APPNAME + ".exe"; } else { appName = m_pDownloader->fileName(); } const QFileInfo appFile(QCoreApplication::applicationDirPath() + "/" + appName); if(appFile.exists()) { QDesktopServices::openUrl(QUrl::fromLocalFile(appFile.absoluteFilePath())); QApplication::quit(); } else { showStatus("update target file not exists: " + appFile.absoluteFilePath()); } } void UpdaterDialog::on_btn_update_patch_clicked() { m_bDownloadFullExe = false; rebuildDownloadInfo(); m_pDownloader->setFileName(m_fileName); m_downloadTimer.restart(); m_lastProgressMs = 0; m_lastProgressBytes = 0; m_pDownloader->startDownload(m_patchUrl); } void UpdaterDialog::setUpdateButtonsEnabled(bool enabled) { ui->btn_update->setEnabled(enabled); ui->btn_update_patch->setEnabled(enabled && !m_patchUrl.isEmpty()); } void UpdaterDialog::rebuildDownloadInfo() { const QString patchFileName = APPNAME + "-patch-v" + m_patchVersion + ".zip"; const QString fullFileName = APPNAME + "-full-v" + m_fullVersion + ".exe"; m_patchUrl = buildDownloadUrl(m_patchBaseUrl, APPNAME, patchFileName); m_fullUrl = buildDownloadUrl(m_fullBaseUrl, APPNAME, fullFileName); m_fileName = m_bDownloadFullExe ? fullFileName : patchFileName; } QString UpdaterDialog::formatBytes(qint64 bytes) const { if(bytes < 0) { return tr("unknown"); } if(bytes < 1024) { return tr("%1 bytes").arg(bytes); } if(bytes < 1048576) { return tr("%1 KB").arg(QString::number(bytes / 1024.0, 'f', 1)); } return tr("%1 MB").arg(QString::number(bytes / 1048576.0, 'f', 2)); } bool UpdaterDialog::restartApplication() { if(APPNAME.trimmed().isEmpty()) { showStatus("app name is empty"); return false; } QString program; #if defined Q_OS_WIN program = APPNAME + ".exe"; #elif defined Q_OS_LINUX program = "./" + APPNAME; #else program = APPNAME; #endif // 取消更新后需要让原程序独立运行,使用 startDetached 避免更新器退出时影响子进程生命周期。 if(!QProcess::startDetached(program, QStringList())) { showStatus("restart app failed"); return false; } return true; }