diff --git a/Uft30ChangeCode.pro b/Uft30ChangeCode.pro index a2d8edd..e873c51 100644 --- a/Uft30ChangeCode.pro +++ b/Uft30ChangeCode.pro @@ -1,4 +1,4 @@ -QT += core gui widgets sql network +QT += core gui widgets sql network concurrent TARGET = Uft30ChangeCode TEMPLATE = app @@ -10,6 +10,7 @@ SOURCES += main.cpp \ src/pythonrunner/PythonRunner.cpp \ src/pages/about/aboutdialog.cpp \ src/pages/batchconvert/batchconvertpage.cpp \ + src/pages/codesearch/codesearchpage.cpp \ src/pages/functionsearch/functionsearchpage.cpp \ src/pages/functionsearch/uf20functionsearchpage.cpp \ src/pages/functionsearch/uft3functionsearchpage.cpp \ @@ -34,6 +35,7 @@ SOURCES += main.cpp \ src/utils/uft3configreader.cpp \ src/utils/logmanager.cpp \ src/utils/configmanager.cpp \ + src/utils/callchainanalyzer.cpp \ src/utils/version.cpp \ src/utils/versionchecker.cpp @@ -41,6 +43,7 @@ HEADERS += src/mainwindow/mainwindow.h \ src/pythonrunner/PythonRunner.h \ src/pages/about/aboutdialog.h \ src/pages/batchconvert/batchconvertpage.h \ + src/pages/codesearch/codesearchpage.h \ src/pages/functionsearch/functionsearchpage.h \ src/pages/functionsearch/uf20functionsearchpage.h \ src/pages/functionsearch/uft3functionsearchpage.h \ @@ -65,6 +68,7 @@ HEADERS += src/mainwindow/mainwindow.h \ src/utils/uft3configreader.h \ src/utils/logmanager.h \ src/utils/configmanager.h \ + src/utils/callchainanalyzer.h \ src/utils/version.h \ src/utils/versionchecker.h diff --git a/build.bat b/build.bat index d5c8a2a..e592080 100644 --- a/build.bat +++ b/build.bat @@ -76,6 +76,16 @@ if errorlevel 1 ( ) echo. +REM Deploy Qt runtime dependencies (auto-copy required Qt DLLs/plugins) +echo Deploying Qt runtime dependencies... +windeployqt --release bin\Uft30ChangeCode.exe +if errorlevel 1 ( + echo [WARN] windeployqt failed, please deploy Qt DLLs manually +) else ( + echo [OK] Qt dependencies deployed +) +echo. + echo ========================================== echo Build SUCCESSFUL! echo ========================================== diff --git a/help/Writerside/hi.tree b/help/Writerside/hi.tree index 05f2b97..614304b 100644 --- a/help/Writerside/hi.tree +++ b/help/Writerside/hi.tree @@ -10,6 +10,7 @@ + @@ -21,7 +22,8 @@ - + + diff --git a/help/Writerside/images/转码业务.png b/help/Writerside/images/转码业务.png new file mode 100644 index 0000000..d25049b Binary files /dev/null and b/help/Writerside/images/转码业务.png differ diff --git a/help/Writerside/images/非转码业务.png b/help/Writerside/images/非转码业务.png new file mode 100644 index 0000000..0731f43 Binary files /dev/null and b/help/Writerside/images/非转码业务.png differ diff --git a/help/Writerside/topics/1-3-0.md b/help/Writerside/topics/update-1-3-0.md similarity index 100% rename from help/Writerside/topics/1-3-0.md rename to help/Writerside/topics/update-1-3-0.md diff --git a/help/Writerside/topics/update-1-4-0.md b/help/Writerside/topics/update-1-4-0.md new file mode 100644 index 0000000..c466ab6 --- /dev/null +++ b/help/Writerside/topics/update-1-4-0.md @@ -0,0 +1,11 @@ +# 1.4.0 + +1.提升业务转码性能; + +2.元数据更新支持增量更新和强制全量更新; + +3.新增代码检索功能; + +4.支持函数调用链路分析; + +5.支持全局文字可选中复制及表格Ctrl+C导出; \ No newline at end of file diff --git a/help/Writerside/topics/下载.md b/help/Writerside/topics/下载.md index fe2ae4a..f11528d 100644 --- a/help/Writerside/topics/下载.md +++ b/help/Writerside/topics/下载.md @@ -14,6 +14,7 @@ | 版本 | 更新日期 | 全量包 | 增量包 | |-------|------------|----------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| 1.4.0 | 2026-07-21 | [Uft30ChangeCode-1.4.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.4.0.zip) | [Uft30ChangeCode-1.4.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.4.0-update.zip) | | 1.3.0 | 2026-07-07 | [Uft30ChangeCode-1.3.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.3.0.zip) | [Uft30ChangeCode-1.3.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.3.0-update.zip) | | 1.2.0 | 2026-07-02 | [Uft30ChangeCode-1.2.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.2.0.zip) | [Uft30ChangeCode-1.2.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.2.0-update.zip) | | 1.1.0 | 2026-07-01 | [Uft30ChangeCode-1.1.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.1.0.zip) | [Uft30ChangeCode-1.1.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.1.0-update.zip) | diff --git a/help/Writerside/topics/代码检索.md b/help/Writerside/topics/代码检索.md new file mode 100644 index 0000000..048ca11 --- /dev/null +++ b/help/Writerside/topics/代码检索.md @@ -0,0 +1,11 @@ +# 代码检索 + +支持"转码业务"、"非转码业务"调用链路查询和导出 + +## 转码业务 +支持查询调用链路和导出查询函数所涉及的文件以及调用链路导出 +转码业务 + +## 非转码业务 +支持查询调用链路和调用链路导出 +非转码业务 \ No newline at end of file diff --git a/help/Writerside/topics/功能转码.md b/help/Writerside/topics/功能转码.md index 4bb1c25..1ee15db 100644 --- a/help/Writerside/topics/功能转码.md +++ b/help/Writerside/topics/功能转码.md @@ -30,3 +30,5 @@ ## 快速跳转 [业务转码](http://10.20.163.105:6045/uft3changecode/业务转码.html) + +[代码检索](http://10.20.163.105:6045/uft3changecode/业务转码.html) diff --git a/main.cpp b/main.cpp index 1646edc..a5c374f 100644 --- a/main.cpp +++ b/main.cpp @@ -4,11 +4,96 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include + +// 全局文字可复制过滤器: +// 1) 所有 QLabel(含 ElaText)文字可用鼠标选中并复制; +// 2) 表格/树(QAbstractItemView)选中的单元格支持 Ctrl+C 复制,制表符分隔列、换行分隔行。 +class TextSelectableFilter : public QObject +{ +public: + using QObject::QObject; + +protected: + bool eventFilter(QObject *watched, QEvent *event) override + { + if (event->type() == QEvent::Polish) { + if (QLabel *label = qobject_cast(watched)) { + // 跳过用作图标按钮的 QLabel(有 pixmap 无文本), + // 否则会被强制设为竖线(IBeamCursor),影响点击类图标控件 + if (label->pixmap().isNull()) { + label->setTextInteractionFlags(label->textInteractionFlags() + | Qt::TextSelectableByMouse); + label->setCursor(Qt::IBeamCursor); + } + } + } else if (event->type() == QEvent::KeyPress) { + if (auto *view = qobject_cast(watched)) { + auto *ke = static_cast(event); + if (ke->matches(QKeySequence::Copy)) { + return copySelectedCells(view); + } + } + } + return QObject::eventFilter(watched, event); + } + +private: + static bool copySelectedCells(QAbstractItemView *view) + { + if (!view->selectionModel()) { + return false; + } + QModelIndexList idxs = view->selectionModel()->selectedIndexes(); + if (idxs.isEmpty()) { + return false; + } + // 按行、列排序,保证复制顺序与显示一致 + std::sort(idxs.begin(), idxs.end(), + [](const QModelIndex &a, const QModelIndex &b) { + if (a.row() != b.row()) { + return a.row() < b.row(); + } + return a.column() < b.column(); + }); + + QString text; + int lastRow = -1; + for (const QModelIndex &idx : idxs) { + if (idx.row() != lastRow) { + if (lastRow != -1) { + text += QLatin1Char('\n'); + } + lastRow = idx.row(); + } else { + text += QLatin1Char('\t'); + } + text += idx.data(Qt::DisplayRole).toString(); + } + if (!text.isEmpty()) { + QApplication::clipboard()->setText(text); + return true; // 已处理,消费该事件 + } + return false; + } +}; int main(int argc, char *argv[]) { QApplication a(argc, argv); a.setWindowIcon(QIcon(":resources/images/ChangeCode.png")); + + // 安装全局过滤器:所有页面文字 / 表格树单元格允许用户复制 + TextSelectableFilter textSelectableFilter; + a.installEventFilter(&textSelectableFilter); + eApp->init(); QString basePath = QCoreApplication::applicationDirPath(); @@ -17,4 +102,4 @@ int main(int argc, char *argv[]) MainWindow w; w.showMaximized(); return a.exec(); -} \ No newline at end of file +} diff --git a/resources.qrc b/resources.qrc index 4dc47a1..287bbd3 100644 --- a/resources.qrc +++ b/resources.qrc @@ -7,5 +7,7 @@ resources/images/max.png resources/images/min.png resources/images/update.png + resources/images/expand_all.png + resources/images/collapse_all.png \ No newline at end of file diff --git a/resources/images/collapse_all.png b/resources/images/collapse_all.png new file mode 100644 index 0000000..cec3a89 Binary files /dev/null and b/resources/images/collapse_all.png differ diff --git a/resources/images/expand_all.png b/resources/images/expand_all.png new file mode 100644 index 0000000..3451a83 Binary files /dev/null and b/resources/images/expand_all.png differ diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index ff0c0b2..4558696 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -40,6 +40,9 @@ MainWindow::MainWindow(QWidget *parent) , m_iconsSet(false) , m_navigationButton(nullptr) , m_isNavigationExpanded(true) + , m_totalFunctions(0) + , m_currentFunctionIndex(0) + , m_lastProgress(0) { initWindow(); initContent(); @@ -133,7 +136,8 @@ void MainWindow::initContent() { addExpanderNode("功能转码", m_funcConvertKey, ElaIconType::FileCode); addPageNode("业务转码", createBatchConvertPage(), m_funcConvertKey, ElaIconType::ArrowsRepeat); - + addPageNode("代码检索", createCodeSearchPage(), m_funcConvertKey, ElaIconType::Cubes); + addExpanderNode("功能查询", m_funcSearchKey, ElaIconType::MagnifyingGlass); addPageNode("UF20功能查询", createUF20FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus); addPageNode("UFT3功能查询", createUFT3FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus); @@ -157,6 +161,12 @@ QWidget* MainWindow::createBatchConvertPage() return m_batchConvertPage; } +QWidget* MainWindow::createCodeSearchPage() +{ + _codeSearchPage = new CodeSearchPage; + return _codeSearchPage; +} + QWidget* MainWindow::createFunctionSearchPage() { return new FunctionSearchPage; @@ -222,6 +232,9 @@ void MainWindow::onBatchConvertStart() m_currentFunctionIndex = 0; m_functionList.clear(); } + + // 进度基准:BatchConvertPage 在开始时已将进度条置为 20(配置加载完成) + m_lastProgress = 20; LogManager::instance()->log("========================================"); LogManager::instance()->log("开始执行 UFT2 到 UFT3 的代码转换..."); @@ -255,6 +268,9 @@ void MainWindow::onPythonRunnerFinished(int exitCode, QProcess::ExitStatus exitS LogManager::instance()->log("========================================"); if (exitStatus == QProcess::NormalExit && exitCode == 0) { LogManager::instance()->log("✓ 代码转换完成!"); + if (_codeSearchPage) { + _codeSearchPage->refreshIndexSilent(); + } QMessageBox::information(this, "完成", "代码转换任务已成功完成!"); } else { LogManager::instance()->logError(QString("✗ 转换失败! 退出码: %1").arg(exitCode)); @@ -265,59 +281,82 @@ void MainWindow::onPythonRunnerFinished(int exitCode, QProcess::ExitStatus exitS void MainWindow::onPythonRunnerOutput(const QString &output) { - if (!output.trimmed().isEmpty()) { - LogManager::instance()->log(output); - - QString trimmedOutput = output.trimmed(); - - QRegularExpression progressRegExp(R"(progress\s*[:=]\s*(\d+))"); - QRegularExpressionMatch match = progressRegExp.match(trimmedOutput); - if (match.hasMatch()) { - int progress = match.captured(1).toInt(); - QString status = trimmedOutput; - if (status.contains("progress", Qt::CaseInsensitive)) { - status = status.replace(progressRegExp, "").trimmed(); - if (status.isEmpty()) { - status = QString("转换中 %1%").arg(progress); - } + if (output.trimmed().isEmpty()) { + return; + } + + LogManager::instance()->log(output); + + const QString text = output.trimmed(); + + // 计算本次输出对应的目标进度;-1 表示本次输出不含可识别的进度标记 + int targetProgress = -1; + QString status; + + // 1) 优先解析工具显式输出的 progress:xx / progress=xx + static const QRegularExpression progressRegExp( + R"(progress\s*[:=]\s*(\d+))", QRegularExpression::CaseInsensitiveOption); + QRegularExpressionMatch pm = progressRegExp.match(text); + if (pm.hasMatch()) { + targetProgress = pm.captured(1).toInt(); + status = QString("转换中 %1%").arg(targetProgress); + } else { + // 2) 解析并行翻译进度 [ n/m](翻译阶段映射到 55~78) + // 注意时间戳 [2026-07-17 ...] 不含 '/' 不会误匹配 + static const QRegularExpression stepRegExp(R"(\[\s*(\d+)\s*/\s*(\d+)\s*\])"); + QRegularExpressionMatch stepMatch; + auto it = stepRegExp.globalMatch(text); + while (it.hasNext()) { + stepMatch = it.next(); // 取本段输出里最后一个 [n/m] + } + if (stepMatch.hasMatch()) { + int cur = stepMatch.captured(1).toInt(); + int tot = stepMatch.captured(2).toInt(); + if (tot > 0) { + targetProgress = 55 + (cur * 23) / tot; + status = QString("正在翻译文件 %1/%2").arg(cur).arg(tot); } - if (m_batchConvertPage) { - m_batchConvertPage->onProgressUpdate(progress, status); - } - } else { - if (m_batchConvertPage) { - QString currentFunction; - QRegularExpression funcRegExp(R"(([\w_]+))"); - QRegularExpressionMatch funcMatch = funcRegExp.match(trimmedOutput); - if (funcMatch.hasMatch()) { - currentFunction = funcMatch.captured(1); - } - - if (!currentFunction.isEmpty() && m_totalFunctions > 0) { - int index = m_functionList.indexOf(currentFunction); - if (index >= 0 && index >= m_currentFunctionIndex) { - m_currentFunctionIndex = index + 1; - int progress = 20 + (m_currentFunctionIndex * 80) / m_totalFunctions; - QString status = QString("正在转换: %1").arg(currentFunction); - m_batchConvertPage->onProgressUpdate(progress, status); - return; + } + + // 3) 解析流水线阶段标记(关键字命中取最大值,保证进度稳定推进) + if (targetProgress < 0) { + struct Marker { const char* key; int prog; const char* label; }; + static const Marker markers[] = { + {"开始加载数据", 25, "正在加载数据..."}, + {"配置文件加载完成", 30, "配置文件加载完成"}, + {"元数据加载完成", 35, "元数据加载完成"}, + {"全部数据加载完成", 42, "数据加载完成"}, + {"数据初始化完成", 45, "数据初始化完成"}, + {"校验完成", 52, "数据校验完成"}, + {"翻译完成", 80, "代码翻译完成"}, + {"后处理完成", 90, "后处理完成"}, + {"输出完成", 96, "结果输出完成"}, + {"转换流水线执行成功", 98, "转换流水线执行成功"}, + {"转码完成", 98, "转码完成"}, + }; + for (const auto &m : markers) { + if (text.contains(QString::fromUtf8(m.key))) { + if (m.prog > targetProgress) { + targetProgress = m.prog; + status = QString::fromUtf8(m.label); } } - - int currentProgress = 20; - if (trimmedOutput.contains("开始转换", Qt::CaseInsensitive)) { - currentProgress = 25; - } else if (trimmedOutput.contains("处理文件", Qt::CaseInsensitive)) { - currentProgress = 30; - } else if (trimmedOutput.contains("生成代码", Qt::CaseInsensitive)) { - currentProgress = 75; - } else if (trimmedOutput.contains("完成", Qt::CaseInsensitive)) { - currentProgress = 95; - } - m_batchConvertPage->onProgressUpdate(currentProgress, trimmedOutput); } } } + + if (!m_batchConvertPage) { + return; + } + + // 进度单调递增:只有更大的进度才会推进进度条,避免"一下长一下短" + if (targetProgress > m_lastProgress) { + m_lastProgress = targetProgress; + } + + // 状态文字:仅展示已识别的处理阶段;未识别的日志行不显示(保持上一次状态) + // 空 status 表示当前行无有效状态,BatchConvertPage 会保留上一次文字 + m_batchConvertPage->onProgressUpdate(m_lastProgress, status); } void MainWindow::onPythonRunnerError(const QString &error) diff --git a/src/mainwindow/mainwindow.h b/src/mainwindow/mainwindow.h index deec0c4..7c8b991 100644 --- a/src/mainwindow/mainwindow.h +++ b/src/mainwindow/mainwindow.h @@ -6,6 +6,7 @@ #include #include "src/pythonrunner/PythonRunner.h" #include "src/pages/batchconvert/batchconvertpage.h" +#include "src/pages/codesearch/codesearchpage.h" #include "src/pages/functionsearch/functionsearchpage.h" #include "src/pages/functionsearch/uf20functionsearchpage.h" #include "src/pages/functionsearch/uft3functionsearchpage.h" @@ -30,6 +31,7 @@ private: void initContent(); QWidget* createBatchConvertPage(); + QWidget* createCodeSearchPage(); QWidget* createFunctionSearchPage(); QWidget* createUF20FunctionSearchPage(); QWidget* createUFT3FunctionSearchPage(); @@ -58,9 +60,11 @@ private: ElaToolButton* m_navigationButton; bool m_isNavigationExpanded; BatchConvertPage *m_batchConvertPage; + CodeSearchPage *_codeSearchPage; int m_totalFunctions; int m_currentFunctionIndex; + int m_lastProgress; // 已上报的最大进度,保证进度单调递增,避免进度条来回跳动 QStringList m_functionList; QString m_helpKey; QString m_aboutKey; diff --git a/src/metadataupdate/metadataprocessor.cpp b/src/metadataupdate/metadataprocessor.cpp index 003e6fb..2c176cf 100644 --- a/src/metadataupdate/metadataprocessor.cpp +++ b/src/metadataupdate/metadataprocessor.cpp @@ -13,13 +13,20 @@ #include "utils/configmanager.h" #include #include +#include #include #include +#include MetadataProcessor::MetadataProcessor(QObject *parent) : QObject(parent) { } +void MetadataProcessor::setForceFull(bool force) +{ + m_forceFull = force; +} + void MetadataProcessor::initConfig(const QString& basePath) { m_basePath = basePath; @@ -181,107 +188,194 @@ void MetadataProcessor::setBasePath(const QString& path) m_basePath = path; } +QJsonObject MetadataProcessor::interfaceInfoToUF2Json(const InterfaceInfo& info) +{ + QJsonObject interfaceJson; + interfaceJson["cname"] = info.cname; + interfaceJson["eName"] = info.eName; + interfaceJson["functionNo"] = info.functionNo; + interfaceJson["flag"] = info.flag; + interfaceJson["path"] = info.path; + interfaceJson["code"] = info.code; + interfaceJson["moudle"] = info.moudle; + interfaceJson["moudleEName"] = info.moudleEName; + interfaceJson["serverType"] = info.serverType; + interfaceJson["returnResultSet"] = info.returnResultSet; + interfaceJson["needTransMonitor"] = info.needTransMonitor; + interfaceJson["checkLicence"] = info.checkLicence; + + QJsonObject inputFieldsJson; + for (const QString& fieldName : info.inputFields.keys()) { + const FieldInfo& field = info.inputFields[fieldName]; + QJsonObject fieldJson; + fieldJson["name"] = field.name; + fieldJson["flag"] = field.flag; + fieldJson["desc"] = field.desc; + inputFieldsJson[fieldName] = fieldJson; + } + interfaceJson["inputFileds"] = inputFieldsJson; + + QJsonObject outputFieldsJson; + for (const QString& fieldName : info.outputFields.keys()) { + const FieldInfo& field = info.outputFields[fieldName]; + QJsonObject fieldJson; + fieldJson["name"] = field.name; + fieldJson["flag"] = field.flag; + fieldJson["desc"] = field.desc; + outputFieldsJson[fieldName] = fieldJson; + } + interfaceJson["outputFileds"] = outputFieldsJson; + + QJsonObject variableFieldsJson; + for (const QString& fieldName : info.variableFields.keys()) { + const FieldInfo& field = info.variableFields[fieldName]; + QJsonObject fieldJson; + fieldJson["name"] = field.name; + fieldJson["cname"] = field.cname; + fieldJson["hsType"] = field.hsType; + fieldJson["desc"] = field.desc; + variableFieldsJson[fieldName] = fieldJson; + } + interfaceJson["variableFileds"] = variableFieldsJson; + + return interfaceJson; +} + +QJsonObject MetadataProcessor::readCache() +{ + QString cachePath = QDir(m_uf2touft3Path).filePath("metadata_cache.json"); + if (!QFile::exists(cachePath)) { + return QJsonObject(); + } + return readJson(cachePath); +} + +bool MetadataProcessor::writeCache(const QJsonObject& cache) +{ + QString cachePath = QDir(m_uf2touft3Path).filePath("metadata_cache.json"); + return writeJson(cachePath, cache); +} + bool MetadataProcessor::processUf2Interfaces(const QString& basePath) { LogManager::instance()->log("========== 开始处理 UF2.0 接口 =========="); LogManager::instance()->log(QString("基础路径(basePath): %1").arg(basePath)); - - QString configPath = QDir(m_uf2touft3Path).filePath("config.ini"); - LogManager::instance()->log(QString("配置文件路径: %1").arg(configPath)); - + QStringList completeNameList = {"module.xml", ".project", "冲突检测.wlua"}; QStringList containNameList = {".vbusiness", "/公共资源/", "/数据库/", "/脚本/", "/通用数据/", "/.svn/"}; - + QMap filterDict; filterDict["completeNameList"] = completeNameList; filterDict["containNameList"] = containNameList; - + QString dirPath = m_uf20Path; QString ufAcct20ProjectDir = m_ufAcct20Path; QString uftDbSett = m_uftDbSettPath; - + QString supportUftdbSett = m_supportUftdbSett; if (supportUftdbSett == "0") { uftDbSett.clear(); } - + LogManager::instance()->log(QString("UF20 项目路径: %1").arg(dirPath)); LogManager::instance()->log(QString("UF20 会计项目路径: %1").arg(ufAcct20ProjectDir)); LogManager::instance()->log(QString("UFT 数据库设置路径: %1").arg(uftDbSett)); - + QString custJsonPath = QDir(m_uf2touft3Path).filePath("cust.json"); - LogManager::instance()->log(QString("客户配置文件路径: %1").arg(custJsonPath)); QJsonObject custJson = readJson(custJsonPath); - - Uf2Interface uf2; - QMap resultMap = uf2.scanDir(dirPath, ufAcct20ProjectDir, uftDbSett, filterDict, custJson); - - QJsonObject resultJson; - for (const QString& key : resultMap.keys()) { - const InterfaceInfo& info = resultMap[key]; - - QJsonObject interfaceJson; - interfaceJson["cname"] = info.cname; - interfaceJson["eName"] = info.eName; - interfaceJson["functionNo"] = info.functionNo; - interfaceJson["flag"] = info.flag; - interfaceJson["path"] = info.path; - interfaceJson["code"] = info.code; - interfaceJson["moudle"] = info.moudle; - interfaceJson["moudleEName"] = info.moudleEName; - interfaceJson["serverType"] = info.serverType; - interfaceJson["returnResultSet"] = info.returnResultSet; - interfaceJson["needTransMonitor"] = info.needTransMonitor; - interfaceJson["checkLicence"] = info.checkLicence; - - QJsonObject inputFieldsJson; - for (const QString& fieldName : info.inputFields.keys()) { - const FieldInfo& field = info.inputFields[fieldName]; - QJsonObject fieldJson; - fieldJson["name"] = field.name; - fieldJson["flag"] = field.flag; - fieldJson["desc"] = field.desc; - inputFieldsJson[fieldName] = fieldJson; - } - interfaceJson["inputFileds"] = inputFieldsJson; - - QJsonObject outputFieldsJson; - for (const QString& fieldName : info.outputFields.keys()) { - const FieldInfo& field = info.outputFields[fieldName]; - QJsonObject fieldJson; - fieldJson["name"] = field.name; - fieldJson["flag"] = field.flag; - fieldJson["desc"] = field.desc; - outputFieldsJson[fieldName] = fieldJson; - } - interfaceJson["outputFileds"] = outputFieldsJson; - - QJsonObject variableFieldsJson; - for (const QString& fieldName : info.variableFields.keys()) { - const FieldInfo& field = info.variableFields[fieldName]; - QJsonObject fieldJson; - fieldJson["name"] = field.name; - fieldJson["cname"] = field.cname; - fieldJson["hsType"] = field.hsType; - fieldJson["desc"] = field.desc; - variableFieldsJson[fieldName] = fieldJson; - } - interfaceJson["variableFileds"] = variableFieldsJson; - - resultJson[key] = interfaceJson; - } - + QString outputPath = QDir(m_uf2touft3Path).filePath("uf2.json"); - + + Uf2Interface uf2; + QJsonObject oldJson = readJson(outputPath); + QJsonObject oldCache = readCache(); + + // 仅收集文件路径与修改时间(不解析内容),用于增量判定 + QList entries; + entries << uf2.collectFiles(dirPath, "trade", filterDict); + if (!ufAcct20ProjectDir.isEmpty()) entries << uf2.collectFiles(ufAcct20ProjectDir, "acct", filterDict); + if (!uftDbSett.isEmpty()) entries << uf2.collectFiles(uftDbSett, "dbsett", filterDict); + + QMap moduleCodeMap = uf2.buildModuleCodeMap(dirPath, ufAcct20ProjectDir, uftDbSett, custJson); + + bool full = m_forceFull || oldJson.isEmpty() + || !oldCache.contains("uf2") || oldCache["uf2"].toObject().isEmpty(); + + QJsonObject resultJson; + QJsonObject newUf2Cache; + + if (full) { + LogManager::instance()->log("UF2.0 全量扫描模式"); + QElapsedTimer timer; + timer.start(); + + QMap mtimeByPath; + for (const Uf2FileEntry& e : entries) mtimeByPath[e.path] = e.mtime; + + QMap resultMap = uf2.scanDir(dirPath, ufAcct20ProjectDir, uftDbSett, filterDict, custJson); + for (const QString& key : resultMap.keys()) { + const InterfaceInfo& info = resultMap[key]; + resultJson[key] = interfaceInfoToUF2Json(info); + QJsonObject rec; + rec["mtime"] = QString::number(mtimeByPath.value(info.path, 0)); + rec["key"] = key; + newUf2Cache[info.path] = rec; + } + + LogManager::instance()->log(QString("UF2.0 全量扫描耗时: %1ms").arg(timer.elapsed())); + } else { + LogManager::instance()->log("UF2.0 增量扫描模式"); + QElapsedTimer timer; + timer.start(); + QJsonObject oldUf2 = oldCache["uf2"].toObject(); + QSet currentPaths; + resultJson = oldJson; // 以旧结果为基准,仅更新变化的文件 + + for (const Uf2FileEntry& e : entries) { + currentPaths.insert(e.path); + QJsonObject rec = oldUf2.value(e.path).toObject(); + bool changed = rec.isEmpty() || rec["mtime"].toString().toLongLong() != e.mtime; + if (changed) { + InterfaceInfo info = uf2.assemble(e.path, e.serverType, moduleCodeMap); + QString newKey = info.cname; + QString oldKey = rec["key"].toString(); + if (!oldKey.isEmpty() && oldKey != newKey && resultJson.contains(oldKey)) { + resultJson.remove(oldKey); + } + resultJson[newKey] = interfaceInfoToUF2Json(info); + QJsonObject crec; + crec["mtime"] = QString::number(e.mtime); + crec["key"] = newKey; + newUf2Cache[e.path] = crec; + } else { + newUf2Cache[e.path] = rec; // 未变化:直接沿用旧缓存记录 + } + } + + // 删除已从磁盘移除的文件对应的条目 + for (auto it = oldUf2.begin(); it != oldUf2.end(); ++it) { + if (!currentPaths.contains(it.key())) { + QString oldKey = it.value().toObject()["key"].toString(); + if (!oldKey.isEmpty() && resultJson.contains(oldKey)) { + resultJson.remove(oldKey); + } + } + } + + LogManager::instance()->log(QString("UF2.0 增量扫描耗时: %1ms").arg(timer.elapsed())); + } + if (!writeJson(outputPath, resultJson)) { return false; } - - LogManager::instance()->log(QString("UF2.0 接口处理完成,结果已保存到: %1").arg(outputPath)); - if (!resultMap.isEmpty()) { - QString firstKey = resultMap.keys().first(); + + QJsonObject merged = oldCache; + merged["uf2"] = newUf2Cache; + if (!writeCache(merged)) { + LogManager::instance()->logWarning("UF2.0 缓存写入失败(不影响本次结果)"); } - + + LogManager::instance()->log(QString("UF2.0 接口处理完成,结果已保存到: %1").arg(outputPath)); return true; } @@ -289,62 +383,148 @@ bool MetadataProcessor::processUft3Interfaces(const QString& basePath) { LogManager::instance()->log("========== 开始处理 UFT3.0 接口 =========="); LogManager::instance()->log(QString("基础路径(basePath): %1").arg(basePath)); - - QString configPath = QDir(m_uf2touft3Path).filePath("config.ini"); - LogManager::instance()->log(QString("配置文件路径: %1").arg(configPath)); - + QString moduleGenerationCheck = m_moduleGenerationCheck; LogManager::instance()->log(QString("模块生成检查: %1").arg(moduleGenerationCheck)); - + QStringList completeNameList = {"module.xml", "project.xml", "冲突检测.wlua"}; QStringList containNameList = {".extinterface", ".uftatomfunction", ".uftatomservice", ".uftfunction", ".uftservice"}; - + QMap filterDict; filterDict["completeNameList"] = completeNameList; filterDict["containNameList"] = containNameList; - + QString dirPath = m_uft30Path; LogManager::instance()->log(QString("UFT30 项目路径: %1").arg(dirPath)); - - Uft3Interface uft3; - auto result = uft3.scanDir(dirPath, filterDict, moduleGenerationCheck); - - QMap resultMap = result.first; - QMap transCodeMap = result.second; - - - QJsonObject resultJson; - for (const QString& key : resultMap.keys()) { - const InterfaceInfo& info = resultMap[key]; - resultJson[key] = interfaceInfoToUFT3Json(info); - } - + QString outputPath = QDir(m_uf2touft3Path).filePath("uft3.json"); - + QString transOutputPath = QDir(m_uf2touft3Path).filePath("uft3Trans.json"); + + Uft3Interface uft3; + QJsonObject oldMainJson = readJson(outputPath); + QJsonObject oldTransJson = readJson(transOutputPath); + QJsonObject oldCache = readCache(); + + QList entries = uft3.collectFiles(dirPath, filterDict, moduleGenerationCheck); + + bool full = m_forceFull || oldMainJson.isEmpty() + || !oldCache.contains("uft3") || oldCache["uft3"].toObject().isEmpty(); + + QJsonObject resultJson; + QJsonObject transCodeJson; + QJsonObject newUft3Cache; + + if (full) { + LogManager::instance()->log("UFT3.0 全量扫描模式"); + QElapsedTimer timer; + timer.start(); + + QMap mtimeByPath; + for (const Uft3FileEntry& e : entries) mtimeByPath[e.path] = e.mtime; + + auto result = uft3.scanDir(dirPath, filterDict, moduleGenerationCheck); + QMap resultMap = result.first; + QMap transCodeMap = result.second; + + for (const QString& key : resultMap.keys()) { + const InterfaceInfo& info = resultMap[key]; + resultJson[key] = interfaceInfoToUFT3Json(info); + QJsonObject rec; + rec["mtime"] = QString::number(mtimeByPath.value(info.path, 0)); + rec["key"] = key; + rec["trans"] = false; + newUft3Cache[info.path] = rec; + } + for (const QString& key : transCodeMap.keys()) { + const InterfaceInfo& info = transCodeMap[key]; + transCodeJson[key] = interfaceInfoToUFT3Json(info); + QJsonObject rec; + rec["mtime"] = QString::number(mtimeByPath.value(info.path, 0)); + rec["key"] = key; + rec["trans"] = true; + newUft3Cache[info.path] = rec; + } + + LogManager::instance()->log(QString("UFT3.0 全量扫描耗时: %1ms").arg(timer.elapsed())); + } else { + LogManager::instance()->log("UFT3.0 增量扫描模式"); + QElapsedTimer timer; + timer.start(); + QJsonObject oldUft3 = oldCache["uft3"].toObject(); + QSet currentPaths; + resultJson = oldMainJson; // 以旧结果为基准 + transCodeJson = oldTransJson; + + for (const Uft3FileEntry& e : entries) { + currentPaths.insert(e.path); + QJsonObject rec = oldUft3.value(e.path).toObject(); + bool changed = rec.isEmpty() || rec["mtime"].toString().toLongLong() != e.mtime; + if (changed) { + InterfaceInfo info = uft3.assemble(e.path); + QString newKey = info.cname; + QString oldKey = rec["key"].toString(); + bool oldTrans = rec["trans"].toBool(); + bool newTrans = e.isTrans; + // 若文件归属(主接口/交易码)或名称发生变化,先移除旧条目 + if (!oldKey.isEmpty()) { + if (oldTrans) { + if (transCodeJson.contains(oldKey)) transCodeJson.remove(oldKey); + } else { + if (resultJson.contains(oldKey)) resultJson.remove(oldKey); + } + } + QJsonObject val = interfaceInfoToUFT3Json(info); + if (newTrans) transCodeJson[newKey] = val; + else resultJson[newKey] = val; + QJsonObject crec; + crec["mtime"] = QString::number(e.mtime); + crec["key"] = newKey; + crec["trans"] = newTrans; + newUft3Cache[e.path] = crec; + } else { + newUft3Cache[e.path] = rec; + } + } + + // 删除已从磁盘移除的文件对应的条目 + for (auto it = oldUft3.begin(); it != oldUft3.end(); ++it) { + if (!currentPaths.contains(it.key())) { + QJsonObject rec = it.value().toObject(); + QString oldKey = rec["key"].toString(); + bool oldTrans = rec["trans"].toBool(); + if (oldTrans) { + if (transCodeJson.contains(oldKey)) transCodeJson.remove(oldKey); + } else { + if (resultJson.contains(oldKey)) resultJson.remove(oldKey); + } + } + } + + LogManager::instance()->log(QString("UFT3.0 增量扫描耗时: %1ms").arg(timer.elapsed())); + } + if (!writeJson(outputPath, resultJson)) { return false; } - - QJsonObject transCodeJson; - for (const QString& key : transCodeMap.keys()) { - const InterfaceInfo& info = transCodeMap[key]; - transCodeJson[key] = interfaceInfoToUFT3Json(info); - } - - QString transOutputPath = QDir(m_uf2touft3Path).filePath("uft3Trans.json"); - if (!writeJson(transOutputPath, transCodeJson)) { return false; } - + + QJsonObject merged = oldCache; + merged["uft3"] = newUft3Cache; + if (!writeCache(merged)) { + LogManager::instance()->logWarning("UFT3.0 缓存写入失败(不影响本次结果)"); + } + LogManager::instance()->log(QString("UFT3.0 接口处理完成,结果已保存到: %1").arg(outputPath)); - return true; } bool MetadataProcessor::processUf2DataType() { LogManager::instance()->log("========== 开始处理 UF2.0 数据类型 =========="); + QElapsedTimer timer; + timer.start(); QString dirPath = m_uf20Path; @@ -364,10 +544,11 @@ bool MetadataProcessor::processUf2DataType() QString outputPath = QDir(m_uf2touft3Path).filePath("uf2dataType.json"); if (!writeJson(outputPath, resultJson)) { + LogManager::instance()->log(QString("UF2.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed())); return false; } - LogManager::instance()->log(QString("UF2.0 数据类型处理完成,结果已保存到: %1").arg(outputPath)); + LogManager::instance()->log(QString("UF2.0 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); return true; } @@ -375,6 +556,8 @@ bool MetadataProcessor::processUf2DataType() bool MetadataProcessor::processUf2StdFields() { LogManager::instance()->log("========== 开始处理 UF2.0 标准字段 =========="); + QElapsedTimer timer; + timer.start(); QString dirPath = m_uf20Path; @@ -406,10 +589,11 @@ bool MetadataProcessor::processUf2StdFields() QString outputPath = QDir(m_uf2touft3Path).filePath("uf2stdfield.json"); if (!writeJson(outputPath, resultJson)) { + LogManager::instance()->log(QString("UF2.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed())); return false; } - LogManager::instance()->log(QString("UF2.0 标准字段处理完成,结果已保存到: %1").arg(outputPath)); + LogManager::instance()->log(QString("UF2.0 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); return true; } @@ -417,6 +601,8 @@ bool MetadataProcessor::processUf2StdFields() bool MetadataProcessor::processUf2ErrNumber() { LogManager::instance()->log("========== 开始处理 UF2.0 标准错误号 =========="); + QElapsedTimer timer; + timer.start(); QString dirPath = m_uf20Path; @@ -436,10 +622,11 @@ bool MetadataProcessor::processUf2ErrNumber() QString outputPath = QDir(m_uf2touft3Path).filePath("uf2Error.json"); if (!writeJson(outputPath, resultJson)) { + LogManager::instance()->log(QString("UF2.0 标准错误号处理失败,耗时: %1ms").arg(timer.elapsed())); return false; } - LogManager::instance()->log(QString("UF2.0 标准错误号处理完成,结果已保存到: %1").arg(outputPath)); + LogManager::instance()->log(QString("UF2.0 标准错误号处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); return true; } @@ -447,6 +634,8 @@ bool MetadataProcessor::processUf2ErrNumber() bool MetadataProcessor::processUft3DataType() { LogManager::instance()->log("========== 开始处理 UFT3.0 数据类型 =========="); + QElapsedTimer timer; + timer.start(); QString dirPath = m_uft30PubPath; @@ -466,10 +655,11 @@ bool MetadataProcessor::processUft3DataType() QString outputPath = QDir(m_uf2touft3Path).filePath("uft3datatype.json"); if (!writeJson(outputPath, resultJson)) { + LogManager::instance()->log(QString("UFT3.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed())); return false; } - LogManager::instance()->log(QString("UFT3.0 数据类型处理完成,结果已保存到: %1").arg(outputPath)); + LogManager::instance()->log(QString("UFT3.0 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); return true; } @@ -477,6 +667,8 @@ bool MetadataProcessor::processUft3DataType() bool MetadataProcessor::processUft3StdFields() { LogManager::instance()->log("========== 开始处理 UFT3.0 标准字段 =========="); + QElapsedTimer timer; + timer.start(); QString dirPath = m_uft30PubPath; @@ -508,10 +700,11 @@ bool MetadataProcessor::processUft3StdFields() QString outputPath = QDir(m_uf2touft3Path).filePath("uft3stdfields.json"); if (!writeJson(outputPath, resultJson)) { + LogManager::instance()->log(QString("UFT3.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed())); return false; } - LogManager::instance()->log(QString("UFT3.0 标准字段处理完成,结果已保存到: %1").arg(outputPath)); + LogManager::instance()->log(QString("UFT3.0 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); return true; } @@ -519,6 +712,8 @@ bool MetadataProcessor::processUft3StdFields() bool MetadataProcessor::processUft3Table() { LogManager::instance()->log("========== 开始处理 UFT3.0 表结构 =========="); + QElapsedTimer timer; + timer.start(); QString dirPath = m_uft30PubPath; @@ -541,10 +736,11 @@ bool MetadataProcessor::processUft3Table() QString outputPath = QDir(m_uf2touft3Path).filePath("uft3Table.json"); if (!writeJson(outputPath, resultJson)) { + LogManager::instance()->log(QString("UFT3.0 表结构处理失败,耗时: %1ms").arg(timer.elapsed())); return false; } - LogManager::instance()->log(QString("UFT3.0 表结构处理完成,结果已保存到: %1").arg(outputPath)); + LogManager::instance()->log(QString("UFT3.0 表结构处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); return true; } \ No newline at end of file diff --git a/src/metadataupdate/metadataprocessor.h b/src/metadataupdate/metadataprocessor.h index affacd5..07da95c 100644 --- a/src/metadataupdate/metadataprocessor.h +++ b/src/metadataupdate/metadataprocessor.h @@ -17,7 +17,9 @@ public: bool processUf2Interfaces(const QString& basePath); bool processUft3Interfaces(const QString& basePath); - + + void setForceFull(bool force); // 强制全量更新(兼容 UI;原版每次均全量解析,无增量缓存时不影响行为) + bool processUf2StdFields(); bool processUf2DataType(); bool processUf2ErrNumber(); @@ -38,6 +40,11 @@ private: bool writeJson(const QString& filePath, const QJsonObject& jsonObject); QJsonObject readJson(const QString& filePath); QJsonObject interfaceInfoToUFT3Json(const InterfaceInfo& info); + QJsonObject interfaceInfoToUF2Json(const InterfaceInfo& info); + + // 轻量增量缓存(仅记录 文件路径->修改时间(mtime)+该文件在 JSON 中的 key) + QJsonObject readCache(); + bool writeCache(const QJsonObject& cache); QString m_basePath; QString m_uf2touft3Path; @@ -51,6 +58,8 @@ private: QString m_supportUftdbSett; QString m_moduleGenerationCheck; + + bool m_forceFull = false; // 强制全量更新标志(兼容 UI;原版每次均全量解析,无增量缓存时不影响行为) }; #endif // METADATAPROCESSOR_H \ No newline at end of file diff --git a/src/metadataupdate/uf2interface.cpp b/src/metadataupdate/uf2interface.cpp index dd8956a..d6d382f 100644 --- a/src/metadataupdate/uf2interface.cpp +++ b/src/metadataupdate/uf2interface.cpp @@ -6,6 +6,7 @@ #include #include #include +#include Uf2Interface::Uf2Interface(QObject *parent) : QObject(parent) { @@ -294,3 +295,97 @@ QMap Uf2Interface::scanDir(const QString& dirPath, return resultMap; } + +QMap Uf2Interface::buildModuleCodeMap(const QString& dirPath, + const QString& ufAcct20ProjectDir, + const QString& uftDbSett, + const QJsonObject& custJson) +{ + QMap moduleCodeMap; + moduleCodeMap["转融通"] = "ref"; + + // 预提取cust.json中的模块英文名映射 + QMap custMoudleENameMap; + if (!custJson.isEmpty() && custJson.contains("moudleEName")) { + QJsonObject moudleENameObj = custJson["moudleEName"].toObject(); + for (const QString& key : moudleENameObj.keys()) { + custMoudleENameMap[key] = moudleENameObj[key].toString(); + } + } + + std::function scanModuleXmlRecursive = [&](const QString& path) { + if (path.isEmpty()) return; + QDir dir(path); + QStringList filters; + filters << "module.xml"; + QFileInfoList fileList = dir.entryInfoList(filters, QDir::Files); + for (const QFileInfo& fileInfo : fileList) { + QString filePath = fileInfo.absoluteFilePath(); + filePath.replace('\\', '/'); + if (filePath.contains("/数据库/")) continue; + QString modName = fileInfo.dir().dirName(); + QString modEName = getModuleEName(filePath); + if (filePath.contains("外部接口(不用编译)") && modEName.trimmed().isEmpty()) { + if (custMoudleENameMap.contains(modName)) modEName = custMoudleENameMap[modName]; + } + moduleCodeMap[modName] = modEName; + } + QFileInfoList dirList = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo& dirInfo : dirList) scanModuleXmlRecursive(dirInfo.absoluteFilePath()); + }; + scanModuleXmlRecursive(dirPath); + scanModuleXmlRecursive(ufAcct20ProjectDir); + scanModuleXmlRecursive(uftDbSett); + return moduleCodeMap; +} + +InterfaceInfo Uf2Interface::assemble(const QString& filePath, + const QString& serverType, + const QMap& moduleCodeMap) +{ + QString sFilePath = filePath; + sFilePath.replace('\\', '/'); + int lastSlash = sFilePath.lastIndexOf('/'); + QString pathWithoutFile = sFilePath.left(lastSlash); + int secondLastSlash = pathWithoutFile.lastIndexOf('/'); + QString pathWithoutLastDir = pathWithoutFile.left(secondLastSlash); + int thirdLastSlash = pathWithoutLastDir.lastIndexOf('/'); + QString modName = pathWithoutLastDir.mid(thirdLastSlash + 1); + + InterfaceInfo interfaceInfo = loadInterface(filePath); + interfaceInfo.moudle = modName; + interfaceInfo.moudleEName = moduleCodeMap.value(modName); + interfaceInfo.serverType = serverType; + return interfaceInfo; +} + +QList Uf2Interface::collectFiles(const QString& dirPath, + const QString& serverType, + const QMap& fileFilterDict) +{ + QList result; + QStringList completeNameList = fileFilterDict.value("completeNameList"); + QStringList containNameList = fileFilterDict.value("containNameList"); + + std::function scanInterfaceFilesRecursive = + [&](const QString& path, const QString& st) { + if (path.isEmpty()) return; + QDir dir(path); + if (!dir.exists()) return; + QFileInfoList fileList = dir.entryInfoList(QStringList() << "*.*", QDir::Files); + for (const QFileInfo& fileInfo : fileList) { + QString filePath = fileInfo.absoluteFilePath(); + filePath.replace('\\', '/'); + if (shouldFilter(filePath, completeNameList, containNameList)) continue; + Uf2FileEntry entry; + entry.path = filePath; + entry.serverType = st; + entry.mtime = fileInfo.lastModified().toMSecsSinceEpoch(); + result.append(entry); + } + QFileInfoList dirList = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo& dirInfo : dirList) scanInterfaceFilesRecursive(dirInfo.absoluteFilePath(), st); + }; + scanInterfaceFilesRecursive(dirPath, serverType); + return result; +} diff --git a/src/metadataupdate/uf2interface.h b/src/metadataupdate/uf2interface.h index e118abb..c5790f1 100644 --- a/src/metadataupdate/uf2interface.h +++ b/src/metadataupdate/uf2interface.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -41,6 +42,13 @@ struct InterfaceInfo { QMap internalFields; }; +// 仅记录文件路径、归属类型、修改时间,用于轻量增量缓存 +struct Uf2FileEntry { + QString path; + QString serverType; // trade / acct / dbsett + qint64 mtime = 0; +}; + class Uf2Interface : public QObject { Q_OBJECT @@ -54,6 +62,18 @@ public: const QMap& fileFilterDict, const QJsonObject& custJson = QJsonObject()); + // 以下为增量更新提供复用能力(与 scanDir 内部逻辑一致) + QMap buildModuleCodeMap(const QString& dirPath, + const QString& ufAcct20ProjectDir, + const QString& uftDbSett, + const QJsonObject& custJson = QJsonObject()); + InterfaceInfo assemble(const QString& filePath, + const QString& serverType, + const QMap& moduleCodeMap); + QList collectFiles(const QString& dirPath, + const QString& serverType, + const QMap& fileFilterDict); + private: QString getModuleEName(const QString& moduleXmlPath); bool shouldFilter(const QString& filePath, diff --git a/src/metadataupdate/uft3interface.cpp b/src/metadataupdate/uft3interface.cpp index 2ee07d7..33e7e73 100644 --- a/src/metadataupdate/uft3interface.cpp +++ b/src/metadataupdate/uft3interface.cpp @@ -6,6 +6,7 @@ #include #include #include +#include Uft3Interface::Uft3Interface(QObject *parent) : QObject(parent) { @@ -243,3 +244,57 @@ Uft3Interface::scanDir(const QString& dirPath, return qMakePair(resultMap, transCodeMap); } + +InterfaceInfo Uft3Interface::assemble(const QString& filePath) +{ + QFileInfo fileInfo(filePath); + fileInfo.setFile(fileInfo.absoluteFilePath()); // 标准化路径 + QString modName = fileInfo.dir().dirName(); + InterfaceInfo interfaceInfo = loadInterface(filePath); + interfaceInfo.moudle = modName; + return interfaceInfo; +} + +QList Uft3Interface::collectFiles(const QString& dirPath, + const QMap& fileFilterDict, + const QString& moduleGenerationCheck) +{ + QList result; + QStringList completeNameList = fileFilterDict.value("completeNameList"); + QStringList containNameList = fileFilterDict.value("containNameList"); + + std::function scanInterfaceFilesRecursive = + [&](const QString& path) { + if (path.isEmpty()) + return; + + QDir dir(path); + QFileInfoList fileList = dir.entryInfoList(QStringList() << "*.*", QDir::Files); + for (const QFileInfo& fileInfo : fileList) { + QString filePath = fileInfo.absoluteFilePath(); + filePath.replace('\\', '/'); + + if (shouldFilter(filePath, dirPath, completeNameList, containNameList, moduleGenerationCheck)) { + continue; + } + + bool isTransCode = (filePath.contains("/cbptrans/") || filePath.contains("/mgrcbptrans/") + || filePath.contains("/converttrans/") || filePath.contains("/ses/")) + && !filePath.contains("/cbptrans/cbptranspub/"); + + Uft3FileEntry entry; + entry.path = filePath; + entry.mtime = fileInfo.lastModified().toMSecsSinceEpoch(); + entry.isTrans = isTransCode; + result.append(entry); + } + + QFileInfoList dirList = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo& dirInfo : dirList) { + scanInterfaceFilesRecursive(dirInfo.absoluteFilePath()); + } + }; + + scanInterfaceFilesRecursive(dirPath); + return result; +} diff --git a/src/metadataupdate/uft3interface.h b/src/metadataupdate/uft3interface.h index b7bb69a..9e056eb 100644 --- a/src/metadataupdate/uft3interface.h +++ b/src/metadataupdate/uft3interface.h @@ -6,9 +6,17 @@ #include #include #include +#include #include "uf2interface.h" +// 仅记录文件路径、修改时间、是否属于交易码,用于轻量增量缓存 +struct Uft3FileEntry { + QString path; + qint64 mtime = 0; + bool isTrans = false; +}; + class Uft3Interface : public QObject { Q_OBJECT @@ -21,6 +29,12 @@ public: const QMap& fileFilterDict, const QString& moduleGenerationCheck); + // 以下为增量更新提供复用能力(与 scanDir 内部逻辑一致) + InterfaceInfo assemble(const QString& filePath); + QList collectFiles(const QString& dirPath, + const QMap& fileFilterDict, + const QString& moduleGenerationCheck); + private: bool shouldFilter(const QString& filePath, const QString& dirPath, diff --git a/src/pages/about/aboutdialog.cpp b/src/pages/about/aboutdialog.cpp index a65818a..a602b4e 100644 --- a/src/pages/about/aboutdialog.cpp +++ b/src/pages/about/aboutdialog.cpp @@ -12,7 +12,7 @@ AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent) { - setFixedSize(354, 180); + setFixedSize(360, 210); setWindowTitle("关于"); setStandardButtons(QDialogButtonBox::Ok); @@ -34,7 +34,7 @@ AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent) QFont nameTextFont = nameText->font(); nameTextFont.setWeight(QFont::Bold); nameText->setFont(nameTextFont); - nameText->setTextPixelSize(17); + nameText->setTextPixelSize(15); nameText->setText("UFT30 ChangeCode"); auto* versionText = new ElaText(this); diff --git a/src/pages/batchconvert/batchconvertpage.cpp b/src/pages/batchconvert/batchconvertpage.cpp index 8a2a142..79b8eb1 100644 --- a/src/pages/batchconvert/batchconvertpage.cpp +++ b/src/pages/batchconvert/batchconvertpage.cpp @@ -151,12 +151,15 @@ void BatchConvertPage::initUI() void BatchConvertPage::onProgressUpdate(int progress, const QString& status) { m_progressBar->setValue(progress); - - QString displayStatus = status; - if (displayStatus.length() > 80) { - displayStatus = displayStatus.left(80) + "..."; + + // 仅当传入有效状态文字时才更新标签,避免无意义的日志行覆盖当前处理状态 + if (!status.isEmpty()) { + QString displayStatus = status; + if (displayStatus.length() > 80) { + displayStatus = displayStatus.left(80) + "..."; + } + m_progressLabel->setText(displayStatus); } - m_progressLabel->setText(displayStatus); } bool BatchConvertPage::checkFunctionExists(const QString &funcName) diff --git a/src/pages/codesearch/codesearchpage.cpp b/src/pages/codesearch/codesearchpage.cpp new file mode 100644 index 0000000..89e75a2 --- /dev/null +++ b/src/pages/codesearch/codesearchpage.cpp @@ -0,0 +1,1012 @@ +#include "codesearchpage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ElaText.h" +#include "utils/datacache.h" +#include "utils/logmanager.h" +#include "utils/Constants.h" +#include + +namespace { + QStringList loadConvertFunctions() + { + QStringList result; + const QString custPath = Constants::custJson(); + + QFile file(custPath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return result; + } + + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + if (!doc.isObject()) { + return result; + } + + QJsonObject convertObj = doc.object().value("convert").toObject(); + QJsonArray dirArray = convertObj.value("dir").toArray(); + QJsonArray extArray = convertObj.value("ext").toArray(); + + QStringList extList; + for (const QJsonValue &v : extArray) { + extList.append(v.toString()); + } + + for (const QJsonValue &v : dirArray) { + QString name = v.toString(); + for (const QString &ext : extList) { + if (name.endsWith(ext)) { + name.chop(ext.length()); + break; + } + } + if (!name.isEmpty()) { + result.append(name); + } + } + + return result; + } + + // "原代码已支持"行高亮角色。Ela 主题会覆盖 item 的 BackgroundRole, + // 因此用自定义 delegate 强制绘制绿色背景,保证两种模式都生效。 + const int RowHighlightRole = Qt::UserRole + 100; + // 当前查询的函数/功能行高亮角色:强制绘制蓝色背景,突出查询节点 + const int RowHighlightBlueRole = Qt::UserRole + 101; + + class CallTreeDelegate : public QStyledItemDelegate + { + public: + explicit CallTreeDelegate(QObject *parent = nullptr) + : QStyledItemDelegate(parent) {} + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + const bool blue = index.data(RowHighlightBlueRole).toBool(); + const bool green = index.data(RowHighlightRole).toBool(); + if (blue || green) { + // 高亮行:直接绘制背景 + 深字(支持节点为叶子,无展开箭头,可整格重绘) + const QColor bg = blue ? QColor(189, 215, 238) : QColor(198, 239, 206); + const QColor fg = blue ? QColor(10, 60, 120) : QColor(0, 97, 0); + painter->save(); + painter->fillRect(option.rect, bg); + painter->setPen(fg); + // 第0列(函数名)左对齐并留出图标位置;"目录"、"模块"列居中 + QRect textRect = option.rect; + int align = Qt::AlignCenter; + if (index.column() == 0) { + textRect = option.rect.adjusted( + option.decorationSize.width() + 4, 0, 0, 0); + align = Qt::AlignLeft | Qt::AlignVCenter; + } + painter->drawText(textRect, align, + index.data(Qt::DisplayRole).toString()); + painter->restore(); + return; + } + QStyledItemDelegate::paint(painter, option, index); + } + }; +} + +CodeSearchPage::CodeSearchPage(QWidget *parent) + : QWidget(parent) + , m_analyzers{new CallChainAnalyzer(this), new CallChainAnalyzer(this)} +{ + // 两种模式的代码检索路径:优先读取 change_code.ini 配置;为空(键不存在或值为空)时回退到默认路径 + // (不使用 config.ini,避免程序更新被覆盖导致配置丢失) + const QString iniPath = Constants::changeCodeIni(); + QSettings cc(iniPath, QSettings::IniFormat); + QString convertPath = cc.value(Constants::CodeSearch::ConvertPathKey).toString(); + QString searchPath = cc.value(Constants::CodeSearch::SearchPathKey).toString(); + if (convertPath.isEmpty()) { + convertPath = Constants::changeCodeDir(); + } + if (searchPath.isEmpty()) { + searchPath = Constants::changeCodeDir(); + } + m_pathByMode[ConvertMode] = convertPath; + m_pathByMode[SearchMode] = searchPath; + // 将(可能为默认的)路径写回配置,确保空配置也会被落盘,下次直接命中 + cc.setValue(Constants::CodeSearch::ConvertPathKey, QDir::fromNativeSeparators(convertPath)); + cc.setValue(Constants::CodeSearch::SearchPathKey, QDir::fromNativeSeparators(searchPath)); + cc.sync(); + initUI(); +} + +CallChainAnalyzer *CodeSearchPage::currentAnalyzer() const +{ + return m_analyzers[m_mode]; +} + +CodeSearchPage::~CodeSearchPage() = default; + +void CodeSearchPage::initUI() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setSpacing(10); + mainLayout->setContentsMargins(15, 15, 15, 15); + + // 业务模式切换:转码业务 / 非转码业务(Tab 形式),置于路径选择栏上方 + QHBoxLayout *modeLayout = new QHBoxLayout(); + m_modePivot = new ElaPivot(); + m_modePivot->appendPivot("转码业务"); + m_modePivot->appendPivot("非转码业务"); + m_modePivot->setTextPixelSize(14); + m_modePivot->setCurrentIndex(0); + connect(m_modePivot, &ElaPivot::pivotClicked, this, &CodeSearchPage::onPivotClicked); + modeLayout->addWidget(m_modePivot); + modeLayout->addStretch(); + + mainLayout->addLayout(modeLayout); + + // 代码检索路径选择栏(位于模式切换下方,两种模式共用) + QHBoxLayout *pathLayout = new QHBoxLayout(); + pathLayout->setSpacing(8); + + ElaText *pathLabel = new ElaText("代码检索路径:"); + pathLabel->setTextPixelSize(14); + pathLayout->addWidget(pathLabel); + + m_pathEdit = new ElaLineEdit(); + m_pathEdit->setText(m_pathByMode[ConvertMode]); + m_pathEdit->setPlaceholderText("请选择需要查询的目录"); + pathLayout->addWidget(m_pathEdit, 1); + + m_browseBtn = new ElaPushButton("浏览"); + connect(m_browseBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onBrowsePath); + pathLayout->addWidget(m_browseBtn); + + m_refreshBtn = new ElaPushButton("刷新索引"); + connect(m_refreshBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onRefreshIndex); + pathLayout->addWidget(m_refreshBtn); + + // 转码业务中"导出调用文件"紧随"刷新索引"同一行(非转码业务隐藏) + m_importBtn = new ElaPushButton("导出调用文件"); + m_importBtn->setMinimumWidth(140); + m_importBtn->setMinimumHeight(38); + { + QFont f = m_importBtn->font(); + f.setPointSize(11); + m_importBtn->setFont(f); + } + m_importBtn->setEnabled(false); + connect(m_importBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onImportChainFiles); + pathLayout->addWidget(m_importBtn); + + mainLayout->addLayout(pathLayout); + + // 非转码业务查询行:函数名/功能号 + 查询链路 + 全部展开/收起(同一行) + QHBoxLayout *queryLayout = new QHBoxLayout(); + queryLayout->setSpacing(8); + + m_queryCombo = new ElaComboBox(); + m_queryCombo->addItem("函数名"); + m_queryCombo->addItem("功能号"); + m_queryCombo->setCurrentText("函数名"); + queryLayout->addWidget(m_queryCombo); + + m_queryEdit = new ElaLineEdit(); + m_queryEdit->setPlaceholderText("输入函数名或功能号,回车查询"); + queryLayout->addWidget(m_queryEdit, 1); + + m_queryBtn = new ElaPushButton("查询链路"); + connect(m_queryBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onQueryByInput); + connect(m_queryEdit, &ElaLineEdit::returnPressed, this, &CodeSearchPage::onQueryByInput); + queryLayout->addWidget(m_queryBtn); + + // 非转码业务:导出调用链路(查询生成链路后可用) + m_exportChainBtn = new ElaPushButton("导出调用链路"); + m_exportChainBtn->setMinimumWidth(140); + m_exportChainBtn->setMinimumHeight(38); + { + QFont f = m_exportChainBtn->font(); + f.setPointSize(11); + m_exportChainBtn->setFont(f); + } + m_exportChainBtn->setEnabled(false); + connect(m_exportChainBtn, &ElaPushButton::clicked, this, &CodeSearchPage::onExportChain); + queryLayout->addWidget(m_exportChainBtn); + + mainLayout->addLayout(queryLayout); + + // 左右分割:左侧函数列表(转码业务),右侧调用链路树 + QSplitter *splitter = new QSplitter(Qt::Horizontal); + + // 左侧函数列表(仅转码业务) + m_leftPanel = new QWidget(); + QVBoxLayout *leftLayout = new QVBoxLayout(m_leftPanel); + leftLayout->setContentsMargins(0, 0, 0, 0); + leftLayout->addWidget(new ElaText("业务转码函数列表")); + + m_funcModel = new QStandardItemModel(this); + m_funcModel->setHorizontalHeaderLabels(QStringList() << "函数名"); + + m_funcTable = new ElaTableView(); + m_funcTable->setModel(m_funcModel); + m_funcTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_funcTable->setSelectionMode(QAbstractItemView::SingleSelection); + m_funcTable->horizontalHeader()->setStretchLastSection(true); + m_funcTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + connect(m_funcTable, &ElaTableView::doubleClicked, this, &CodeSearchPage::onFunctionDoubleClicked); + leftLayout->addWidget(m_funcTable); + + // 右侧调用链路树(每个模式各自独立的树,互不干扰) + QWidget *rightPanel = new QWidget(); + QVBoxLayout *rightLayout = new QVBoxLayout(rightPanel); + rightLayout->setContentsMargins(0, 0, 0, 0); + + // "调用链路"标题行:左侧标题,右侧"全部展开/全部折叠"图标按钮(两种模式共用,居右) + QHBoxLayout *chainTitleLayout = new QHBoxLayout(); + chainTitleLayout->setSpacing(8); + ElaText *chainTitle = new ElaText("调用链路"); + chainTitle->setTextPixelSize(14); + chainTitleLayout->addWidget(chainTitle); + chainTitleLayout->addStretch(); + + m_expandBtn = new IconButton(":/resources/images/expand_all.png", "全部展开", this); + connect(m_expandBtn, &IconButton::clicked, this, &CodeSearchPage::onExpandAll); + chainTitleLayout->addWidget(m_expandBtn); + + m_collapseBtn = new IconButton(":/resources/images/collapse_all.png", "全部折叠", this); + connect(m_collapseBtn, &IconButton::clicked, this, &CodeSearchPage::onCollapseAll); + chainTitleLayout->addWidget(m_collapseBtn); + + rightLayout->addLayout(chainTitleLayout); + + for (int i = 0; i < 2; ++i) { + m_treeModel[i] = new QStandardItemModel(this); + m_treeModel[i]->setHorizontalHeaderLabels(QStringList() << "函数名" << "目录" << "模块"); + // 表头列名居中显示 + for (int c = 0; c < 3; ++c) { + m_treeModel[i]->setHeaderData(c, Qt::Horizontal, + QVariant(Qt::AlignCenter), Qt::TextAlignmentRole); + } + + m_callTree[i] = new ElaTreeView(); + m_callTree[i]->setModel(m_treeModel[i]); + m_callTree[i]->setEditTriggers(QAbstractItemView::NoEditTriggers); + // 自定义 delegate:强制绘制"原代码已支持"行的绿色背景(绕过 Ela 主题覆盖) + m_callTree[i]->setItemDelegate(new CallTreeDelegate(m_callTree[i])); + // 初始按内容自适应列宽,之后允许用户拖拽自定义调整列宽 + m_callTree[i]->header()->setSectionResizeMode(0, QHeaderView::Interactive); + m_callTree[i]->header()->setSectionResizeMode(1, QHeaderView::Interactive); + m_callTree[i]->header()->setSectionResizeMode(2, QHeaderView::Interactive); + m_callTree[i]->header()->setStretchLastSection(false); + m_callTree[i]->setAlternatingRowColors(true); + rightLayout->addWidget(m_callTree[i]); + } + + splitter->addWidget(m_leftPanel); + splitter->addWidget(rightPanel); + splitter->setSizes(QList() << 300 << 700); + mainLayout->addWidget(splitter, 1); + + // 底部静态提示(仿 UF20 功能查询页最下方提示) + QHBoxLayout *tipLayout = new QHBoxLayout(); + m_bottomTip = new QLabel("💡 双击左侧业务转码函数查看调用链路"); + m_bottomTip->setStyleSheet("color: #666; font-size: 13px;"); + tipLayout->addWidget(m_bottomTip); + tipLayout->addStretch(); + mainLayout->addLayout(tipLayout); + + // 初始化为转码业务模式 + applyMode(); +} + +void CodeSearchPage::applyMode() +{ + const bool convert = (m_mode == ConvertMode); + + // 转码业务:左侧函数列表 + 导出调用文件 + // 非转码业务:手动查询行 + 全部展开/收起 + m_leftPanel->setVisible(convert); + m_importBtn->setVisible(convert); + m_funcTable->setVisible(convert); + + // 各自显示各自的调用链结果 + m_callTree[ConvertMode]->setVisible(convert); + m_callTree[SearchMode]->setVisible(!convert); + + m_queryCombo->setVisible(!convert); + m_queryEdit->setVisible(!convert); + m_queryBtn->setVisible(!convert); + m_exportChainBtn->setVisible(!convert); + + // 底部静态提示:转码业务显示操作提示,非转码业务隐藏 + m_bottomTip->setText("💡 双击左侧业务转码函数查看调用链路"); + m_bottomTip->setVisible(convert); +} + +void CodeSearchPage::onPivotClicked(int index) +{ + // 0:转码业务;1:非转码业务 + m_mode = (index == 1) ? SearchMode : ConvertMode; + // 切回该页面对应的检索路径,并按需自动构建索引 + m_pathEdit->setText(m_pathByMode[m_mode]); + applyMode(); + autoBuildIndexIfNeeded(); +} + +void CodeSearchPage::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + + // 每次显示页面都重新读取 cust.json 的 convert 列表, + // 以同步「业务转码」中新增/删除的转码函数 + loadFunctionList(); + + // 默认自动构建索引(仅首次/路径未变更且未索引时); + // 若用户手动改过路径,则需其点"刷新索引"手动触发 + autoBuildIndexIfNeeded(); +} + +void CodeSearchPage::loadFunctionList() +{ + m_funcModel->removeRows(0, m_funcModel->rowCount()); + + const QStringList functions = loadConvertFunctions(); + for (const QString &func : functions) { + QStandardItem *item = new QStandardItem(func); + item->setToolTip(func); + item->setData(func, Qt::UserRole + 1); + m_funcModel->appendRow(item); + } + + // 业务转码函数列表加载完成(结果直接体现在左侧表格,无需顶部状态栏提示) +} + +void CodeSearchPage::onBrowsePath() +{ + const QString dir = QFileDialog::getExistingDirectory(this, "选择代码检索目录", m_pathEdit->text()); + if (!dir.isEmpty()) { + m_pathByMode[m_mode] = QDir::toNativeSeparators(dir); + m_pathEdit->setText(m_pathByMode[m_mode]); + // 用户重新选择路径:标记需要手动刷新索引 + m_userReSelectedByMode[m_mode] = true; + // 持久化保存该模式路径,下次启动恢复 + savePathsToConfig(); + // 顶部状态栏已移除,改用弹窗提示用户需手动刷新索引 + QMessageBox::information(this, "提示", "路径已更改,请点击\"刷新索引\"重新加载代码目录"); + } +} + +void CodeSearchPage::onRefreshIndex() +{ + const QString path = m_pathEdit->text().trimmed(); + if (path.isEmpty()) { + QMessageBox::warning(this, "提示", "请先选择代码检索路径"); + return; + } + if (m_indexing) { + return; + } + + m_pathByMode[m_mode] = path; + + // 刷新索引时使用的路径也持久化保存,下次启动恢复 + savePathsToConfig(); + + CallChainAnalyzer *ca = currentAnalyzer(); + ca->setRootPath(path); + + // 后台线程构建,期间显示进度框,避免界面卡死 + m_indexing = true; + runIndexBuild(ca, "正在刷新代码索引,请稍候...", + [this, path](bool ok, const QString &message) { + m_indexing = false; + if (ok) { + currentAnalyzer()->saveToCache(); // 写入缓存,后续直接读取 + m_indexedPathByMode[m_mode] = path; + m_userReSelectedByMode[m_mode] = false; + LogManager::instance()->log(QString("[代码检索] %1").arg(message)); + QMessageBox::information(this, "刷新成功", + message.isEmpty() ? QString("索引刷新成功") : message); + } else { + m_indexedPathByMode[m_mode] = path; // 避免重复自动重试 + QMessageBox::warning(this, "刷新失败", + message.isEmpty() ? QString("索引刷新失败") : message); + } + }); +} + +void CodeSearchPage::savePathsToConfig() +{ + // 路径持久化到独立的 change_code.ini(与 config.ini 分离,避免更新被覆盖) + const QString iniPath = Constants::changeCodeIni(); + QSettings cc(iniPath, QSettings::IniFormat); + // 统一用正斜杠写入,避免 Ini 格式把反斜杠当转义符导致出现多余反斜杠 + cc.setValue(Constants::CodeSearch::ConvertPathKey, QDir::fromNativeSeparators(m_pathByMode[ConvertMode])); + cc.setValue(Constants::CodeSearch::SearchPathKey, QDir::fromNativeSeparators(m_pathByMode[SearchMode])); + cc.sync(); +} + +void CodeSearchPage::refreshIndexSilent() +{ + if (m_indexing) { + return; + } + // 收集已配置路径的模式,后台线程依次异步刷新,避免阻塞转换完成后的界面 + QList modes; + for (int mode = 0; mode < 2; ++mode) { + if (!m_pathByMode[mode].isEmpty() && m_analyzers[mode]) { + modes.append(mode); + } + } + if (modes.isEmpty()) { + return; + } + m_indexing = true; + buildSilentChain(modes, 0); +} + +void CodeSearchPage::buildSilentChain(const QList &modes, int idx) +{ + if (idx >= modes.size()) { + m_indexing = false; + return; + } + const int mode = modes[idx]; + const QString path = m_pathByMode[mode]; + CallChainAnalyzer *ca = m_analyzers[mode]; + ca->setRootPath(path); + + // 后台线程构建,复用进度框;无弹窗(静默),完成后继续下一模式 + runIndexBuild(ca, "正在刷新代码索引,请稍候...", + [this, modes, idx, mode, path, ca](bool ok, const QString &message) { + if (ok) { + ca->saveToCache(); + m_indexedPathByMode[mode] = path; + m_userReSelectedByMode[mode] = false; + LogManager::instance()->log(QString("[代码检索] 转码后自动刷新索引成功 %1: %2") + .arg(mode == 0 ? "转码业务" : "非转码业务", message)); + } else { + m_indexedPathByMode[mode] = path; + LogManager::instance()->logWarning(QString("[代码检索] 转码后自动刷新索引失败 %1: %2") + .arg(mode == 0 ? "转码业务" : "非转码业务", message)); + } + buildSilentChain(modes, idx + 1); + }); +} + +void CodeSearchPage::onFunctionDoubleClicked(const QModelIndex &index) +{ + if (!index.isValid()) { + return; + } + + if (!currentAnalyzer()->isIndexed()) { + QMessageBox::information(this, "提示", "请先点击\"刷新索引\"加载代码目录"); + return; + } + + const QString displayName = m_funcModel->item(index.row())->data(Qt::UserRole + 1).toString(); + runQuery(displayName); +} + +void CodeSearchPage::populateTree(const QList &rootNodes) +{ + QStandardItemModel *model = m_treeModel[m_mode]; + ElaTreeView *tree = m_callTree[m_mode]; + + model->removeRows(0, model->rowCount()); + + QStandardItem *rootItem = model->invisibleRootItem(); + for (const CallChainAnalyzer::TreeNode &r : rootNodes) { + addTreeItem(rootItem, r); + } + + tree->expandToDepth(0); + + // 渲染后根据实际内容(含字体度量)自适应列宽,用户仍可自行拖拽调整 + autoResizeColumns(tree); +} + +void CodeSearchPage::autoResizeColumns(ElaTreeView *tree) +{ + // 由于 resizeColumnToContents 只测量"可见"项,被折叠的深层节点不会参与计算, + // 导致第3级及以后的函数名/目录/模块列宽无法自适应。 + // 这里改为遍历整个模型(含未展开节点),按字体度量取各列最大内容宽度。 + QStandardItemModel *model = qobject_cast(tree->model()); + if (!model) { + return; + } + + QFontMetrics fm(tree->font()); + int colMax[3] = {0, 0, 0}; + + // 递归遍历所有行(level 为相对调用树根的层级,用于累加缩进占位) + std::function walk = [&](QStandardItem *parent, int level) { + for (int r = 0; r < parent->rowCount(); ++r) { + QStandardItem *cells[3] = { + parent->child(r, 0), + parent->child(r, 1), + parent->child(r, 2) + }; + // 第0列(函数名)需加上各级缩进与展开箭头占位 + const int indent = tree->indentation() * (level + 1); + if (cells[0]) { + colMax[0] = qMax(colMax[0], + fm.horizontalAdvance(cells[0]->text()) + indent); + } + if (cells[1]) { + colMax[1] = qMax(colMax[1], fm.horizontalAdvance(cells[1]->text())); + } + if (cells[2]) { + colMax[2] = qMax(colMax[2], fm.horizontalAdvance(cells[2]->text())); + } + // 以第0列单元格为父,继续向下遍历更深层 + if (cells[0]) { + walk(cells[0], level + 1); + } + } + }; + walk(model->invisibleRootItem(), 0); + + // 表头列名宽度也应纳入,避免列名比数据更宽时被截断 + for (int c = 0; c < 3; ++c) { + const QString header = model->headerData(c, Qt::Horizontal, + Qt::DisplayRole).toString(); + colMax[c] = qMax(colMax[c], fm.horizontalAdvance(header)); + } + + // 补充边距避免文字紧贴边界,用户仍可自行拖拽调整 + for (int c = 0; c < 3; ++c) { + tree->setColumnWidth(c, colMax[c] + 16); + } +} + +void CodeSearchPage::addTreeItem(QStandardItem *parentItem, const CallChainAnalyzer::TreeNode &node) +{ + QString nameText = node.displayName; + if (node.state == "notfound") { + nameText += " [未找到]"; + } else if (node.state == "cycle") { + nameText += " [循环调用]"; + } else if (node.state == "depthlimit") { + nameText += " [层级截断]"; + } else if (node.state == "supported") { + // 未在转码代码中找到:说明该函数无需转码,原代码已支持 + // 功能号通过 uft3 功能查询(按功能名称)获取 + const QString funcNo = lookupUFT3FunctionNo(node.displayName); + nameText += " [原代码已支持]"; + if (!funcNo.isEmpty()) { + nameText = QString("%1-%2").arg(funcNo, nameText); + } + } + + QStandardItem *nameItem = new QStandardItem(nameText); + nameItem->setToolTip(nameText); + nameItem->setEditable(false); + + QStandardItem *dirItem = new QStandardItem(); + QStandardItem *moduleItem = new QStandardItem(); + + // "目录"、"模块"列数据居中显示 + dirItem->setTextAlignment(Qt::AlignCenter); + moduleItem->setTextAlignment(Qt::AlignCenter); + + if (node.info) { + dirItem->setText(node.info->relDir); + dirItem->setToolTip(node.info->filePath); + moduleItem->setText(node.info->module); + } + + // 不同状态用不同颜色提示 + if (node.state == "notfound") { + nameItem->setForeground(QBrush(QColor(180, 0, 0))); + } else if (node.state == "cycle") { + nameItem->setForeground(QBrush(QColor(180, 100, 0))); + } else if (node.state == "supported") { + // 原代码已支持:整行绿色背景高亮;功能号置于函数名前,格式:功能号-函数名[原代码已支持] + const QString funcNo = lookupUFT3FunctionNo(node.displayName); + if (!funcNo.isEmpty()) { + nameText = QString("%1-%2[原代码已支持]").arg(funcNo, node.displayName); + } else { + nameText += " [原代码已支持]"; + } + const QBrush greenBg(QColor(198, 239, 206)); + nameItem->setBackground(greenBg); + dirItem->setBackground(greenBg); + moduleItem->setBackground(greenBg); + nameItem->setForeground(QBrush(QColor(0, 97, 0))); + // 标记高亮角色,交由自定义 delegate 强制绘制绿色背景 + nameItem->setData(true, RowHighlightRole); + dirItem->setData(true, RowHighlightRole); + moduleItem->setData(true, RowHighlightRole); + } + + dirItem->setEditable(false); + moduleItem->setEditable(false); + + // 查询的目标函数/功能:整行蓝色背景高亮,突出当前查询节点(仅对非"原代码已支持"节点生效) + if (!m_highlightName.isEmpty() && node.displayName == m_highlightName + && node.state != "supported") { + const QBrush blueBg(QColor(189, 215, 238)); + nameItem->setBackground(blueBg); + dirItem->setBackground(blueBg); + moduleItem->setBackground(blueBg); + nameItem->setForeground(QBrush(QColor(10, 60, 120))); + // 标记蓝色高亮角色,交由自定义 delegate 强制绘制蓝色背景 + nameItem->setData(true, RowHighlightBlueRole); + dirItem->setData(true, RowHighlightBlueRole); + moduleItem->setData(true, RowHighlightBlueRole); + } + + QList row; + row << nameItem << dirItem << moduleItem; + parentItem->appendRow(row); + + for (const CallChainAnalyzer::TreeNode &child : node.children) { + addTreeItem(nameItem, child); + } +} + +QString CodeSearchPage::lookupUFT3FunctionNo(const QString &funcName) +{ + DataCache *cache = DataCache::instance(); + if (!cache || !cache->isDataLoaded()) { + return QString(); + } + + // 先按"功能名称"精确匹配(cname) + QList results = cache->searchUFT3Functions(funcName, "cname", true); + if (results.isEmpty()) { + // 退化为模糊匹配(cname/英文名/功能号) + results = cache->searchUFT3Functions(funcName, "", false); + } + if (!results.isEmpty()) { + return results.first().functionNo; + } + return QString(); +} + +QString CodeSearchPage::resolveChineseName(const QString &displayName) const +{ + // 支持两种格式: + // 1) LS_xxx / LF_xxx(cust.json 中去掉后缀后的格式) + // 2) objectId-LS_xxx / objectId-LF_xxx(带功能号前缀的格式) + const int dashIndex = displayName.indexOf('-'); + if (dashIndex > 0) { + return displayName.mid(dashIndex + 1); + } + return displayName; +} + +void CodeSearchPage::autoBuildIndexIfNeeded() +{ + const QString path = m_pathByMode[m_mode].trimmed(); + // 仅当:路径有效、尚未索引、非用户手动改过路径、且未正在构建时,才默认自动加载 + if (path.isEmpty() || m_userReSelectedByMode[m_mode] + || currentAnalyzer()->isIndexed() || m_indexing) { + return; + } + + CallChainAnalyzer *ca = currentAnalyzer(); + ca->setRootPath(path); + + // 1) 优先从磁盘缓存读取(程序重启 / 页面切换时秒开,无需重新扫描目录) + QString message; + if (ca->loadFromCache(path, &message)) { + m_indexedPathByMode[m_mode] = path; + LogManager::instance()->log(QString("[代码检索-缓存] %1").arg(message)); + return; + } + + // 2) 缓存不存在或已失效:后台线程扫描目录构建索引,期间显示进度框,避免界面卡死; + // 构建完成后写入缓存,供下次直接读取。 + m_indexing = true; + runIndexBuild(ca, "正在构建代码索引,请稍候...", + [this, path](bool ok, const QString &message) { + m_indexing = false; + if (ok) { + currentAnalyzer()->saveToCache(); + m_indexedPathByMode[m_mode] = path; + LogManager::instance()->log(QString("[代码检索-自动] %1").arg(message)); + } else { + m_indexedPathByMode[m_mode] = path; // 标记已尝试,避免每次显示重复重试 + } + }); +} + +void CodeSearchPage::runIndexBuild(CallChainAnalyzer *ca, const QString &title, + std::function onDone) +{ + // 模态进度框(不确定进度),后台构建期间维持界面响应 + auto *dlg = new QProgressDialog(title, QString(), 0, 0, this); + dlg->setWindowTitle("请稍候"); + dlg->setWindowModality(Qt::WindowModal); + dlg->setCancelButton(nullptr); + dlg->setRange(0, 0); // 不确定进度 + dlg->setMinimumDuration(0); + dlg->show(); + + using BuildResult = QPair; + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [=]() { + const BuildResult r = watcher->result(); + watcher->deleteLater(); + dlg->close(); + dlg->deleteLater(); + if (onDone) { + onDone(r.first, r.second); + } + }); + + // 在后台线程执行实际扫描,避免阻塞 UI 线程 + auto future = QtConcurrent::run([=]() -> BuildResult { + QString msg; + const bool ok = ca->buildIndex(&msg); + return {ok, msg}; + }); + watcher->setFuture(future); +} + +void CodeSearchPage::onQueryByInput() +{ + const QString raw = m_queryEdit->text().trimmed(); + if (raw.isEmpty()) { + QMessageBox::information(this, "提示", "请输入要查询的内容"); + return; + } + + QString target = raw; + if (m_queryCombo->currentText() == "功能号") { + // 按功能号解析为对应的函数名(英文名 eName)后再查询 + DataCache *cache = DataCache::instance(); + if (!cache || !cache->isDataLoaded()) { + QMessageBox::information(this, "提示", "功能数据未加载,无法按功能号查询"); + return; + } + const QList results = cache->searchUFT3Functions(raw, "function_no", true); + if (results.isEmpty() || results.first().eName.isEmpty()) { + QMessageBox::information(this, "提示", + QString("未找到功能号:%1 对应的函数").arg(raw)); + return; + } + target = results.first().eName; + } + + runQuery(target); +} + +void CodeSearchPage::runQuery(const QString &displayName) +{ + if (!currentAnalyzer()->isIndexed()) { + QMessageBox::information(this, "提示", "请先点击\"刷新索引\"加载代码目录"); + return; + } + + const QString chineseName = resolveChineseName(displayName); + + // 记录当前查询的函数/功能名,供调用链树中对应行蓝色高亮。 + // 注意:树节点 displayName 由 resolver 拼成 "objectId-chineseName" 形式, + // 因此这里要用与 CallChainAnalyzer::resolveNode 一致的公式还原展示名, + // 否则功能号查询(target=eName)或带功能号前缀的查询会匹配不上,导致目标行不高亮。 + m_highlightName = chineseName; + if (QSharedPointer hi = + currentAnalyzer()->lookupBest(chineseName)) { + m_highlightName = hi->objectId.isEmpty() + ? hi->chineseName + : QString("%1-%2").arg(hi->objectId, hi->chineseName); + } + + QApplication::setOverrideCursor(Qt::WaitCursor); + // buildCallTree 内部使用多策略查找,如果未找到根节点会标记为 "notfound" + CallChainAnalyzer::TreeNode downTree = currentAnalyzer()->buildCallTree(chineseName); + + if (downTree.state != "ok") { + QApplication::restoreOverrideCursor(); + QMessageBox::information(this, "未找到", + QString("未在索引中找到函数:%1\n请确认代码检索路径正确并已刷新索引。").arg(displayName)); + return; + } + + // 往前查:构造上游调用方森林(每条链以 downTree 收尾),无上游时退化为单根 downTree + QList roots = + currentAnalyzer()->buildCallerForest(chineseName, downTree); + + populateTree(roots); + QApplication::restoreOverrideCursor(); + + m_callTree[m_mode]->expandToDepth(2); + + // 收集调用链路文件(排除"原代码已支持"),并记录导出基准路径与查询名 + QSet collected; + for (const CallChainAnalyzer::TreeNode &r : roots) { + collectChainFiles(r, collected); + } + m_currentChainRoots = roots; + m_currentChainFilePaths = collected.values(); + m_searchRootPath = m_pathEdit->text().trimmed(); + m_currentQueryName = displayName; + m_importBtn->setEnabled(!m_currentChainFilePaths.isEmpty()); + m_exportChainBtn->setEnabled(!m_currentChainFilePaths.isEmpty()); +} + +void CodeSearchPage::onExpandAll() +{ + // 展开/收起操作当前模式对应的调用链路树 + ElaTreeView *tree = m_callTree[m_mode]; + tree->expandAll(); + // 展开后内容宽度可能变化,重新自适应列宽 + autoResizeColumns(tree); +} + +void CodeSearchPage::onCollapseAll() +{ + m_callTree[m_mode]->collapseAll(); +} + +void CodeSearchPage::collectChainFiles(const CallChainAnalyzer::TreeNode &node, QSet &out) +{ + // 仅收集有文件且非"原代码已支持"(supported)的节点 + if (node.info && node.state != "supported") { + out.insert(node.info->filePath); + } + for (const CallChainAnalyzer::TreeNode &child : node.children) { + collectChainFiles(child, out); + } +} + +void CodeSearchPage::collectChainLines(const CallChainAnalyzer::TreeNode &node, int depth, QStringList &lines) +{ + if (depth >= 64) return; // 防无限递归 + + QString indent(depth * 4, QChar(' ')); + QString stateTag; + if (node.state == "supported") stateTag = " [原代码已支持]"; + else if (node.state == "notfound") stateTag = " [未找到]"; + + QString funcLabel = node.displayName; + // 为 supported / notfound 节点补充功能号前缀(若可解析且名称中尚未包含) + if ((node.state == "supported" || node.state == "notfound") + && !node.objectId.isEmpty() && !funcLabel.startsWith(node.objectId)) { + funcLabel = node.objectId + "-" + funcLabel; + } + + QString line = indent; + if (depth > 0) line += "|- "; + line += funcLabel; + line += stateTag; + lines.append(line); + + for (const CallChainAnalyzer::TreeNode &child : node.children) { + collectChainLines(child, depth + 1, lines); + } +} + +void CodeSearchPage::onImportChainFiles() +{ + if (m_currentChainFilePaths.isEmpty()) { + QMessageBox::information(this, "提示", "请先双击函数生成调用链路"); + return; + } + if (m_searchRootPath.isEmpty() || !QFile::exists(m_searchRootPath)) { + QMessageBox::warning(this, "提示", "无法定位代码检索路径"); + return; + } + + // function_output / <查询函数名> / + const QString outRoot = QDir(m_searchRootPath).filePath(Constants::functionOutput()); + const QString queryDir = QDir(outRoot).filePath(sanitizeDirName(m_currentQueryName)); + + QDir().mkpath(queryDir); + + QDir rootDir(m_searchRootPath); + int copied = 0; + QStringList failed; + + for (const QString &src : m_currentChainFilePaths) { + QFileInfo fi(src); + if (!fi.exists()) { + failed.append(src); + continue; + } + // 保持相对搜索根的路径结构:b/c/d/a.function + const QString relPath = rootDir.relativeFilePath(src); + const QString dest = QDir(queryDir).filePath(relPath); + QDir().mkpath(QFileInfo(dest).absolutePath()); + if (QFile::exists(dest) && !QFile::remove(dest)) { + failed.append(src); + continue; + } + if (QFile::copy(src, dest)) { + copied++; + } else { + failed.append(src); + } + } + + // 生成调用链路文本文件(遍历整片森林,含往前查的上游链) + QStringList chainLines; + for (const CallChainAnalyzer::TreeNode &r : m_currentChainRoots) { + collectChainLines(r, 0, chainLines); + } + const QString txtPath = QDir(queryDir).filePath("_call_chain.txt"); + QFile txtFile(txtPath); + if (txtFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + QTextStream ts(&txtFile); + for (const QString &line : chainLines) { + ts << line << "\n"; + } + } + + QString msg = QString("已将 %1 个调用文件导出到:\n%2").arg(copied).arg(QDir::toNativeSeparators(queryDir)); + if (!failed.isEmpty()) { + msg += QString("\n\n%1 个文件导出失败:\n%2").arg(failed.size()).arg(failed.join("\n")); + } + QMessageBox::information(this, "导出完成", msg); +} + +void CodeSearchPage::onExportChain() +{ + if (m_currentChainFilePaths.isEmpty()) { + QMessageBox::information(this, "提示", "请先查询函数生成调用链路"); + return; + } + if (m_searchRootPath.isEmpty() || !QFile::exists(m_searchRootPath)) { + QMessageBox::warning(this, "提示", "无法定位代码检索路径"); + return; + } + + // function_output / <查询函数名> / + const QString outRoot = QDir(m_searchRootPath).filePath(Constants::functionOutput()); + const QString queryDir = QDir(outRoot).filePath(sanitizeDirName(m_currentQueryName)); + QDir().mkpath(queryDir); + + // 非转码业务:仅导出调用链路文本,不复制源文件 + QStringList chainLines; + for (const CallChainAnalyzer::TreeNode &r : m_currentChainRoots) { + collectChainLines(r, 0, chainLines); + } + const QString txtPath = QDir(queryDir).filePath("_call_chain.txt"); + QFile txtFile(txtPath); + if (!txtFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, "提示", "调用链路文件写入失败"); + return; + } + QTextStream ts(&txtFile); + for (const QString &line : chainLines) { + ts << line << "\n"; + } + txtFile.close(); + + QString msg = QString("已导出调用链路(%1 行)到:\n%2") + .arg(chainLines.size()).arg(QDir::toNativeSeparators(queryDir)); + QMessageBox::information(this, "导出完成", msg); +} + +QString CodeSearchPage::sanitizeDirName(const QString &name) +{ + // 去除 Windows 非法目录字符 + QString out = name; + static const QRegularExpression re(R"([\\/:*?"<>|])"); + out.replace(re, "_"); + out = out.trimmed(); + if (out.isEmpty()) { + out = "function"; + } + return out; +} diff --git a/src/pages/codesearch/codesearchpage.h b/src/pages/codesearch/codesearchpage.h new file mode 100644 index 0000000..73a6753 --- /dev/null +++ b/src/pages/codesearch/codesearchpage.h @@ -0,0 +1,127 @@ +#ifndef CODESEARCHPAGE_H +#define CODESEARCHPAGE_H + +#include +#include +#include +#include +#include +#include "ElaTableView.h" +#include "ElaText.h" +#include "ElaLineEdit.h" +#include "ElaPushButton.h" +#include "ElaTreeView.h" +#include "ElaComboBox.h" +#include "ElaPivot.h" +#include "utils/callchainanalyzer.h" + +// 图标按钮(基于 QLabel):显示图标 + 悬停提示,鼠标点击触发信号 +class IconButton : public QLabel +{ + Q_OBJECT +public: + explicit IconButton(const QString &pixmapPath, const QString &tip, + QWidget *parent = nullptr) + : QLabel(parent) + { + const QPixmap pm(pixmapPath); + setPixmap(pm.scaled(22, 22, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + setToolTip(tip); + setCursor(Qt::ArrowCursor); + setFixedSize(28, 28); + setAlignment(Qt::AlignCenter); + } + +signals: + void clicked(); + +protected: + void mousePressEvent(QMouseEvent *event) override + { + emit clicked(); + QLabel::mousePressEvent(event); + } +}; + +class CodeSearchPage : public QWidget +{ + Q_OBJECT + +public: + enum Mode { ConvertMode, SearchMode }; + + explicit CodeSearchPage(QWidget *parent = nullptr); + ~CodeSearchPage(); + + /// 静默刷新索引(无弹窗,转码完成后自动调用) + void refreshIndexSilent(); + /// 转码后静默异步刷新:依次(后台线程)刷新各模式索引,全部完成后清除 m_indexing + void buildSilentChain(const QList &modes, int idx); + +protected: + void showEvent(QShowEvent *event) override; + +private slots: + void onBrowsePath(); + void onRefreshIndex(); + void onFunctionDoubleClicked(const QModelIndex &index); + void onExpandAll(); + void onCollapseAll(); + void onImportChainFiles(); + void onExportChain(); + void onQueryByInput(); + void savePathsToConfig(); + void onPivotClicked(int index); + +private: + void initUI(); + void loadFunctionList(); + void populateTree(const QList &rootNodes); + void autoResizeColumns(ElaTreeView *tree); + void addTreeItem(QStandardItem *parentItem, const CallChainAnalyzer::TreeNode &node); + QString resolveChineseName(const QString &displayName) const; + QString lookupUFT3FunctionNo(const QString &funcName); + void collectChainFiles(const CallChainAnalyzer::TreeNode &node, QSet &out); + static void collectChainLines(const CallChainAnalyzer::TreeNode &node, int depth, QStringList &lines); + static QString sanitizeDirName(const QString &name); + void runQuery(const QString &displayName); + void autoBuildIndexIfNeeded(); + void applyMode(); + CallChainAnalyzer *currentAnalyzer() const; + + /// 后台线程异步构建索引(避免界面卡顿),构建期间显示进度框;完成后回调 onDone(ok, message) + void runIndexBuild(CallChainAnalyzer *ca, const QString &title, + std::function onDone); + + Mode m_mode = ConvertMode; + + ElaLineEdit *m_pathEdit; + ElaPushButton *m_browseBtn; + ElaPushButton *m_refreshBtn; + ElaPivot *m_modePivot; + IconButton *m_expandBtn; + IconButton *m_collapseBtn; + ElaPushButton *m_importBtn; + ElaPushButton *m_exportChainBtn; + ElaComboBox *m_queryCombo; + ElaLineEdit *m_queryEdit; + ElaPushButton *m_queryBtn; + ElaTableView *m_funcTable; + QWidget *m_leftPanel; + ElaTreeView *m_callTree[2]; + QStandardItemModel *m_funcModel; + QStandardItemModel *m_treeModel[2]; + QLabel *m_bottomTip; // 底部静态提示(仿 UF20 功能查询页) + CallChainAnalyzer *m_analyzers[2]; // 两种模式各自的分析器(独立索引) + QString m_pathByMode[2]; // 两种模式各自的代码检索路径 + QString m_indexedPathByMode[2]; // 两种模式各自已构建索引的路径 + bool m_userReSelectedByMode[2] = {false, false}; // 各自改路径后需手动刷新 + QStringList m_currentChainFilePaths; // 当前调用链路中所有非"原代码已支持"的文件 + QList m_currentChainRoots; // 当前调用链路森林(支持往前查),供导出文本用 + QString m_searchRootPath; // 当前索引根路径(导出基准) + QString m_currentQueryName; // 当前查询的函数名称(导出目录名) + QString m_highlightName; // 当前查询的函数/功能名(调用链树中对应行蓝色高亮) + bool m_indexing = false; // 后台索引构建进行中,防止重复触发 +}; + +#endif // CODESEARCHPAGE_H diff --git a/src/pages/metadatupdate/metadatupdatepage.cpp b/src/pages/metadatupdate/metadatupdatepage.cpp index 515d72c..8105da7 100644 --- a/src/pages/metadatupdate/metadatupdatepage.cpp +++ b/src/pages/metadatupdate/metadatupdatepage.cpp @@ -1,5 +1,6 @@ #include "metadatupdatepage.h" #include "utils/Constants.h" +#include "utils/logmanager.h" #include #include #include @@ -8,6 +9,7 @@ #include #include #include +#include const int UPDATE_ITEM_SHOW_COLUMN_COUNT = 3; @@ -17,13 +19,18 @@ MetadataUpdatePage::MetadataUpdatePage(QWidget *parent) initUI(); _processor = new MetadataProcessor(this); _processor->initConfig(Constants::toolDir()); + + _watcher = new QFutureWatcher(this); + connect(_watcher, &QFutureWatcher::finished, + this, &MetadataUpdatePage::onWorkerFinished); + connect(this, &MetadataUpdatePage::sigProgress, + this, &MetadataUpdatePage::onUpdateProgress, Qt::QueuedConnection); } MetadataUpdatePage::~MetadataUpdatePage() { - if (_workerThread && _workerThread->isRunning()) { - _workerThread->quit(); - _workerThread->wait(); + if (_watcher && _watcher->isRunning()) { + _watcher->waitForFinished(); } } @@ -72,6 +79,13 @@ void MetadataUpdatePage::initUI() gridLayout->addWidget(_uft3CancelBtn, 1, 3, 1, 1); _uft3UpdateBtn = new ElaPushButton("更新UFT30", contentWidget); + _uft3UpdateBtn->setMinimumWidth(140); + _uft3UpdateBtn->setMinimumHeight(38); + { + QFont f = _uft3UpdateBtn->font(); + f.setPointSize(11); + _uft3UpdateBtn->setFont(f); + } connect(_uft3UpdateBtn, &QPushButton::clicked, this, &MetadataUpdatePage::onUpdateUFT3); gridLayout->addWidget(_uft3UpdateBtn, 1, 4, 1, 1); @@ -105,17 +119,42 @@ void MetadataUpdatePage::initUI() gridLayout->addWidget(_uf2CancelBtn, 1, 8, 1, 1); _uf2UpdateBtn = new ElaPushButton("更新UF20", contentWidget); + _uf2UpdateBtn->setMinimumWidth(140); + _uf2UpdateBtn->setMinimumHeight(38); + { + QFont f = _uf2UpdateBtn->font(); + f.setPointSize(11); + _uf2UpdateBtn->setFont(f); + } connect(_uf2UpdateBtn, &QPushButton::clicked, this, &MetadataUpdatePage::onUpdateUF20); gridLayout->addWidget(_uf2UpdateBtn, 1, 9, 1, 1); gridLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Minimum), 0, 10, 2, 1); + _forceFullCheckBox = new ElaCheckBox("强制全量更新", contentWidget); + _forceFullCheckBox->setToolTip("勾选后本次将重新扫描并解析全部接口文件、重建增量缓存;不勾选时仅处理有变化的文件"); + connect(_forceFullCheckBox, &QCheckBox::toggled, this, [this](bool checked){ + if (checked) { + auto ret = QMessageBox::question(this, "提示", + "全量更新接口耗时较久,请确认是否继续?", + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (ret != QMessageBox::Yes) { + _forceFullCheckBox->setChecked(false); + } + } + }); + centerLayout->addWidget(contentWidget); centerLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding)); + // 进度标签行:左侧显示状态文字,右侧放置强制全量更新复选框 + QHBoxLayout* processHeaderLayout = new QHBoxLayout(); _processLabel = new ElaText(this); - centerLayout->addWidget(_processLabel); + _processLabel->setTextPixelSize(14); + processHeaderLayout->addWidget(_processLabel, 1); + processHeaderLayout->addWidget(_forceFullCheckBox); + centerLayout->addLayout(processHeaderLayout); _progressBar = new ElaProgressBar(this); _progressBar->setValue(0); @@ -209,8 +248,9 @@ void MetadataUpdatePage::onUpdateUF20() _uf2AllBtn->setEnabled(false); _uf2CancelBtn->setEnabled(false); _progressBar->setValue(0); - - updateUf2Interfaces(selectedItems); + _processor->setForceFull(_forceFullCheckBox->isChecked()); + + startUpdate(selectedItems, "UF20"); } void MetadataUpdatePage::onUFT3SelectAll() @@ -245,22 +285,22 @@ void MetadataUpdatePage::onUpdateUFT3() _uft3AllBtn->setEnabled(false); _uft3CancelBtn->setEnabled(false); _progressBar->setValue(0); - - updateUft3Interfaces(selectedItems); + _processor->setForceFull(_forceFullCheckBox->isChecked()); + + startUpdate(selectedItems, "UFT30"); } -void MetadataUpdatePage::updateUf2Interfaces(const QStringList& items) +UpdateResult MetadataUpdatePage::runUpdateUf2(const QStringList& items) { - int itemCount = items.size(); + bool success = true; bool interfaceUpdated = false; - - foreach (const QString& item, items) { - _processLabel->setText(QString("正在更新UF20 - %1").arg(item)); - int currentIndex = items.indexOf(item); - int progress = (itemCount == 1) ? 0 : (currentIndex * 100 / itemCount); - _progressBar->setValue(progress); - QCoreApplication::processEvents(); - + int itemCount = items.size(); + int idx = 0; + + for (const QString& item : items) { + emit sigProgress((itemCount == 1) ? 0 : (idx * 100 / itemCount), + QString("正在更新UF20 - %1").arg(item)); + if (item == "标准字段") { processUf2StdFields(); } else if (item == "数据类型") { @@ -268,35 +308,29 @@ void MetadataUpdatePage::updateUf2Interfaces(const QStringList& items) } else if (item == "标准错误号") { processUf2ErrNumber(); } else if (item == "接口数据") { - processUf2Interface(Constants::toolDir()); + if (!processUf2Interface(Constants::toolDir())) { + success = false; + } interfaceUpdated = true; } + ++idx; } - - if (interfaceUpdated) { - _processLabel->setText("正在更新数据缓存..."); - QCoreApplication::processEvents(); - UF2ConfigReader::reloadUF20Config(); - } - - _progressBar->setValue(100); - _processLabel->setText("UF20更新完成"); - - onUpdateFinished(true, "UF20更新完成"); + + // 数据缓存重载放到主线程执行(DataCache 单例非线程安全),见 onWorkerFinished + return UpdateResult{success, interfaceUpdated}; } -void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items) +UpdateResult MetadataUpdatePage::runUpdateUft3(const QStringList& items) { - int itemCount = items.size(); + bool success = true; bool interfaceUpdated = false; - - foreach (const QString& item, items) { - _processLabel->setText(QString("正在更新UFT30 - %1").arg(item)); - int currentIndex = items.indexOf(item); - int progress = (itemCount == 1) ? 0 : (currentIndex * 100 / itemCount); - _progressBar->setValue(progress); - QCoreApplication::processEvents(); - + int itemCount = items.size(); + int idx = 0; + + for (const QString& item : items) { + emit sigProgress((itemCount == 1) ? 0 : (idx * 100 / itemCount), + QString("正在更新UFT30 - %1").arg(item)); + if (item == "标准字段") { processUft3StdFields(); } else if (item == "数据类型") { @@ -304,21 +338,15 @@ void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items) } else if (item == "表结构") { processUft3Table(); } else if (item == "接口数据") { - processUft3Interface(Constants::toolDir()); + if (!processUft3Interface(Constants::toolDir())) { + success = false; + } interfaceUpdated = true; } + ++idx; } - - if (interfaceUpdated) { - _processLabel->setText("正在更新数据缓存..."); - QCoreApplication::processEvents(); - UFT3ConfigReader::reloadUFT3Config(); - } - - _progressBar->setValue(100); - _processLabel->setText("UFT30更新完成"); - - onUpdateFinished(true, "UFT30更新完成"); + + return UpdateResult{success, interfaceUpdated}; } void MetadataUpdatePage::processUf2StdFields() @@ -336,9 +364,9 @@ void MetadataUpdatePage::processUf2ErrNumber() _processor->processUf2ErrNumber(); } -void MetadataUpdatePage::processUf2Interface(const QString& basePath) +bool MetadataUpdatePage::processUf2Interface(const QString& basePath) { - _processor->processUf2Interfaces(basePath); + return _processor->processUf2Interfaces(basePath); } void MetadataUpdatePage::processUft3StdFields() @@ -356,9 +384,60 @@ void MetadataUpdatePage::processUft3Table() _processor->processUft3Table(); } -void MetadataUpdatePage::processUft3Interface(const QString& basePath) +bool MetadataUpdatePage::processUft3Interface(const QString& basePath) { - _processor->processUft3Interfaces(basePath); + return _processor->processUft3Interfaces(basePath); +} + +void MetadataUpdatePage::startUpdate(const QStringList& items, const QString& taskName) +{ + m_currentTaskName = taskName; + _updateTimer.start(); + _progressBar->setValue(0); + _processLabel->setText(QString("正在准备更新%1...").arg(taskName)); + + // 防御:若上一个任务仍在运行则不再启动(按钮已禁用,正常情况下不会触发) + if (_watcher->isRunning()) { + return; + } + + if (taskName == "UF20") { + _watcher->setFuture(QtConcurrent::run([this, items]() { + return runUpdateUf2(items); + })); + } else { + _watcher->setFuture(QtConcurrent::run([this, items]() { + return runUpdateUft3(items); + })); + } +} + +void MetadataUpdatePage::onWorkerFinished() +{ + UpdateResult r = _watcher->result(); + + // 计算总耗时 + qint64 elapsedMs = _updateTimer.elapsed(); + QString elapsedStr; + if (elapsedMs < 1000) { + elapsedStr = QString::number(elapsedMs) + "ms"; + } else { + elapsedStr = QString("%1s (%2ms)").arg(elapsedMs / 1000.0, 0, 'f', 1).arg(elapsedMs); + } + LogManager::instance()->log(QString("%1 元数据更新总耗时: %2").arg(m_currentTaskName, elapsedStr)); + + // 数据缓存重载必须在主线程执行(DataCache 单例与静态 cnameCache 非线程安全) + if (r.needReload) { + _processLabel->setText("正在更新数据缓存..."); + if (m_currentTaskName == "UF20") { + UF2ConfigReader::reloadUF20Config(); + } else { + UFT3ConfigReader::reloadUFT3Config(); + } + } + + onUpdateFinished(r.success, QString("%1更新完成").arg(m_currentTaskName)); + _processLabel->setText(QString("%1更新完成(总耗时: %2)").arg(m_currentTaskName, elapsedStr)); } void MetadataUpdatePage::onUpdateProgress(int progress, const QString& message) diff --git a/src/pages/metadatupdate/metadatupdatepage.h b/src/pages/metadatupdate/metadatupdatepage.h index 239d27c..b9d7dfe 100644 --- a/src/pages/metadatupdate/metadatupdatepage.h +++ b/src/pages/metadatupdate/metadatupdatepage.h @@ -12,6 +12,14 @@ #include #include #include +#include +#include + +// 后台线程执行结果:success 表示整体是否成功,needReload 表示是否需要重新加载数据缓存 +struct UpdateResult { + bool success; + bool needReload; +}; class MetadataUpdatePage : public ElaScrollPage { @@ -35,22 +43,30 @@ private slots: private slots: void onUpdateProgress(int progress, const QString& message); void onUpdateFinished(bool success, const QString& message); + void onWorkerFinished(); // QFutureWatcher::finished 的回调(运行在主线程) + +signals: + void sigProgress(int progress, const QString& message); // 后台线程 -> 主线程回传进度 private: void initUI(); void showUpdateItems(); - void updateUf2Interfaces(const QStringList& items); - void updateUft3Interfaces(const QStringList& items); - + + // 后台线程执行体(由 QtConcurrent 调用,禁止直接操作 UI) + UpdateResult runUpdateUf2(const QStringList& items); + UpdateResult runUpdateUft3(const QStringList& items); + // 在主线程启动后台任务 + void startUpdate(const QStringList& items, const QString& taskName); + void processUf2StdFields(); void processUf2DataType(); void processUf2ErrNumber(); - void processUf2Interface(const QString& basePath); - + bool processUf2Interface(const QString& basePath); + void processUft3StdFields(); void processUft3DataType(); void processUft3Table(); - void processUft3Interface(const QString& basePath); + bool processUft3Interface(const QString& basePath); QGridLayout* _updateGridLayout{nullptr}; QGridLayout* _updateUf2GridLayout{nullptr}; @@ -60,14 +76,17 @@ private: ElaPushButton* _uft3CancelBtn{nullptr}; ElaPushButton* _uf2AllBtn{nullptr}; ElaPushButton* _uft3UpdateBtn{nullptr}; + ElaCheckBox* _forceFullCheckBox{nullptr}; ElaText* _processLabel{nullptr}; ElaProgressBar* _progressBar{nullptr}; QList uft3CheckBoxes; QList uf20CheckBoxes; - + MetadataProcessor* _processor{nullptr}; - QThread* _workerThread{nullptr}; + QFutureWatcher* _watcher{nullptr}; + QElapsedTimer _updateTimer; + QString m_currentTaskName; // "UF20" / "UFT30",用于区分当前任务 }; -#endif \ No newline at end of file +#endif diff --git a/src/utils/Constants.h b/src/utils/Constants.h index 6dc8cec..5c051b9 100644 --- a/src/utils/Constants.h +++ b/src/utils/Constants.h @@ -14,7 +14,7 @@ namespace Constants { // 应用版本号(发版时只需修改此处) // --------------------------------------------------------------------------- namespace App { - constexpr char Version[] = "1.3.0"; + constexpr char Version[] = "1.4.0"; } // --------------------------------------------------------------------------- @@ -35,6 +35,8 @@ namespace Paths { inline const QString ToolDir = "uf2touft3"; // 工具主目录 inline const QString LogDir = "logs"; // 日志目录 inline const QString CacheDir = "cache"; // 缓存目录 + inline const QString ChangeCodeDir = "change_code"; // 代码检索根目录(toolDir 下) + inline const QString FunctionOutput = "function_output"; // 调用链/文件导出目录 } // --------------------------------------------------------------------------- @@ -67,6 +69,19 @@ inline QString logDir() { return QCoreApplication::applicationDirPath() + " inline QString cacheDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::CacheDir; } inline QString cacheDb() { return cacheDir() + "/" + Cache::DbFile; } +// --------------------------------------------------------------------------- +// 代码检索(CodeSearch)相关常量与路径 +// --------------------------------------------------------------------------- +namespace CodeSearch { + constexpr char ChangeCodeIni[] = "change_code.ini"; // 路径持久化配置文件名 + constexpr char ConvertPathKey[] = "codeSearch/convertPath"; // QSettings 键:转码业务路径 + constexpr char SearchPathKey[] = "codeSearch/searchPath"; // QSettings 键:非转码业务路径 +} + +inline QString changeCodeDir() { return toolDir() + "/" + Paths::ChangeCodeDir; } // 默认代码检索根目录 +inline QString changeCodeIni() { return toolDir() + "/" + QString::fromLatin1(CodeSearch::ChangeCodeIni); } +inline QString functionOutput() { return Paths::FunctionOutput; } // 调用链/文件导出目录名 + } // namespace Constants #endif // CONSTANTS_H diff --git a/src/utils/callchainanalyzer.cpp b/src/utils/callchainanalyzer.cpp new file mode 100644 index 0000000..084d237 --- /dev/null +++ b/src/utils/callchainanalyzer.cpp @@ -0,0 +1,698 @@ +#include "callchainanalyzer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CallChainAnalyzer::CallChainAnalyzer(QObject *parent) + : QObject(parent) +{ +} + +CallChainAnalyzer::~CallChainAnalyzer() = default; + +// 同名冲突时优先保留“真正的实现”而非外部接口声明: +// 外部接口(.extinterface)通常无 ,若同名覆盖实现会让调用链路在叶子处断掉。 +static bool shouldPrefer(const CallChainAnalyzer::FunctionInfo *existing, + const CallChainAnalyzer::FunctionInfo *candidate) +{ + if (existing->isExternal != candidate->isExternal) { + return !candidate->isExternal; // 候选是实现则替换声明 + } + // 同类型时,优先有内部调用的(更能展开链路) + if (existing->calledFunctions.isEmpty() != candidate->calledFunctions.isEmpty()) { + return !candidate->calledFunctions.isEmpty(); + } + return false; // 同优先级,保留先入者 +} + +// 索引时排除的目录(框架/平台目录,无需纳入调用链分析,且体量大、扫描慢) +static bool isExcludedDir(const QString &filePath) +{ + const QStringList parts = QDir::fromNativeSeparators(filePath) + .toLower().split('/', Qt::SkipEmptyParts); + for (const QString &part : parts) { + if (part == "toolset" || part == "ldp_platform") { + return true; + } + } + return false; +} + +void CallChainAnalyzer::setRootPath(const QString &path) +{ + m_rootPath = path; +} + +QString CallChainAnalyzer::rootPath() const +{ + return m_rootPath; +} + +bool CallChainAnalyzer::isIndexed() const +{ + return m_indexed; +} + +int CallChainAnalyzer::functionCount() const +{ + return m_chineseNameIndex.size(); +} + +bool CallChainAnalyzer::buildIndex(QString *errorMessage) +{ + m_chineseNameIndex.clear(); + m_fullNameIndex.clear(); + m_canonicalNameIndex.clear(); + m_indexed = false; + + QDir rootDir(m_rootPath); + if (!rootDir.exists()) { + if (errorMessage) { + *errorMessage = QString("代码目录不存在: %1").arg(m_rootPath); + } + return false; + } + + // 同名冲突时优先保留“真正的实现”而非外部接口声明: + // 外部接口(.extinterface)通常无 ,若同名覆盖实现会让调用链路在叶子处断掉。 + auto insertPrefer = [&](QHash> &idx, + const QString &key, QSharedPointer info) { + auto it = idx.find(key); + if (it == idx.end()) { + idx.insert(key, info); + } else if (shouldPrefer(it.value().data(), info.data())) { + it.value() = info; + } + }; + + // 扫描 .uftfunction、.uftservice、.uftatomfunction、.uftatomservice、.extinterface 五种格式 + static const QStringList kScanExtensions = { + "*.uftfunction", "*.uftservice", + "*.uftatomfunction", "*.uftatomservice", + "*.extinterface" + }; + + int count = 0; + for (const QString &ext : kScanExtensions) { + QDirIterator it(m_rootPath, QStringList() << ext, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + const QString filePath = it.next(); + if (isExcludedDir(filePath)) { + continue; + } + QSharedPointer info = parseFile(filePath); + if (!info || info->chineseName.isEmpty()) { + continue; + } + + // 按 chineseName 索引 + insertPrefer(m_chineseNameIndex, info->chineseName, info); + // 按 objectId-chineseName 索引 + if (!info->objectId.isEmpty()) { + insertPrefer(m_fullNameIndex, info->objectId + "-" + info->chineseName, info); + } + // 按文件 basename(不含扩展名)索引,方便从 cust.json 名称匹配 + if (!info->canonicalName.isEmpty()) { + insertPrefer(m_canonicalNameIndex, info->canonicalName, info); + } + + ++count; + } + } + + m_indexed = true; + rebuildReverseIndex(); + if (errorMessage) { + *errorMessage = QString("已索引 %1 个函数/服务文件").arg(count); + } + return true; +} + +QString CallChainAnalyzer::computeFingerprint(const QString &rootPath) +{ + // 目录内容指纹:文件数量 + 总字节数 + 最新修改时间,用于判断缓存是否失效。 + // 仅读取元数据,不解析内容,开销远小于重新构建索引。 + static const QStringList kScanExtensions = { + "*.uftfunction", "*.uftservice", + "*.uftatomfunction", "*.uftatomservice", + "*.extinterface" + }; + + qint64 totalSize = 0; + qint64 latestMtime = 0; + qint64 count = 0; + + QDirIterator it(rootPath, kScanExtensions, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + const QFileInfo fi(it.next()); + if (isExcludedDir(fi.filePath())) { + continue; + } + totalSize += fi.size(); + const qint64 mtime = fi.lastModified().toSecsSinceEpoch(); + if (mtime > latestMtime) { + latestMtime = mtime; + } + ++count; + } + return QString("%1:%2:%3").arg(count).arg(totalSize).arg(latestMtime); +} + +QString CallChainAnalyzer::cacheFilePathFor(const QString &rootPath) +{ + // 缓存目录:程序目录(bin)下的 cache 子目录 + const QString dir = QDir(QCoreApplication::applicationDirPath()).filePath("cache"); + // 以根路径最后一个目录名作为缓存文件名,便于识别(如 A/B/C/D -> D.json)。 + // 不同路径同名目录的冲突由 loadFromCache 中的 rootPath 校验兜底(失效则重建)。 + const QString base = QDir(rootPath).dirName(); + const QString safe = base.isEmpty() ? QStringLiteral("index") : base; + return QDir(dir).filePath(safe + ".json"); +} + +void CallChainAnalyzer::saveToCache() const +{ + if (!m_indexed || m_rootPath.isEmpty()) { + return; + } + + const QString cacheFile = cacheFilePathFor(m_rootPath); + QDir().mkpath(QFileInfo(cacheFile).absolutePath()); + + QJsonArray funcs; + for (auto it = m_chineseNameIndex.constBegin(); it != m_chineseNameIndex.constEnd(); ++it) { + const FunctionInfo *info = it.value().data(); + QJsonObject o; + o["objectId"] = info->objectId; + o["chineseName"] = info->chineseName; + o["canonicalName"] = info->canonicalName; + o["filePath"] = info->filePath; + o["relDir"] = info->relDir; + o["module"] = info->module; + o["isExternal"] = info->isExternal; + QJsonArray calls; + for (const QString &c : info->calledFunctions) { + calls.append(c); + } + o["calledFunctions"] = calls; + funcs.append(o); + } + + QJsonObject meta; + meta["rootPath"] = m_rootPath; + meta["fingerprint"] = computeFingerprint(m_rootPath); + meta["version"] = 1; + + QJsonObject root; + root["meta"] = meta; + root["functions"] = funcs; + + QSaveFile file(cacheFile); + if (file.open(QIODevice::WriteOnly)) { + file.write(QJsonDocument(root).toJson(QJsonDocument::Compact)); + file.commit(); + } +} + +bool CallChainAnalyzer::loadFromCache(const QString &path, QString *message) +{ + m_chineseNameIndex.clear(); + m_fullNameIndex.clear(); + m_canonicalNameIndex.clear(); + m_indexed = false; + + if (path.isEmpty() || !QDir(path).exists()) { + return false; + } + + const QString cacheFile = cacheFilePathFor(path); + QFile file(cacheFile); + if (!file.exists() || !file.open(QIODevice::ReadOnly)) { + return false; + } + + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + if (!doc.isObject()) { + return false; + } + + const QJsonObject root = doc.object(); + const QJsonObject meta = root.value("meta").toObject(); + // 路径或目录内容发生变化则视为缓存失效,交由调用方重建 + if (meta.value("rootPath").toString() != path) { + return false; + } + if (meta.value("fingerprint").toString() != computeFingerprint(path)) { + return false; + } + + const QJsonArray funcs = root.value("functions").toArray(); + int count = 0; + for (const QJsonValue &v : funcs) { + const QJsonObject o = v.toObject(); + QSharedPointer info(new FunctionInfo()); + info->objectId = o.value("objectId").toString(); + info->chineseName = o.value("chineseName").toString(); + info->canonicalName = o.value("canonicalName").toString(); + info->filePath = o.value("filePath").toString(); + info->relDir = o.value("relDir").toString(); + info->module = o.value("module").toString(); + info->isExternal = o.value("isExternal").toBool(); + const QJsonArray calls = o.value("calledFunctions").toArray(); + for (const QJsonValue &c : calls) { + info->calledFunctions.append(c.toString()); + } + if (info->chineseName.isEmpty()) { + continue; + } + + // 同名冲突同样优先“真正的实现” + auto itCn = m_chineseNameIndex.find(info->chineseName); + if (itCn == m_chineseNameIndex.end()) { + m_chineseNameIndex.insert(info->chineseName, info); + } else if (shouldPrefer(itCn.value().data(), info.data())) { + itCn.value() = info; + } + if (!info->objectId.isEmpty()) { + const QString fullKey = info->objectId + "-" + info->chineseName; + auto itFull = m_fullNameIndex.find(fullKey); + if (itFull == m_fullNameIndex.end()) { + m_fullNameIndex.insert(fullKey, info); + } else if (shouldPrefer(itFull.value().data(), info.data())) { + itFull.value() = info; + } + } + if (!info->canonicalName.isEmpty()) { + auto itCan = m_canonicalNameIndex.find(info->canonicalName); + if (itCan == m_canonicalNameIndex.end()) { + m_canonicalNameIndex.insert(info->canonicalName, info); + } else if (shouldPrefer(itCan.value().data(), info.data())) { + itCan.value() = info; + } + } + ++count; + } + + m_rootPath = path; + m_indexed = true; + rebuildReverseIndex(); + if (message) { + *message = QString("已从缓存加载 %1 个函数/服务文件").arg(count); + } + return true; +} + +QSharedPointer +CallChainAnalyzer::lookupBest(const QString &name) const +{ + // 1) 精确匹配 chineseName + { + auto it = m_chineseNameIndex.find(name); + if (it != m_chineseNameIndex.end()) { + return it.value(); + } + } + + // 2) 精确匹配 canonicalName(文件 basename) + { + auto it = m_canonicalNameIndex.find(name); + if (it != m_canonicalNameIndex.end()) { + return it.value(); + } + } + + // 3) 尝试添加 "_转码" 后缀匹配(部分 .uftservice 的 chineseName 带有 _转码) + { + const QString withSuffix = name + QString::fromUtf8("_转码"); + auto it = m_chineseNameIndex.find(withSuffix); + if (it != m_chineseNameIndex.end()) { + return it.value(); + } + } + + // 4) 尝试去除 "_转码" 后缀匹配 + if (name.endsWith(QString::fromUtf8("_转码"))) { + const QString withoutSuffix = name.chopped(3); // "_转码" = 3 UTF-8 chars → but QString::chopped counts QChars + auto it = m_chineseNameIndex.find(withoutSuffix); + if (it != m_chineseNameIndex.end()) { + return it.value(); + } + } + + // 5) 在 chineseName 中做前缀匹配(如 LS_证券周边_大约可买获取 匹配 LS_证券周边_大约可买获取_转码) + // 注意:name 作为前缀只在它是完整函数名(非已调用子函数名)时才合理, + // 所以这里只做开头前缀匹配避免误匹配 + if (name.length() >= 4) { + // name 必须至少是一个有效函数名的前半部分 + for (auto it = m_chineseNameIndex.constBegin(); + it != m_chineseNameIndex.constEnd(); ++it) { + if (it.key().startsWith(name)) { + // 只接受 chineseName 在 name 后紧跟 "_转码" 或其他已知后缀的情况 + const QString suffix = it.key().mid(name.length()); + if (suffix == QString::fromUtf8("_转码") || suffix == "_转码") { + return it.value(); + } + } + } + } + + return nullptr; +} + +QSharedPointer +CallChainAnalyzer::findByChineseName(const QString &name) const +{ + auto it = m_chineseNameIndex.find(name); + if (it != m_chineseNameIndex.end()) { + return it.value(); + } + return nullptr; +} + +QSharedPointer +CallChainAnalyzer::findByFullName(const QString &fullName) const +{ + auto it = m_fullNameIndex.find(fullName); + if (it != m_fullNameIndex.end()) { + return it.value(); + } + return nullptr; +} + +CallChainAnalyzer::TreeNode +CallChainAnalyzer::buildCallTree(const QString &startChineseName, int maxDepth) const +{ + TreeNode root; + QSet visited; + buildNode(startChineseName, root, visited, 0, maxDepth, true); + return root; +} + +void CallChainAnalyzer::resolveNode(const QString &name, TreeNode &node, bool isRoot) const +{ + // 使用多策略查找,而非仅 chineseName 精确匹配 + QSharedPointer info = lookupBest(name); + if (info) { + node.info = info.data(); + node.objectId = info->objectId; + node.displayName = info->objectId.isEmpty() + ? info->chineseName + : QString("%1-%2").arg(info->objectId, info->chineseName); + node.state = "ok"; + } else { + node.displayName = name; + // 根节点未找到:整个函数不在索引中 + // 子调用未找到:说明该函数无需转码,原代码已支持 + node.state = isRoot ? "notfound" : "supported"; + // 若调用名称本身以 "功能号-中文名" 形式传入,解析出功能号用于导出展示 + int dash = name.indexOf('-'); + if (dash > 0) { + node.objectId = name.left(dash); + } + } +} + +CallChainAnalyzer::TreeNode CallChainAnalyzer::makeNode(const QString &name, bool isRoot) const +{ + TreeNode node; + resolveNode(name, node, isRoot); + return node; +} + +void CallChainAnalyzer::buildNode(const QString &name, TreeNode &node, + QSet &visited, int depth, + int maxDepth, bool isRoot) const +{ + resolveNode(name, node, isRoot); + + if (depth >= maxDepth) { + node.state = "depthlimit"; + return; + } + + if (visited.contains(name)) { + node.state = "cycle"; + return; + } + + visited.insert(name); + + QStringList calls; + if (node.info) { + calls = node.info->calledFunctions; + } + + QSet seen; + for (const QString &call : calls) { + if (seen.contains(call)) { + continue; + } + seen.insert(call); + + TreeNode child; + buildNode(call, child, visited, depth + 1, maxDepth); + node.children.append(child); + } + + visited.remove(name); +} + +void CallChainAnalyzer::rebuildReverseIndex() +{ + m_reverseIndex.clear(); + for (auto it = m_chineseNameIndex.constBegin(); it != m_chineseNameIndex.constEnd(); ++it) { + QSharedPointer info = it.value(); + // 同一“被调用名”下,按调用方 canonicalName 去重,避免同一函数多次调用产生重复链 + QHash> seenByKey; + for (const QString &call : info->calledFunctions) { + // 用与正向查找一致的方式解析被调用名,使“往前查”时能正确命中 + QSharedPointer target = lookupBest(call); + // 以多种方式建键,最大化“往前查”命中率: + // 解析到的 chineseName / 解析到的 canonicalName / 原始调用名 + QStringList keys; + if (target) { + keys.append(target->chineseName); + if (!target->canonicalName.isEmpty()) { + keys.append(target->canonicalName); + } + } + keys.append(call); + for (const QString &k : keys) { + if (k.isEmpty() || seenByKey[k].contains(info->canonicalName)) { + continue; + } + seenByKey[k].insert(info->canonicalName); + m_reverseIndex[k].append(info); + } + } + } +} + +QList +CallChainAnalyzer::buildCallerForest(const QString &startName, const TreeNode &downTree, + int maxDepth) const +{ + QList roots; + QSet visited; + + // 将 startName 归一化为其在索引中的 chineseName,保证与反向索引键一致。 + // 否则当用户以 basename / 功能号前缀等形式查询(正向可命中)时,反向查找会因键不一致而失效。 + QString startKey = startName; + { + QSharedPointer startInfo = lookupBest(startName); + if (startInfo) { + startKey = startInfo->chineseName; + } + } + + // 递归构造“上游”链:children 为调用方(caller),链末端为 startName + auto recurse = [&](auto &self, const QString &name, QSet vis, int depth) -> QList { + QList out; + const auto it = m_reverseIndex.find(name); + QList> callers; + if (it != m_reverseIndex.end()) { + callers = it.value(); + } + + if (callers.isEmpty() || depth >= maxDepth) { + // 顶层调用方(无人再调用它):作为返回链的根 + TreeNode n = makeNode(name, name == startKey); + if (name == startKey) { + n = downTree; // 起始函数节点直接用下游树(自带内部调用) + } + out.append(n); + return out; + } + + for (const QSharedPointer &C : callers) { + const QString cName = C->chineseName; + if (vis.contains(cName)) { + // 循环调用:标记并收尾 + TreeNode cn = makeNode(cName, false); + cn.state = "cycle"; + if (name == startKey) { + cn.children.append(downTree); + } else { + cn.children.append(makeNode(name, false)); + } + out.append(cn); + continue; + } + QSet nextVis = vis; + nextVis.insert(name); + QList sub = self(self, cName, nextVis, depth + 1); + for (TreeNode &s : sub) { + if (name == startKey) { + s.children.append(downTree); + } else { + s.children.append(makeNode(name, false)); + } + out.append(s); + } + } + return out; + }; + + return recurse(recurse, startKey, visited, 0); +} + +QSharedPointer +CallChainAnalyzer::parseFile(const QString &filePath) const +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return nullptr; + } + + QSharedPointer info(new FunctionInfo()); + info->filePath = filePath; + + const QFileInfo fi(filePath); + info->canonicalName = fi.completeBaseName(); // 不含扩展名的文件名 + + QDir rootDir(m_rootPath); + info->relDir = rootDir.relativeFilePath(fi.path()); + info->relDir = QDir::fromNativeSeparators(info->relDir); + if (info->relDir.isEmpty()) { + info->relDir = "."; + } + + const QStringList dirParts = info->relDir.split('/', Qt::SkipEmptyParts); + info->module = dirParts.isEmpty() ? QString() : dirParts.first(); + + QXmlStreamReader reader(&file); + bool inCodeElement = false; + bool isRoot = true; + QString codeText; + + while (!reader.atEnd() && !reader.hasError()) { + reader.readNext(); + + if (reader.isStartElement()) { + const QString elementName = reader.name().toString(); + if (isRoot) { + // 优先从根元素属性读取对象名/中文名, + // 兼容 / 以及 .extinterface 等其它带属性格式 + if (reader.attributes().hasAttribute("chineseName")) { + info->chineseName = reader.attributes().value("chineseName").toString(); + } + if (reader.attributes().hasAttribute("objectId")) { + info->objectId = reader.attributes().value("objectId").toString(); + } + } else if (elementName == "Function" || elementName == "Service") { + // 兜底:对象信息写在嵌套元素的情况 + if (info->chineseName.isEmpty()) { + info->chineseName = reader.attributes().value("chineseName").toString(); + } + if (info->objectId.isEmpty()) { + info->objectId = reader.attributes().value("objectId").toString(); + } + } else if (elementName == "code") { + inCodeElement = true; + } + isRoot = false; + } else if (reader.isCharacters() && inCodeElement) { + codeText += reader.text().toString(); + } else if (reader.isEndElement()) { + if (reader.name().toString() == "code") { + inCodeElement = false; + } + } + } + + if (reader.hasError()) { + return nullptr; + } + + info->calledFunctions = extractFunctionCalls(codeText); + // 标记是否为外部接口声明文件(.extinterface 无 ,仅作为调用声明) + info->isExternal = (fi.suffix().compare("extinterface", Qt::CaseInsensitive) == 0); + return info; +} + +QStringList CallChainAnalyzer::extractFunctionCalls(const QString &code) +{ + QStringList result; + QSet seen; + + // 先去除注释(// 行注释、/* */ 块注释),避免把注释里提到的函数当成真实调用 + const QString cleaned = stripComments(code); + + // 格式 1: [函数名] 或 [函数名](.uftfunction 中的调用格式) + { + static const QRegularExpression callRe1(R"(<(I|MI)>\s*\[([^\]]+)\])"); + QRegularExpressionMatchIterator it = callRe1.globalMatch(cleaned); + while (it.hasNext()) { + const QRegularExpressionMatch match = it.next(); + const QString name = match.captured(2).trimmed(); + if (!name.isEmpty() && !seen.contains(name)) { + if (name.startsWith("LS_") || name.startsWith("LF_") || + name.startsWith("AS_") || name.startsWith("AF_")) { + seen.insert(name); + result.append(name); + } + } + } + } + + // 格式 2: [LS_xxx] / [LF_xxx] / [AS_xxx] / [AF_xxx](.uftservice 中的调用格式) + // 如 [LF_证券周边_大约可买获取4][...参数...] + { + static const QRegularExpression callRe2( + R"(\[(L[SF]_[^\]]+|A[SF]_[^\]]+)\])"); + QRegularExpressionMatchIterator it = callRe2.globalMatch(cleaned); + while (it.hasNext()) { + const QRegularExpressionMatch match = it.next(); + const QString name = match.captured(1).trimmed(); + if (!name.isEmpty() && !seen.contains(name)) { + if (name.startsWith("LS_") || name.startsWith("LF_") || + name.startsWith("AS_") || name.startsWith("AF_")) { + seen.insert(name); + result.append(name); + } + } + } + } + + return result; +} + +QString CallChainAnalyzer::stripComments(const QString &code) +{ + QString cleaned = code; + // 去除 /* ... */ 块注释(跨行、非贪婪) + cleaned.remove(QRegularExpression(R"(/\*.*?\*/)", + QRegularExpression::DotMatchesEverythingOption)); + // 去除 // 行注释(到行尾) + cleaned.remove(QRegularExpression(R"(//[^\n]*)")); + return cleaned; +} diff --git a/src/utils/callchainanalyzer.h b/src/utils/callchainanalyzer.h new file mode 100644 index 0000000..17e1a1a --- /dev/null +++ b/src/utils/callchainanalyzer.h @@ -0,0 +1,101 @@ +#ifndef CALLCHAINANALYZER_H +#define CALLCHAINANALYZER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +class CallChainAnalyzer : public QObject +{ + Q_OBJECT + +public: + struct FunctionInfo + { + QString objectId; // XML 中的 objectId + QString chineseName; // XML 中的 chineseName,如 LF_证券公用_大约可买获取2 + QString canonicalName; // 文件 basename(不含扩展名),如 LS_SESEXT_ALMOSTBUY_REQ + QString filePath; // 文件绝对路径 + QString relDir; // 文件所在目录(相对于搜索根目录) + QString module; // 模块:取 relDir 的第一级目录,如 ucbp + QStringList calledFunctions; // 本函数代码中调用的函数中文名列表 + bool isExternal = false; // 是否为外部接口声明文件(.extinterface) + }; + + struct TreeNode + { + const FunctionInfo *info = nullptr; + QString displayName; + QString objectId; // 功能号(supported 节点尽量从名称中解析) + QString state; // ok / notfound / cycle / depthlimit + QList children; + }; + + explicit CallChainAnalyzer(QObject *parent = nullptr); + ~CallChainAnalyzer(); + + void setRootPath(const QString &path); + QString rootPath() const; + + bool isIndexed() const; + int functionCount() const; + + // 构建索引:扫描根目录下所有 .uftfunction 文件 + bool buildIndex(QString *errorMessage = nullptr); + + // 缓存:将已构建的索引持久化到磁盘,避免每次打开都重新扫描目录 + // 加载时按 rootPath + 目录指纹校验,命中则直接反序列化,否则返回 false 由调用方重建 + bool loadFromCache(const QString &path, QString *message = nullptr); + void saveToCache() const; + + // 根据函数中文名查找 + QSharedPointer findByChineseName(const QString &name) const; + // 根据 objectId-chineseName 查找 + QSharedPointer findByFullName(const QString &fullName) const; + + // 多策略查找:先精确匹配 chineseName,再尝试 basename、去后缀等。 + // 与 resolveNode 使用同一解析入口,供外部还原节点的展示名(如蓝底高亮匹配)。 + QSharedPointer lookupBest(const QString &name) const; + + // 构建从 startChineseName 出发的调用链路树(下游:被其调用的函数) + TreeNode buildCallTree(const QString &startChineseName, int maxDepth = 32) const; + + // 构建“往前/反向”调用链路森林:返回所有上游调用方链的根节点集合, + // 每条链都以 startName 收尾,并将 downTree(startName 的下游树)挂在其末端。 + // 例:A 被 AA/AB/AC 调用,AA 被 AAA 调用,则返回 AAA→AA→A、AB→A、AC→A 三条链。 + QList buildCallerForest(const QString &startName, const TreeNode &downTree, + int maxDepth = 32) const; + +private: + QSharedPointer parseFile(const QString &filePath) const; + void buildNode(const QString &name, TreeNode &node, QSet &visited, + int depth, int maxDepth, bool isRoot = false) const; + // 仅解析单个节点(定位函数并填充 info/displayName/state),不展开子调用 + void resolveNode(const QString &name, TreeNode &node, bool isRoot) const; + // 由函数名构造一个已解析的节点 + TreeNode makeNode(const QString &name, bool isRoot) const; + // 由正向索引反推调用方索引:calledName -> 调用它的函数列表 + void rebuildReverseIndex(); + + static QStringList extractFunctionCalls(const QString &code); + static QString stripComments(const QString &code); + + // 缓存辅助:根路径 -> 缓存文件路径(按路径哈希命名),以及目录内容指纹(用于失效判断) + static QString cacheFilePathFor(const QString &rootPath); + static QString computeFingerprint(const QString &rootPath); + + QString m_rootPath; + QHash> m_chineseNameIndex; + QHash> m_fullNameIndex; + QHash> m_canonicalNameIndex; + // 反向索引:被调用函数名 -> 调用它的函数列表(用于“往前查”) + QHash>> m_reverseIndex; + bool m_indexed = false; +}; + +#endif // CALLCHAINANALYZER_H diff --git a/update_info.json b/update_info.json index 20bf812..3956c0e 100644 --- a/update_info.json +++ b/update_info.json @@ -1,6 +1,6 @@ { "V1.0.0": { - "release_notes": "1. 支持业务转码;\n2. 支持代码uf20、uft3功能查找;\n3.支持帮助菜单和在线帮助文档;\n4.支持接口元数据更新;\n5.设置菜单UFT30项目环境路径、UF20项目环境路径、UF20账户环境路径、转码生成路径、支持弹窗选择目录。" + "release_notes": "1.支持业务转码;\n2.支持代码uf20、uft3功能查找;\n3.支持帮助菜单和在线帮助文档;\n4.支持接口元数据更新;\n5.设置菜单UFT30项目环境路径、UF20项目环境路径、UF20账户环境路径、转码生成路径、支持弹窗选择目录。" }, "V1.1.0": { "release_notes": "1.支持2.0账户功能查询及业务转码" @@ -10,5 +10,8 @@ }, "V1.3.0": { "release_notes": "1.修复设置路径不生效;\n2.修复元数据更新后业务转码模块对函数检查状态异常问题;" - } + }, + "V1.4.0": { + "release_notes": "1.提升业务转码性能;\n2.元数据更新支持增量更新和强制全量更新;\n3.新增代码检索功能;\n4.支持函数调用链路分析;\n5.支持全局文字可选中复制及表格Ctrl+C导出;" + } }