From: snickersun Date: Tue, 21 Apr 2026 12:41:05 +0000 (+0800) Subject: 添加日志上传服务器 X-Git-Url: http://git.snicker.com.cn/Users/apple/Desktop/static/gitweb.css?a=commitdiff_plain;h=3f04c60e130a5c6895d61ec4c9b3e3ec5fafe0c4;p=mirrors%2Ffactory_tool.git 添加日志上传服务器 添加mic,4g信号测试通过值 日志按天存储,相同设备可以重复测试。 --- diff --git a/.gitignore b/.gitignore index 1013540..6c19ed9 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ compile_commands.json *creator.user* *_qmlcache.qrc + +build_release/ +build_debug/ diff --git a/config_manager.cpp b/config_manager.cpp index 1b9131a..c49910f 100644 --- a/config_manager.cpp +++ b/config_manager.cpp @@ -63,6 +63,7 @@ void ConfigManager::setDefaultConfig() testConfig["expectedVer"] = ""; testConfig["bluetoothAddress"] = ""; testConfig["bluetoothRSSI"] = ""; + testConfig["cat1MinSignal"] = ""; defaultConfig["testConfig"] = testConfig; // 默认测试项checkbox配置(默认全部选中) diff --git a/deploy_qt.bat b/deploy_qt.bat new file mode 100644 index 0000000..128b290 --- /dev/null +++ b/deploy_qt.bat @@ -0,0 +1,125 @@ +@echo off +setlocal EnableDelayedExpansion +set "ROOT=%~1" +set "APP=%~2" +set "WD=%~3" +set "EXTRA=%~4" +set "QTBIN=%~5" +set "OSSL_TOOLS=%~6" +set "SSL_ARCH=%~7" +set "PROJ_LIBS=%~8" +if "%EXTRA%"=="__NO_EXTRA__" set "EXTRA=" +if "%SSL_ARCH%"=="" set "SSL_ARCH=x64" +echo [deploy_qt] args: EXE_ROOT=%ROOT% TARGET=%APP% SSL_ARCH=%SSL_ARCH% PROJ_LIBS=%PROJ_LIBS% +if "%WD%"=="" ( + echo [deploy_qt] windeployqt path empty, skip. + exit /b 0 +) + +set "EXE_PATH=" +set "EXE_DIR=" +set "WDFLAG=" + +if exist "%ROOT%\release\%APP%.exe" ( + set "EXE_PATH=%ROOT%\release\%APP%.exe" + set "EXE_DIR=%ROOT%\release" + set "WDFLAG=--release" + goto :do_deploy +) +if exist "%ROOT%\debug\%APP%.exe" ( + set "EXE_PATH=%ROOT%\debug\%APP%.exe" + set "EXE_DIR=%ROOT%\debug" + set "WDFLAG=--debug" + goto :do_deploy +) +if exist "%ROOT%\%APP%.exe" ( + set "EXE_PATH=%ROOT%\%APP%.exe" + set "EXE_DIR=%ROOT%" + set "WDFLAG=--release" + echo %ROOT% | findstr /i "build_debug" >nul + if not errorlevel 1 set "WDFLAG=--debug" + goto :do_deploy +) + +echo [deploy_qt] skip: %APP%.exe not found under "%ROOT%" +exit /b 0 + +:do_deploy +echo [deploy_qt] windeployqt "%EXE_PATH%" +echo [deploy_qt] OpenSSL 部署位数: %SSL_ARCH% (与当前编译目标 QT_ARCH 一致) +"%WD%" %EXTRA% %WDFLAG% --no-translations "%EXE_PATH%" +if errorlevel 1 ( + echo [deploy_qt] windeployqt returned error +) + +REM OpenSSL:先 Qt bin,再 Tools\OpenSSL(路径已由 qmake 按 i386/x86_64 选 Win_x86 或 Win_x64) +call :copy_openssl "%QTBIN%" "%EXE_DIR%" "Qt bin" "%SSL_ARCH%" +if not "%OSSL_TOOLS%"=="" ( + call :copy_openssl "%OSSL_TOOLS%" "%EXE_DIR%" "Tools\\OpenSSL" "%SSL_ARCH%" +) +REM 可选:环境变量 OPENSSL_ROOT 指向 OpenSSL-Win32/Win64 安装根目录(含 bin\libssl*.dll) +if defined OPENSSL_ROOT if exist "%OPENSSL_ROOT%\bin\." ( + call :copy_openssl "%OPENSSL_ROOT%\bin" "%EXE_DIR%" "OPENSSL_ROOT" "%SSL_ARCH%" +) +REM 工程 libs 下全部文件(含子目录)复制到 exe 输出目录;后执行,可覆盖前述 OpenSSL 同名文件 +if not "%PROJ_LIBS%"=="" if exist "%PROJ_LIBS%\." ( + call :copy_proj_libs_tree "%PROJ_LIBS%" "%EXE_DIR%" +) +exit /b 0 + +:copy_openssl +set "SRC=%~1" +set "DST=%~2" +set "LABEL=%~3" +set "SSL_ARCH=%~4" +if "%SSL_ARCH%"=="" set "SSL_ARCH=x64" +if "%SRC%"=="" exit /b 0 +if not exist "%SRC%\." exit /b 0 + +set "FOUND=0" +for %%F in ("%SRC%\libssl*.dll") do if exist "%%F" set "FOUND=1" +for %%F in ("%SRC%\libcrypto*.dll") do if exist "%%F" set "FOUND=1" +if "!FOUND!"=="0" ( + echo [deploy_qt] OpenSSL: %LABEL% 在 "%SRC%" 无 libssl*.dll 且无 libcrypto*.dll,跳过 + exit /b 0 +) + +echo [deploy_qt] OpenSSL: 从 %LABEL% 复制到 "%DST%" (仅 %SSL_ARCH%) +for %%F in ("%SRC%\libssl*.dll") do call :copy_ssl_if_arch "%%F" "%DST%" "%SSL_ARCH%" +for %%F in ("%SRC%\libcrypto*.dll") do call :copy_ssl_if_arch "%%F" "%DST%" "%SSL_ARCH%" +exit /b 0 + +REM 将工程 libs 整棵目录复制到输出目录(robocopy 退出码 0~7 视为成功) +:copy_proj_libs_tree +set "LSRC=%~1" +set "LDST=%~2" +if "%LSRC%"=="" exit /b 0 +if not exist "%LSRC%\." exit /b 0 +echo [deploy_qt] 复制 project\libs 全部内容到 "%LDST%" +robocopy "%LSRC%" "%LDST%" /E /IS /R:1 /W:1 /NFL /NDL /NJH /NJS /NC /NS /NP +set "RC=!ERRORLEVEL!" +if !RC! GEQ 8 ( + echo [deploy_qt] robocopy libs 警告,退出码 !RC! +) +exit /b 0 + +REM x86:文件名不得含 x64;x64:文件名须含 x64(与官方 Windows OpenSSL 命名一致) +:copy_ssl_if_arch +set "F=%~1" +set "DST=%~2" +set "A=%~3" +echo %~nx1 | findstr /i "x64" >nul +if "%A%"=="x86" ( + if errorlevel 1 ( + echo + %~nx1 + copy /y "%F" "%DST%\" >nul + ) + exit /b 0 +) +if "%A%"=="x64" ( + if not errorlevel 1 ( + echo + %~nx1 + copy /y "%F" "%DST%\" >nul + ) +) +exit /b 0 diff --git a/dmp_log_uploader.cpp b/dmp_log_uploader.cpp new file mode 100644 index 0000000..0ba4c00 --- /dev/null +++ b/dmp_log_uploader.cpp @@ -0,0 +1,179 @@ +#include "dmp_log_uploader.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +const char *const kDmpUploadUrl = + "https://dmp-dev-global.raisound.com/dmp-api/log/log-add" + "?dmpSecretKey=df4d7e24-31b6-4888-a5a6-ae8b1612c8e5"; + +void appendLineToLogFile(const QString &absolutePath, const QString &line) +{ + if (absolutePath.isEmpty()) + return; + QDir().mkpath(QFileInfo(absolutePath).absolutePath()); + + QFile f(absolutePath); + if (!f.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { + qDebug() << "DMP 上传日志写入失败:" << absolutePath << f.errorString(); + return; + } + const QString row = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz")) + + QLatin1Char(' ') + line + QLatin1Char('\n'); + f.write(row.toUtf8()); +} + +} // namespace + +QStringList DmpLogUploader::csvColumnNames() +{ + return QStringList{ + QStringLiteral("开始时间"), + QStringLiteral("结束时间"), + QStringLiteral("测试耗时"), + QStringLiteral("软件版本"), + QStringLiteral("IMEI"), + QStringLiteral("CGSN_IMEI"), + QStringLiteral("ICCID"), + QStringLiteral("Modem版本"), + QStringLiteral("PYVER版本"), + QStringLiteral("LCD测试"), + QStringLiteral("马达测试"), + QStringLiteral("按键测试"), + QStringLiteral("录音测试"), + QStringLiteral("电池测试"), + QStringLiteral("G-sensor测试"), + QStringLiteral("蓝牙测试"), + QStringLiteral("4G信号"), + QStringLiteral("Ping测试"), + QStringLiteral("4G平均信号强度"), + QStringLiteral("录音MIC采样依据"), + QStringLiteral("整体结果"), + }; +} + +QString DmpLogUploader::resolveDevid(const QString &imei, const QString &cgsnImei, const QString &iccid) +{ + const QRegularExpression digits15(QStringLiteral("^\\d{15}$")); + if (digits15.match(cgsnImei.trimmed()).hasMatch()) + return cgsnImei.trimmed(); + if (digits15.match(imei.trimmed()).hasMatch()) + return imei.trimmed(); + const QString ic = iccid.trimmed(); + if (!ic.isEmpty()) + return ic; + if (!cgsnImei.isEmpty()) + return cgsnImei.trimmed(); + if (!imei.isEmpty()) + return imei.trimmed(); + return QStringLiteral("unknown"); +} + +QString DmpLogUploader::logFieldFromOverall(const QString &overallText) +{ + const QString t = overallText.trimmed(); + if (t == QStringLiteral("通过")) + return QStringLiteral("success"); + if (t.contains(QStringLiteral("未通过")) || t.contains(QStringLiteral("失败"))) + return QStringLiteral("fail"); + return t; +} + +void DmpLogUploader::appendUploadLog(const QString &line) +{ + appendLineToLogFile(m_uploadLogFilePath, line); +} + +DmpLogUploader::DmpLogUploader(QObject *parent) + : QObject(parent) + , m_nam(new QNetworkAccessManager(this)) +{ + if (!QSslSocket::supportsSsl()) { + qWarning() << "[DMP] supportsSsl==false,HTTPS 可能失败;请将与本程序位数一致的 libssl/libcrypto 置于 exe 同目录" + << (QT_POINTER_SIZE == 4 ? QStringLiteral("(32 位为 libssl-1_1.dll 等,非 x64 后缀)") + : QStringLiteral("(64 位常见为 *-x64.dll)")); + } else { + qDebug() << "[DMP] TLS 已就绪,SSL 构建版本:" << QSslSocket::sslLibraryBuildVersionString() + << "运行时:" << QSslSocket::sslLibraryVersionString(); + } +} + +void DmpLogUploader::uploadProductionRecord(const QStringList &fields, + const QString &imei, + const QString &cgsnImei, + const QString &iccid, + const QString &logFilePath) +{ + m_uploadLogFilePath = logFilePath; + + const QStringList cols = csvColumnNames(); + if (fields.size() != cols.size()) { + const QString msg = QStringLiteral("uploadProductionRecord: 字段数量不匹配 expect=%1 got=%2") + .arg(cols.size()) + .arg(fields.size()); + qDebug() << msg; + appendUploadLog(msg); + return; + } + + QJsonObject root; + root.insert(QStringLiteral("product"), QStringLiteral("ln")); + + const QString devid = resolveDevid(imei, cgsnImei, iccid); + root.insert(QStringLiteral("devid"), devid); + + const QString overall = fields.value(cols.size() - 1); + root.insert(QStringLiteral("log"), logFieldFromOverall(overall)); + + for (int i = 0; i < cols.size(); ++i) + root.insert(cols.at(i), fields.at(i)); + + const QJsonDocument doc(root); + const QByteArray body = doc.toJson(QJsonDocument::Compact); + + const QUrl url(QString::fromUtf8(kDmpUploadUrl)); + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); + req.setRawHeader("Accept", "*/*"); + + const QString bodyStr = QString::fromUtf8(body); + appendUploadLog(QStringLiteral("--- DMP POST ---")); + appendUploadLog(QStringLiteral("Header Content-Type: application/json")); + appendUploadLog(QStringLiteral("Header Accept: */*")); + appendUploadLog(QStringLiteral("Body(JSON): %1").arg(bodyStr)); + qDebug() << "[DMP upload] Body:" << bodyStr; + + const QString logPathForReply = m_uploadLogFilePath; + QNetworkReply *reply = m_nam->post(req, body); + connect(reply, &QNetworkReply::finished, this, [this, reply, logPathForReply]() { + const QVariant codeVar = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + const int httpCode = codeVar.isValid() ? codeVar.toInt() : -1; + const QString err = reply->error() != QNetworkReply::NoError ? reply->errorString() : QString(); + const QByteArray resp = reply->readAll(); + const QString respStr = QString::fromUtf8(resp); + const QString line = QStringLiteral("Response HTTP=%1 error=%2 body=%3") + .arg(httpCode) + .arg(err.isEmpty() ? QStringLiteral("(none)") : err) + .arg(respStr); + appendLineToLogFile(logPathForReply, line); + qDebug() << "[DMP upload] Response HTTP:" << httpCode + << "error:" << (err.isEmpty() ? QStringLiteral("none") : err) + << "body:" << respStr; + reply->deleteLater(); + }); +} diff --git a/dmp_log_uploader.h b/dmp_log_uploader.h new file mode 100644 index 0000000..1852e00 --- /dev/null +++ b/dmp_log_uploader.h @@ -0,0 +1,40 @@ +#ifndef DMP_LOG_UPLOADER_H +#define DMP_LOG_UPLOADER_H + +#include +#include + +class QNetworkAccessManager; + +/** DMP 产测记录 HTTPS POST;上传过程写入产测记录/logs 下与主会话日志同名的 *_dmp_upload.log */ +class DmpLogUploader : public QObject +{ + Q_OBJECT +public: + explicit DmpLogUploader(QObject *parent = nullptr); + + /** 与 saveTestRecord 中 CSV 表头列名、顺序一致(21 列) */ + static QStringList csvColumnNames(); + + /** + * @param fields 与 csvColumnNames() 顺序一致的一行数据 + * @param imei / cgsnImei / iccid 用于生成 devid(优先 15 位 IMEI) + * @param logFilePath 本次上传对应的本地日志绝对路径(如 .../logs/yyyyMMdd_hhmmss_IMEI_ICCID_dmp_upload.log) + */ + void uploadProductionRecord(const QStringList &fields, + const QString &imei, + const QString &cgsnImei, + const QString &iccid, + const QString &logFilePath); + +private: + void appendUploadLog(const QString &line); + static QString resolveDevid(const QString &imei, const QString &cgsnImei, const QString &iccid); + static QString logFieldFromOverall(const QString &overallText); + + QNetworkAccessManager *m_nam; + /** 当前一次 upload 写文件用;与主会话 log 同目录命名 */ + QString m_uploadLogFilePath; +}; + +#endif // DMP_LOG_UPLOADER_H diff --git a/factory_tool.pro b/factory_tool.pro index 26dac3a..8358ac9 100644 --- a/factory_tool.pro +++ b/factory_tool.pro @@ -1,10 +1,27 @@ -QT += core gui serialport +QT += core gui serialport network +# HTTPS 与 HTTP 同属 Qt Network,无需额外 QT 模块。 +# Windows 若 TLS initialization failed:将 libssl/libcrypto(与 Qt 同位数)复制到 exe 同目录,或对 exe 运行 windeployqt;Qt6 可部署 plugins/tls/qschannelbackend.dll 使用系统 Schannel。 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = LeiNiaoFactoryTool TEMPLATE = app +# 统一输出根目录 build_debug / build_release,其下仍使用与 Qt 默认一致的 debug、release、moc、ui 命名 +CONFIG(debug, debug|release) { + BUILD_SUBDIR = $$_PRO_FILE_PWD_/build_debug + DESTDIR = $$BUILD_SUBDIR/debug + OBJECTS_DIR = $$BUILD_SUBDIR/debug + MOC_DIR = $$BUILD_SUBDIR/debug/moc + UI_DIR = $$BUILD_SUBDIR/debug/ui +} else { + BUILD_SUBDIR = $$_PRO_FILE_PWD_/build_release + DESTDIR = $$BUILD_SUBDIR/release + OBJECTS_DIR = $$BUILD_SUBDIR/release + MOC_DIR = $$BUILD_SUBDIR/release/moc + UI_DIR = $$BUILD_SUBDIR/release/ui +} + # 设置编码为UTF-8 QMAKE_CXXFLAGS += -finput-charset=UTF-8 -fexec-charset=UTF-8 CONFIG += utf8_source @@ -15,6 +32,7 @@ CODECFORSRC = UTF-8 SOURCES += \ main.cpp \ mainwindow.cpp \ + dmp_log_uploader.cpp \ app_debug_log.cpp \ config_manager.cpp \ qrcode_barcode_generator.cpp \ @@ -23,6 +41,7 @@ SOURCES += \ # 头文件列表 HEADERS += \ mainwindow.h \ + dmp_log_uploader.h \ app_debug_log.h \ config_manager.h \ qrcode_barcode_generator.h \ @@ -32,6 +51,25 @@ HEADERS += \ FORMS += \ mainwindow.ui -# 确保 MOC 正确处理 Qt 元对象系统 -MOC_DIR += moc -UI_DIR += ui # 指定 UI 生成文件的输出目录 +# ---------- 编译后自动部署 Qt 依赖(Windows:windeployqt + OpenSSL + 工程 libs 全量复制)---------- +# deploy_qt.bat:windeployqt;再从 Qt bin、Tools\\OpenSSL 复制 SSL;最后将工程 libs\\ 下全部文件拷到 exe 同目录。 +win32:equals(TEMPLATE, app) { + PRO_DIR = $$_PRO_FILE_PWD_ + PROJECT_LIBS_SSL = $$PRO_DIR/libs + WINDEPLOYQT = $$[QT_INSTALL_BINS]/windeployqt.exe + QT_BIN_DIR = $$[QT_INSTALL_BINS] + equals(QT_ARCH, i386) { + QT_TOOLS_OPENSSL = $$[QT_INSTALL_PREFIX]/../../Tools/OpenSSL/Win_x86/bin + SSL_DEPLOY_ARCH = x86 + } else { + QT_TOOLS_OPENSSL = $$[QT_INSTALL_PREFIX]/../../Tools/OpenSSL/Win_x64/bin + SSL_DEPLOY_ARCH = x64 + } + msvc { + WIN_DEPLOY_EXTRA = --compiler-runtime + } else { + # MinGW 等:勿留空,否则 nmake/mingw32-make 下 QMAKE_POST_LINK 参数易错位,导致 SSL_ARCH 未传到 deploy_qt.bat + WIN_DEPLOY_EXTRA = __NO_EXTRA__ + } + QMAKE_POST_LINK += $$quote($$shell_path($$PRO_DIR/deploy_qt.bat)) $$quote($$shell_path($$BUILD_SUBDIR)) $$quote($$TARGET) $$quote($$shell_path($$WINDEPLOYQT)) $$quote($$WIN_DEPLOY_EXTRA) $$quote($$shell_path($$QT_BIN_DIR)) $$quote($$shell_path($$QT_TOOLS_OPENSSL)) $$quote($$SSL_DEPLOY_ARCH) $$quote($$shell_path($$PROJECT_LIBS_SSL)) $$escape_expand(\n\t) +} diff --git a/libs/libcrypto-1_1-x64.dll b/libs/libcrypto-1_1-x64.dll new file mode 100644 index 0000000..0568962 Binary files /dev/null and b/libs/libcrypto-1_1-x64.dll differ diff --git a/libs/libcrypto-1_1.dll b/libs/libcrypto-1_1.dll new file mode 100644 index 0000000..bdabe3f Binary files /dev/null and b/libs/libcrypto-1_1.dll differ diff --git a/libs/libssl-1_1-x64.dll b/libs/libssl-1_1-x64.dll new file mode 100644 index 0000000..7934d86 Binary files /dev/null and b/libs/libssl-1_1-x64.dll differ diff --git a/libs/libssl-1_1.dll b/libs/libssl-1_1.dll new file mode 100644 index 0000000..5c87ce4 Binary files /dev/null and b/libs/libssl-1_1.dll differ diff --git a/main.cpp b/main.cpp index 58b01c8..284c670 100644 --- a/main.cpp +++ b/main.cpp @@ -2,17 +2,195 @@ #include "app_debug_log.h" #include +#include #include +#include +#include +#ifdef Q_OS_WIN +#include +#include +#include +#include +#include +#include +#endif #ifdef _WIN32 #include #include #include #endif +#ifdef Q_OS_WIN +static QString tlsFormatWin32Message(DWORD err) +{ + if (err == 0) + return QString(); + wchar_t *buf = nullptr; + const DWORD n = FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&buf), 0, nullptr); + QString msg; + if (n && buf) { + msg = QString::fromWCharArray(buf).trimmed(); + LocalFree(buf); + } + return msg; +} + +static void printTlsWin32Error(const char *context, DWORD err) +{ + const QString sysMsg = tlsFormatWin32Message(err); + if (err == 0) + qWarning() << "[TLS]" << context << "GetLastError: 0(可能被 Qt 内部调用覆盖,以下 LoadLibraryW 探测为准)"; + else + qWarning() << "[TLS]" << context << "GetLastError:" << err << sysMsg; +} + +/** 列出 applicationDirPath 下 Qt 能枚举到的 *.dll,用于核对“是否真在同一目录、文件名是否一致”。 */ +static void printTlsExeDirDllInventory(const QString &appDir) +{ + const QString exeDir = QDir::cleanPath(appDir); + QDir d(exeDir); + qWarning() << "[TLS] applicationDirPath:" << exeDir << "QDir.exists:" << d.exists(); + const QStringList names = d.entryList(QStringList() << QStringLiteral("*.dll"), QDir::Files, QDir::Name); + if (names.isEmpty()) + qWarning() << "[TLS] 本目录下 Qt 枚举 *.dll: 无。若资源管理器里有:请确认是否**同一文件夹**、是否**云盘仅联机占位**未落盘、是否改的是别的 build 目录下的 debug。"; + else + qWarning() << "[TLS] 本目录下 *.dll 共" << names.size() << "个:" << names.join(QStringLiteral(", ")); + for (const QString &n : names) { + if (n.contains(QLatin1String("ssl"), Qt::CaseInsensitive) + || n.contains(QLatin1String("crypto"), Qt::CaseInsensitive)) { + const QFileInfo x(d.absoluteFilePath(n)); + qWarning() << "[TLS] (含 ssl/crypto)" << n << "size:" << x.size(); + } + } +} + +static void printTlsDllLoadSearchPaths(const QString &dllBaseName) +{ + const QString dllFile = dllBaseName + QStringLiteral(".dll"); + qWarning() << "[TLS] LoadLibrary(" << dllFile << ") 失败时的典型搜索顺序与每个目录下的完整路径:"; + + QStringList paths; + paths << QCoreApplication::applicationDirPath(); + paths << QDir::currentPath(); + + wchar_t wbuf[MAX_PATH]; + if (GetSystemDirectoryW(wbuf, MAX_PATH)) + paths << QString::fromWCharArray(wbuf); + if (GetWindowsDirectoryW(wbuf, MAX_PATH)) + paths << QString::fromWCharArray(wbuf); + if (GetDllDirectoryW(MAX_PATH, wbuf) > 0u) + paths << QString::fromWCharArray(wbuf); + + const QString pathEnv = qEnvironmentVariable("PATH"); + const QStringList pathParts = pathEnv.split(QLatin1Char(';'), QString::SkipEmptyParts); + for (const QString &p : pathParts) + paths << p.trimmed(); + + qWarning() << "[TLS] exe:" << QCoreApplication::applicationFilePath(); + qWarning() << "[TLS] PATH 环境变量(原始):" << pathEnv; + + int idx = 1; + QStringList seen; + for (const QString &dir : paths) { + if (dir.isEmpty()) + continue; + const QString norm = QDir::cleanPath(dir); + if (seen.contains(norm)) + continue; + seen << norm; + const QString full = QDir::cleanPath(norm + QLatin1Char('/') + dllFile); + const bool exists = QFileInfo::exists(full); + qWarning() << "[TLS] " << idx++ << norm << "->" << full << (exists ? QLatin1String("[存在]") : QLatin1String("[无]")); + } +} + +/** exe 同目录下若已有该 DLL,用完整路径 LoadLibrary,区分“文件缺失”与“子依赖缺失”(126 常为后者)。 */ +static void printTlsLoadDllProbe(const QString &appDir, const QString &dllBaseName) +{ + const QString dllFile = dllBaseName + QStringLiteral(".dll"); + const QString fullPath = QDir::cleanPath(appDir + QLatin1Char('/') + dllFile); + const QFileInfo fi(fullPath); + const QString nativeFull = QDir::toNativeSeparators(fullPath); + qWarning() << "[TLS] 探测" << dllFile << "完整路径:" << nativeFull << "QFileInfo::exists:" << fi.exists() + << "QFile::exists:" << QFile::exists(fullPath); + if (!fi.exists()) { + const std::wstring wnative = nativeFull.toStdWString(); + const DWORD attr = GetFileAttributesW(wnative.c_str()); + if (attr == INVALID_FILE_ATTRIBUTES) { + qWarning() << "[TLS] GetFileAttributesW 失败 LastError:" << GetLastError() + << "(若仍为“文件在的”,多为路径不是 exe 同目录,或云盘/权限导致 API 不可见)"; + } else { + qWarning() << "[TLS] GetFileAttributesW 成功 attrs=0x" << QString::number(attr, 16) + << "(与 QFileInfo 不一致时请反馈 Qt/路径问题)"; + } + return; + } + + const std::wstring wpath = QDir::toNativeSeparators(fullPath).toStdWString(); + HMODULE h = LoadLibraryW(wpath.c_str()); + if (h) { + qWarning() << "[TLS] LoadLibraryW(完整路径) 成功;若 QLibrary(短名) 仍失败,多为搜索路径/工作目录与 exe 目录不一致。"; + FreeLibrary(h); + return; + } + const DWORD err = GetLastError(); + printTlsWin32Error("LoadLibraryW(完整路径)", err); + if (err == 126u) + qWarning() << "[TLS] 错误 126:多为该 DLL 的“子依赖”未找到(不是主文件名错)。" + "MinGW 构建的 OpenSSL 常需同目录另有:libgcc_s_seh-1.dll、libwinpthread-1.dll、" + "libstdc++-6.dll(从本机 Qt 的 Tools\\mingw730_64\\bin 或 mingw73_64\\bin 复制);" + "MSVC 构建的 OpenSSL 则需 VCRUNTIME140.dll、api-ms-win-crt-*.dll(装 VC++ 运行库)。" + "可用 dumpbin /dependents xxx.dll 或 Dependencies 工具查看完整依赖树。"; +} +#endif + int main(int argc, char *argv[]) { QApplication a(argc, argv); + // HTTPS 依赖 Qt Network 在运行时加载 TLS 后端;Windows 下若缺 OpenSSL DLL,会出现 TLS initialization failed + qDebug() << "[TLS] supportsSsl:" << QSslSocket::supportsSsl() + << "build:" << QSslSocket::sslLibraryBuildVersionString() + << "runtime:" << QSslSocket::sslLibraryVersionString(); +#ifdef Q_OS_WIN + if (!QSslSocket::supportsSsl()) { + const QString appDir = QCoreApplication::applicationDirPath(); + printTlsExeDirDllInventory(appDir); + const bool is32 = (QT_POINTER_SIZE == 4); + const QString cryptoName = is32 ? QStringLiteral("libcrypto-1_1") : QStringLiteral("libcrypto-1_1-x64"); + const QString sslName = is32 ? QStringLiteral("libssl-1_1") : QStringLiteral("libssl-1_1-x64"); + QLibrary libCrypto(cryptoName); + SetLastError(0); + const bool okCrypto = libCrypto.load(); + if (!okCrypto) { + qWarning() << "[TLS] QLibrary 加载" << cryptoName << "失败:" << libCrypto.errorString(); + printTlsWin32Error("QLibrary(libcrypto)", GetLastError()); + printTlsDllLoadSearchPaths(cryptoName); + printTlsLoadDllProbe(appDir, cryptoName); + } + QLibrary libSsl(sslName); + SetLastError(0); + const bool okSsl = libSsl.load(); + if (!okSsl) { + qWarning() << "[TLS] QLibrary 加载" << sslName << "失败:" << libSsl.errorString(); + printTlsWin32Error("QLibrary(libssl)", GetLastError()); + printTlsDllLoadSearchPaths(sslName); + printTlsLoadDllProbe(appDir, sslName); + } + if (okCrypto && okSsl) + qWarning() << "[TLS] 注:QLibrary 能加载但 QSslSocket 仍 false,多为 Qt 与 OpenSSL 版本/符号不匹配,请使用与 Qt 同套件提供的 OpenSSL。"; + + const QString dllHint = is32 + ? QStringLiteral("本程序为 32 位:仅需 libssl-1_1.dll、libcrypto-1_1.dll(必须同为 32 位 PE,勿用 *-x64.dll)。同目录混放 64 位文件无效。") + : QStringLiteral("本程序为 64 位:需 libssl-1_1-x64.dll、libcrypto-1_1-x64.dll(勿用无 x64 的 32 位库)。"); + qWarning() << "[TLS] exe 目录:" << appDir << dllHint + << "若提示“不是有效的 Win32 应用程序”,说明 DLL 位数与 exe 不一致。"; + } +#endif + AppDebugLog::install(); #ifdef _WIN32 diff --git a/mainwindow.cpp b/mainwindow.cpp index a6bcc62..82162b9 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) + , m_dmpUploader(new DmpLogUploader(this)) { ui->setupUi(this); setWindowTitle("雷鸟产测工具 V1.9"); @@ -3302,9 +3304,121 @@ void MainWindow::updateOverallStatus() ui->overallStatusText->setStyleSheet(textStyle); } +QString MainWindow::csvFieldForTestItem(const QString& key, const QString& uiText) const +{ + if (!isTestItemEnabled(key)) + return QStringLiteral("没测"); + QString t = uiText.trimmed(); + if (t == QStringLiteral("未测试")) + return QStringLiteral("没测"); + return t; +} + +QString MainWindow::csvFieldForBattery() const +{ + if (!isTestItemEnabled(QStringLiteral("charge")) && !isTestItemEnabled(QStringLiteral("ntc"))) + return QStringLiteral("没测"); + QString t = ui->batteryStatusText->text().trimmed(); + if (t == QStringLiteral("未测试")) + return QStringLiteral("没测"); + return t; +} + +QString MainWindow::csvFieldFor4gSignal() const +{ + if (!isTestItemEnabled(QStringLiteral("4g"))) + return QStringLiteral("没测"); + QString t = ui->fourgSignalStatusText->text().trimmed(); + if (t == QStringLiteral("未测试")) + t = QStringLiteral("没测"); + return t; +} + +QString MainWindow::csvField4gAverageSignalDetail() const +{ + if (!isTestItemEnabled(QStringLiteral("4g"))) + return QStringLiteral("没测"); + const int valid = cat1SignalReceivedCount - cat1Signal99Count; + if (valid > 0) + return QString::number(double(cat1SignalTotalValue) / valid, 'f', 1); + return QStringLiteral("无有效采样"); +} + +QString MainWindow::csvFieldForPing() const +{ + if (!isTestItemEnabled(QStringLiteral("4g"))) + return QStringLiteral("没测"); + QString t = ui->PingStatusText->text().trimmed(); + if (t == QStringLiteral("未测试")) + return QStringLiteral("没测"); + return t; +} + +QString MainWindow::csvFieldForAudio() const +{ + if (!isTestItemEnabled(QStringLiteral("audio"))) + return QStringLiteral("没测"); + QString t = ui->audioStatusText->text().trimmed(); + if (t == QStringLiteral("未测试")) + t = QStringLiteral("没测"); + return t; +} + +QString MainWindow::csvFieldAudioMicSampleDetail() const +{ + if (!isTestItemEnabled(QStringLiteral("audio"))) + return QStringLiteral("没测"); + if (!audioMicLevelsHistory.isEmpty()) + return audioMicLevelsHistory; + return QStringLiteral("无采样 门槛>=%1").arg(audioLevelStandard); +} + +namespace { + +/** 旧版 19 列(Ping 后为整体结果)→ 补「4G平均信号强度」「录音MIC采样依据」两列,表头对齐 kCsvHeader */ +QStringList migrateProdTestCsvIfOld19Columns(const QStringList &existingLines, const QString &kCsvHeader) +{ + if (existingLines.isEmpty()) + return existingLines; + const QString header0 = existingLines[0]; + if (header0.contains(QStringLiteral("4G平均信号强度"))) + return existingLines; + + qDebug() << "[产测CSV] 检测到旧表头,自动补全两列依据字段(历史行填「历史无此列」)"; + QStringList out; + out << kCsvHeader; + for (int i = 1; i < existingLines.size(); ++i) { + const QString line = existingLines[i]; + if (line.trimmed().isEmpty()) { + out << line; + continue; + } + QStringList f = line.split(QStringLiteral(",")); + if (f.size() == 19) { + QStringList nf; + nf.reserve(21); + for (int j = 0; j < 18; ++j) + nf << f[j]; + nf << QStringLiteral("历史无此列") << QStringLiteral("历史无此列") << f[18]; + out << nf.join(QStringLiteral(",")); + } else { + qDebug() << "[产测CSV] 跳过迁移:行字段数非19,保持原样,行号" << (i + 1); + out << line; + } + } + return out; +} + +} // namespace + // 保存测试记录到CSV文件(可用Excel打开) QString MainWindow::saveTestRecord() { + const QString kCsvHeader = QStringLiteral( + "开始时间,结束时间,测试耗时,软件版本,IMEI,CGSN_IMEI,ICCID,Modem版本,PYVER版本," + "LCD测试,马达测试,按键测试,录音测试,电池测试,G-sensor测试,蓝牙测试,4G信号,Ping测试," + "4G平均信号强度,录音MIC采样依据,整体结果"); + QDateTime endTime = QDateTime::currentDateTime(); QString startTimeStr = testStartTime.toString("yyyy-MM-dd hh:mm:ss"); QString endTimeStr = endTime.toString("yyyy-MM-dd hh:mm:ss"); @@ -3354,19 +3468,7 @@ QString MainWindow::saveTestRecord() qDebug() << " ICCID:" << iccid; qDebug() << " Modem:" << (modem.isEmpty() ? "空" : modem); qDebug() << " PYVER:" << (pyver.isEmpty() ? "空" : pyver); - // 获取各项测试结果 - QString lcdResult = ui->lcdStatusText->text(); - QString motorResult = ui->motorStatusText->text(); - QString keyResult = ui->keyStatusText->text(); - QString audioResult = ui->audioStatusText->text(); - QString batteryResult = ui->batteryStatusText->text(); - QString gsensorResult = ui->gsensorStatusText->text(); - QString bluetoothResult = ui->bluetoothStatusText->text(); - QString fourgSignalResult = ui->fourgSignalStatusText->text(); - const QString pingResult = ui->PingStatusText->text(); QString overallResult = ui->overallStatusText->text(); - - qDebug() << " Ping结果:" << pingResult; // 创建保存目录(在源码同一路径下) QString appPath = QCoreApplication::applicationDirPath(); @@ -3376,9 +3478,10 @@ QString MainWindow::saveTestRecord() dir.mkpath(saveDir); } - // CSV文件路径(统一命名) - QString csvFileName = "产测记录.csv"; - QString csvFilePath = saveDir + "/" + csvFileName; + // CSV:按保存时刻的日期命名(产测记录_yyyyMMdd.csv),每天一个文件;每次写入前重新计算 + const QDateTime csvFileStamp = QDateTime::currentDateTime(); + const QString csvFileName = QStringLiteral("产测记录_%1.csv").arg(csvFileStamp.toString(QStringLiteral("yyyyMMdd"))); + const QString csvFilePath = saveDir + QLatin1Char('/') + csvFileName; // 检查是否是新文件(需要写入表头) bool isNewFile = !QFile::exists(csvFilePath); @@ -3394,15 +3497,17 @@ QString MainWindow::saveTestRecord() << (iccid.isEmpty() ? "未获取" : iccid) << (modem.isEmpty() ? "未获取" : modem) << (pyver.isEmpty() ? "未获取" : pyver) - << lcdResult - << motorResult - << keyResult - << audioResult - << batteryResult - << gsensorResult - << bluetoothResult - << fourgSignalResult - << pingResult + << csvFieldForTestItem(QStringLiteral("lcd"), ui->lcdStatusText->text()) + << csvFieldForTestItem(QStringLiteral("motor"), ui->motorStatusText->text()) + << csvFieldForTestItem(QStringLiteral("key"), ui->keyStatusText->text()) + << csvFieldForAudio() + << csvFieldForBattery() + << csvFieldForTestItem(QStringLiteral("gsensor"), ui->gsensorStatusText->text()) + << csvFieldForTestItem(QStringLiteral("bluetooth"), ui->bluetoothStatusText->text()) + << csvFieldFor4gSignal() + << csvFieldForPing() + << csvField4gAverageSignalDetail() + << csvFieldAudioMicSampleDetail() << overallResult; QString newDataLine = dataFields.join(",") + "\n"; @@ -3410,7 +3515,7 @@ QString MainWindow::saveTestRecord() if (isNewFile) { // 新文件,写入表头和数据 - finalContent += "开始时间,结束时间,测试耗时,软件版本,IMEI,CGSN_IMEI,ICCID,Modem版本,PYVER版本,LCD测试,马达测试,按键测试,录音测试,电池测试,G-sensor测试,蓝牙测试,4G信号,Ping测试,整体结果\n"; + finalContent += kCsvHeader + QLatin1Char('\n'); finalContent += newDataLine; qDebug() << "创建新的CSV文件:" << csvFilePath; } else { @@ -3429,46 +3534,23 @@ QString MainWindow::saveTestRecord() existingLines << readStream.readLine(); } existingFile.close(); + + const QStringList workingLines = migrateProdTestCsvIfOld19Columns(existingLines, kCsvHeader); - // 构建最终内容 - if (!existingLines.isEmpty()) { - finalContent = existingLines[0] + "\n"; // 表头 - - bool iccidFound = false; - - // 检查每一行数据,查找相同ICCID - for (int i = 1; i < existingLines.size(); i++) { - QString line = existingLines[i]; - if (line.trimmed().isEmpty()) continue; - - // 分割CSV行,获取ICCID字段(第7列,索引为6) - QStringList fields = line.split(","); - if (fields.size() >= 7) { - QString existingIccid = fields[6].trimmed(); - - if (existingIccid == iccid) { - // 找到相同ICCID,替换为新记录 - finalContent += newDataLine; - iccidFound = true; - qDebug() << "找到相同ICCID,替换记录:" << iccid; - } else { - // 保留不同ICCID的记录 - finalContent += line + "\n"; - } - } else { - // 格式不正确的行,保留 - finalContent += line + "\n"; - } - } - - // 如果没有找到相同ICCID,追加新记录 - if (!iccidFound) { - finalContent += newDataLine; - qDebug() << "未找到相同ICCID,追加新记录:" << iccid; + // 构建最终内容:保留全部历史行,同一硬件多次测试**追加**新行(不再按 ICCID 覆盖) + if (!workingLines.isEmpty()) { + finalContent = workingLines[0] + QLatin1Char('\n'); // 表头(可能已由迁移更新) + for (int i = 1; i < workingLines.size(); i++) { + const QString line = workingLines[i]; + if (line.trimmed().isEmpty()) + continue; + finalContent += line + QLatin1Char('\n'); } + finalContent += newDataLine; + qDebug() << "产测 CSV 追加一行(同一 ICCID 可多条)ICCID:" << iccid; } else { // 文件为空,重建 - finalContent = "开始时间,结束时间,测试耗时,软件版本,IMEI,CGSN_IMEI,ICCID,Modem版本,PYVER版本,LCD测试,马达测试,按键测试,录音测试,电池测试,G-sensor测试,蓝牙测试,4G信号,Ping测试,整体结果\n"; + finalContent = kCsvHeader + QLatin1Char('\n'); finalContent += newDataLine; } } @@ -3493,8 +3575,11 @@ QString MainWindow::saveTestRecord() qDebug() << "测试记录已保存到:" << csvFilePath; qDebug() << "记录内容: 开始时间=" << startTimeStr << ", 结束时间=" << endTimeStr << ", 耗时=" << durationStr << ", Ver=" << ver << ", IMEI=" << imei << ", CGSN=" << cgsnImei << ", ICCID=" << iccid << ", Modem=" << modem - << ", PYVER=" << pyver << ", LCD=" << lcdResult << ", 马达=" << motorResult - << ", 按键=" << keyResult << ", 4G信号=" << fourgSignalResult << ", Ping=" << pingResult + << ", PYVER=" << pyver << ", LCD=" << csvFieldForTestItem(QStringLiteral("lcd"), ui->lcdStatusText->text()) + << ", 录音=" << csvFieldForAudio() + << ", 录音MIC依据=" << csvFieldAudioMicSampleDetail() + << ", 4G=" << csvFieldFor4gSignal() << ", 4G平均=" << csvField4gAverageSignalDetail() + << ", Ping=" << csvFieldForPing() << ", 整体=" << overallResult; // 显示保存成功消息 @@ -3510,6 +3595,29 @@ QString MainWindow::saveTestRecord() // 用最终设备信息修正会话日志和UI日志的文件名与文件头(可能之前是TMP占位) renameLogFileWithDeviceInfo(); renameUiLogFileWithDeviceInfo(); + + // DMP 上传日志:与产测记录/logs 下主会话日志同名,尾缀 _dmp_upload.log + { + QString dmpLogPath; + if (!testLogFilePath.isEmpty()) { + const QFileInfo fi(testLogFilePath); + dmpLogPath = fi.absolutePath() + QLatin1Char('/') + fi.completeBaseName() + QStringLiteral("_dmp_upload.log"); + } else { + const QString logsDir = appPath + QStringLiteral("/产测记录/logs"); + QDir().mkpath(logsDir); + const QDateTime now = QDateTime::currentDateTime(); + const QString im = imei.isEmpty() ? QStringLiteral("NA") : imei; + const QString ic = iccid.isEmpty() ? QStringLiteral("NA") : iccid; + dmpLogPath = QStringLiteral("%1/%2_%3_%4_%5_dmp_upload.log") + .arg(logsDir) + .arg(now.toString(QStringLiteral("yyyyMMdd"))) + .arg(now.toString(QStringLiteral("hhmmss"))) + .arg(im) + .arg(ic); + } + m_dmpUploader->uploadProductionRecord(dataFields, imei, cgsnImei, iccid, dmpLogPath); + } + // 测试结束立即关闭日志文件,后续不再写入;下一轮开始会重新创建 closeTestLogFile(); closeUiLogFile(); @@ -3845,6 +3953,7 @@ void MainWindow::on_openButton_2_clicked() keyTestStarted = false; audioTestPopupShown = false; audioPlaybackPopupShown = false; + audioMicLevelsHistory.clear(); // 重置状态指示器 updateTestStatus("lcd", STATUS_UNTESTED); @@ -3942,6 +4051,7 @@ void MainWindow::performAudioTestContinue() audioMicMiddleConsecutiveCount = 0; audioMicTotalPassCount = 0; audioTestFinished = false; + audioMicLevelsHistory.clear(); // 获取音频测试通过标准(从UI中读取) audioLevelStandard = "中"; // 默认值 @@ -4355,6 +4465,12 @@ void MainWindow::checkAudioTestResult(const QString& data) qDebug() << "无法解析音频等级,跳过"; return; } + + if (!audioMicLevelsHistory.isEmpty()) + audioMicLevelsHistory += QLatin1Char('|'); + audioMicLevelsHistory += detectedLevel; + if (audioMicLevelsHistory.size() > 400) + audioMicLevelsHistory = audioMicLevelsHistory.right(400); qDebug() << QString("解析到音频等级: %1 (要求: >= %2)").arg(detectedLevel).arg(audioLevelStandard); @@ -6777,6 +6893,8 @@ void MainWindow::saveTestConfigToSettings() // 保存蓝牙测试参数 configManager.setTestConfig("bluetoothAddress", ui->bluetoothAddressLineEdit->text()); configManager.setTestConfig("bluetoothRSSI", ui->bluetoothRSSILineEdit->text()); + // 4G CAT1 信号门槛(空则运行时用默认 25) + configManager.setTestConfig("cat1MinSignal", ui->cat1RSSILineEdit->text()); // 保存测试项checkbox状态 configManager.setTestCheckbox("lcd", ui->checkBox_lcd->isChecked()); @@ -6828,6 +6946,7 @@ void MainWindow::loadTestConfigFromSettings() // 恢复蓝牙测试参数 ui->bluetoothAddressLineEdit->setText(configManager.getTestConfig("bluetoothAddress", "")); ui->bluetoothRSSILineEdit->setText(configManager.getTestConfig("bluetoothRSSI", "")); + ui->cat1RSSILineEdit->setText(configManager.getTestConfig("cat1MinSignal", "")); // 恢复测试项checkbox状态 ui->checkBox_lcd->setChecked(configManager.getTestCheckbox("lcd", true)); @@ -7094,11 +7213,13 @@ void MainWindow::createTestLogFile() dir.mkpath(logsDir); } - // 生成临时文件名:yyyymmdd_hhmmss_TMP_TMP.log - // 说明:测试过程中数据要安全落盘;文件名关键信息在获取到IMEI/ICCID后再重命名补齐 - QString fileName = QString("%1_%2_%3_%4.log") + // 临时文件名:yyyymmdd_hhmmss_mmm_TMP_TMP.log(含毫秒,避免同秒两次产测重命名撞车覆盖) + // 获取到 IMEI/ICCID 后重命名为 日期_时间_毫秒_IMEI_ICCID.log + const QString msecPart = QStringLiteral("%1").arg(now.time().msec(), 3, 10, QChar('0')); + QString fileName = QString("%1_%2_%3_%4_%5.log") .arg(now.toString("yyyyMMdd")) .arg(now.toString("hhmmss")) + .arg(msecPart) .arg(imei) .arg(iccid); @@ -7160,10 +7281,11 @@ void MainWindow::createUiLogFile() dir.mkpath(logsDir); } - // 生成临时文件名:yyyymmdd_hhmmss_TMP_TMP_UI.log - QString fileName = QString("%1_%2_%3_%4_UI.log") + const QString msecPartUi = QStringLiteral("%1").arg(now.time().msec(), 3, 10, QChar('0')); + QString fileName = QString("%1_%2_%3_%4_%5_UI.log") .arg(now.toString("yyyyMMdd")) .arg(now.toString("hhmmss")) + .arg(msecPartUi) .arg(imei) .arg(iccid); @@ -7271,28 +7393,41 @@ void MainWindow::renameLogFileWithDeviceInfo() return; } - // 从当前文件名中提取时间部分 QFileInfo fileInfo(testLogFilePath); QString baseName = fileInfo.baseName(); - QStringList parts = baseName.split("_"); + QStringList parts = baseName.split(QStringLiteral("_")); const QString oldMainPathForMirror = testLogFilePath; - if (parts.size() >= 2) { - QString datePart = parts[0]; // yyyymmdd - QString timePart = parts[1]; // hhmmss - - // 生成新文件名 - QString newFileName = QString("%1_%2_%3_%4.log") - .arg(datePart) - .arg(timePart) - .arg(imei) - .arg(iccid); - - QString newFilePath = fileInfo.absolutePath() + "/" + newFileName; - - // 如果新文件名与当前文件名不同,重命名并修正文件头中的IMEI/ICCID - if (newFilePath != testLogFilePath) { + QString datePart; + QString timePart; + QString msecPart; + // 新:yyyyMMdd_hhmmss_mmm_TMP_TMP;旧:yyyyMMdd_hhmmss_TMP_TMP(无毫秒) + if (parts.size() >= 5 && parts[3] == QStringLiteral("TMP") && parts[4] == QStringLiteral("TMP")) { + datePart = parts[0]; + timePart = parts[1]; + msecPart = parts[2]; + } else if (parts.size() >= 4 && parts[2] == QStringLiteral("TMP") && parts[3] == QStringLiteral("TMP")) { + datePart = parts[0]; + timePart = parts[1]; + msecPart = QStringLiteral("000"); + } else { + qDebug() << "renameLogFileWithDeviceInfo: 无法解析会话日志基名,跳过:" << baseName; + return; + } + + // 生成新文件名(保留毫秒段,同一硬件多次测试各有一份文件) + const QString newFileName = QString("%1_%2_%3_%4_%5.log") + .arg(datePart) + .arg(timePart) + .arg(msecPart) + .arg(imei) + .arg(iccid); + + const QString newFilePath = fileInfo.absolutePath() + QLatin1Char('/') + newFileName; + + // 如果新文件名与当前文件名不同,重命名并修正文件头中的IMEI/ICCID + if (newFilePath != testLogFilePath) { // 确保当前文件关闭 if (testLogFile->isOpen()) { testLogFile->close(); @@ -7351,7 +7486,6 @@ void MainWindow::renameLogFileWithDeviceInfo() } } } - } } // 使用设备信息重命名UI日志文件(与会话测试日志同步) @@ -7373,23 +7507,40 @@ void MainWindow::renameUiLogFileWithDeviceInfo() } QFileInfo fileInfo(uiLogFilePath); - QString baseName = fileInfo.baseName(); - QStringList parts = baseName.split("_"); - - if (parts.size() >= 2) { - QString datePart = parts[0]; // yyyymmdd - QString timePart = parts[1]; // hhmmss + const QString baseName = fileInfo.baseName(); + QStringList parts = baseName.split(QStringLiteral("_")); + if (parts.isEmpty() || parts.last() != QStringLiteral("UI")) { + qDebug() << "renameUiLogFileWithDeviceInfo: 基名不以 _UI 结尾,跳过:" << baseName; + return; + } + parts.removeLast(); + + QString datePart; + QString timePart; + QString msecPart; + if (parts.size() >= 5 && parts[3] == QStringLiteral("TMP") && parts[4] == QStringLiteral("TMP")) { + datePart = parts[0]; + timePart = parts[1]; + msecPart = parts[2]; + } else if (parts.size() >= 4 && parts[2] == QStringLiteral("TMP") && parts[3] == QStringLiteral("TMP")) { + datePart = parts[0]; + timePart = parts[1]; + msecPart = QStringLiteral("000"); + } else { + qDebug() << "renameUiLogFileWithDeviceInfo: 无法解析 UI 日志基名,跳过:" << baseName; + return; + } - // 生成新文件名:与旧方案类似,后缀标记为UI - QString newFileName = QString("%1_%2_%3_%4_UI.log") - .arg(datePart) - .arg(timePart) - .arg(imei) - .arg(iccid); + const QString newFileName = QString("%1_%2_%3_%4_%5_UI.log") + .arg(datePart) + .arg(timePart) + .arg(msecPart) + .arg(imei) + .arg(iccid); - QString newFilePath = fileInfo.absolutePath() + "/" + newFileName; + const QString newFilePath = fileInfo.absolutePath() + QLatin1Char('/') + newFileName; - if (newFilePath != uiLogFilePath) { + if (newFilePath != uiLogFilePath) { // 确保当前文件关闭 if (uiLogFile->isOpen()) { uiLogFile->close(); @@ -7443,7 +7594,6 @@ void MainWindow::renameUiLogFileWithDeviceInfo() } } } - } } // ICCID输入框文本改变时触发 diff --git a/mainwindow.h b/mainwindow.h index 3c35dd3..4c757be 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -20,6 +20,7 @@ #include #include #include "config_manager.h" +#include "dmp_log_uploader.h" #include "qrcode_barcode_generator.h" QT_BEGIN_NAMESPACE @@ -202,6 +203,7 @@ private: int audioMicTotalPassCount; // 累计检测到满足要求的Level的次数(包括非连续的) QString audioLevelStandard; // 音频测试通过标准("中"或"低"等) bool audioTestFinished; // 录音测试是否已完成(成功或失败) + QString audioMicLevelsHistory; // fac:mic level 采样序列(低|高|静音…),供 CSV/DMP QSet bluetoothAddresses; // 蓝牙测试检测到的不同蓝牙地址集合 bool bluetoothTestFinished; // 蓝牙测试是否已完成(成功或失败) QString detectedBluetoothAddress; // 检测到的指定蓝牙地址 @@ -221,6 +223,7 @@ private: // 配置管理 ConfigManager configManager; // 配置管理器(不使用指针,直接作为成员) + DmpLogUploader *m_dmpUploader; // DMP 产测记录 HTTPS 上传(独立日志) // 开机检测相关变量 bool bootDetected; // 是否已检测到开机 @@ -295,6 +298,16 @@ private: // 测试项checkbox检查辅助函数 bool isTestItemEnabled(const QString& testName) const; void skipToNextEnabledTest(); // 跳过当前测试项,推进到下一个启用的测试项 + /** 产测 CSV/DMP:未勾选项写「没测」;录音/4G 等附加明细 */ + QString csvFieldForTestItem(const QString& key, const QString& uiText) const; + QString csvFieldForBattery() const; + QString csvFieldFor4gSignal() const; + QString csvFieldForPing() const; + QString csvFieldForAudio() const; + /** 独立列:CAT1 有效采样平均 RSSI;无有效值时“无有效采样”或未测“没测” */ + QString csvField4gAverageSignalDetail() const; + /** 独立列:fac:mic 档位序列(| 分隔)或 无采样/没测 */ + QString csvFieldAudioMicSampleDetail() const; // 二维码和条形码生成相关函数 void onICCIDLineEditTextChanged(const QString& text); // ICCID输入框文本改变时触发