添加日志上传服务器
authorsnickersun <snickersun@163.com>
Tue, 21 Apr 2026 12:41:05 +0000 (20:41 +0800)
committersnickersun <snickersun@163.com>
Tue, 21 Apr 2026 12:41:05 +0000 (20:41 +0800)
添加mic,4g信号测试通过值
日志按天存储,相同设备可以重复测试。

13 files changed:
.gitignore
config_manager.cpp
deploy_qt.bat [new file with mode: 0644]
dmp_log_uploader.cpp [new file with mode: 0644]
dmp_log_uploader.h [new file with mode: 0644]
factory_tool.pro
libs/libcrypto-1_1-x64.dll [new file with mode: 0644]
libs/libcrypto-1_1.dll [new file with mode: 0644]
libs/libssl-1_1-x64.dll [new file with mode: 0644]
libs/libssl-1_1.dll [new file with mode: 0644]
main.cpp
mainwindow.cpp
mainwindow.h

index 1013540..6c19ed9 100644 (file)
@@ -52,3 +52,6 @@ compile_commands.json
 *creator.user*
 
 *_qmlcache.qrc
+
+build_release/
+build_debug/
index 1b9131a..c49910f 100644 (file)
@@ -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 (file)
index 0000000..128b290
--- /dev/null
@@ -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 (file)
index 0000000..0ba4c00
--- /dev/null
@@ -0,0 +1,179 @@
+#include "dmp_log_uploader.h"
+
+#include <QCoreApplication>
+#include <QDateTime>
+#include <QDebug>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QRegularExpression>
+#include <QSslSocket>
+#include <QUrl>
+#include <QtGlobal>
+
+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 (file)
index 0000000..1852e00
--- /dev/null
@@ -0,0 +1,40 @@
+#ifndef DMP_LOG_UPLOADER_H
+#define DMP_LOG_UPLOADER_H
+
+#include <QObject>
+#include <QStringList>
+
+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
index 26dac3a..8358ac9 100644 (file)
@@ -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 (file)
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 (file)
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 (file)
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 (file)
index 0000000..5c87ce4
Binary files /dev/null and b/libs/libssl-1_1.dll differ
index 58b01c8..284c670 100644 (file)
--- a/main.cpp
+++ b/main.cpp
 #include "app_debug_log.h"
 
 #include <QApplication>
+#include <QDebug>
 #include <QTextCodec>
+#include <QtGlobal>
+#include <QSslSocket>
+#ifdef Q_OS_WIN
+#include <QLibrary>
+#include <QCoreApplication>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <string>
+#endif
 #ifdef _WIN32
 #include <windows.h>
 #include <io.h>
 #include <fcntl.h>
 #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<LPWSTR>(&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
index a6bcc62..82162b9 100644 (file)
@@ -15,6 +15,7 @@
 #include <QStandardPaths>
 #include <QDir>
 #include <QFile>
+#include <QFileInfo>
 #include <QDesktopServices>
 #include <QUrl>
 #include <QCoreApplication>
@@ -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输入框文本改变时触发
index 3c35dd3..4c757be 100644 (file)
@@ -20,6 +20,7 @@
 #include <QFile>
 #include <QTextStream>
 #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<QString> 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输入框文本改变时触发