新增代码检索功能、提升业务转码效率等
This commit is contained in:
parent
b86fc071c2
commit
389c6eeecd
|
|
@ -1,4 +1,4 @@
|
||||||
QT += core gui widgets sql network
|
QT += core gui widgets sql network concurrent
|
||||||
|
|
||||||
TARGET = Uft30ChangeCode
|
TARGET = Uft30ChangeCode
|
||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
|
|
@ -10,6 +10,7 @@ SOURCES += main.cpp \
|
||||||
src/pythonrunner/PythonRunner.cpp \
|
src/pythonrunner/PythonRunner.cpp \
|
||||||
src/pages/about/aboutdialog.cpp \
|
src/pages/about/aboutdialog.cpp \
|
||||||
src/pages/batchconvert/batchconvertpage.cpp \
|
src/pages/batchconvert/batchconvertpage.cpp \
|
||||||
|
src/pages/codesearch/codesearchpage.cpp \
|
||||||
src/pages/functionsearch/functionsearchpage.cpp \
|
src/pages/functionsearch/functionsearchpage.cpp \
|
||||||
src/pages/functionsearch/uf20functionsearchpage.cpp \
|
src/pages/functionsearch/uf20functionsearchpage.cpp \
|
||||||
src/pages/functionsearch/uft3functionsearchpage.cpp \
|
src/pages/functionsearch/uft3functionsearchpage.cpp \
|
||||||
|
|
@ -34,6 +35,7 @@ SOURCES += main.cpp \
|
||||||
src/utils/uft3configreader.cpp \
|
src/utils/uft3configreader.cpp \
|
||||||
src/utils/logmanager.cpp \
|
src/utils/logmanager.cpp \
|
||||||
src/utils/configmanager.cpp \
|
src/utils/configmanager.cpp \
|
||||||
|
src/utils/callchainanalyzer.cpp \
|
||||||
src/utils/version.cpp \
|
src/utils/version.cpp \
|
||||||
src/utils/versionchecker.cpp
|
src/utils/versionchecker.cpp
|
||||||
|
|
||||||
|
|
@ -41,6 +43,7 @@ HEADERS += src/mainwindow/mainwindow.h \
|
||||||
src/pythonrunner/PythonRunner.h \
|
src/pythonrunner/PythonRunner.h \
|
||||||
src/pages/about/aboutdialog.h \
|
src/pages/about/aboutdialog.h \
|
||||||
src/pages/batchconvert/batchconvertpage.h \
|
src/pages/batchconvert/batchconvertpage.h \
|
||||||
|
src/pages/codesearch/codesearchpage.h \
|
||||||
src/pages/functionsearch/functionsearchpage.h \
|
src/pages/functionsearch/functionsearchpage.h \
|
||||||
src/pages/functionsearch/uf20functionsearchpage.h \
|
src/pages/functionsearch/uf20functionsearchpage.h \
|
||||||
src/pages/functionsearch/uft3functionsearchpage.h \
|
src/pages/functionsearch/uft3functionsearchpage.h \
|
||||||
|
|
@ -65,6 +68,7 @@ HEADERS += src/mainwindow/mainwindow.h \
|
||||||
src/utils/uft3configreader.h \
|
src/utils/uft3configreader.h \
|
||||||
src/utils/logmanager.h \
|
src/utils/logmanager.h \
|
||||||
src/utils/configmanager.h \
|
src/utils/configmanager.h \
|
||||||
|
src/utils/callchainanalyzer.h \
|
||||||
src/utils/version.h \
|
src/utils/version.h \
|
||||||
src/utils/versionchecker.h
|
src/utils/versionchecker.h
|
||||||
|
|
||||||
|
|
|
||||||
10
build.bat
10
build.bat
|
|
@ -76,6 +76,16 @@ if errorlevel 1 (
|
||||||
)
|
)
|
||||||
|
|
||||||
echo.
|
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 ==========================================
|
||||||
echo Build SUCCESSFUL!
|
echo Build SUCCESSFUL!
|
||||||
echo ==========================================
|
echo ==========================================
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
</toc-element>
|
</toc-element>
|
||||||
<toc-element topic="功能转码.md">
|
<toc-element topic="功能转码.md">
|
||||||
<toc-element topic="业务转码.md"/>
|
<toc-element topic="业务转码.md"/>
|
||||||
|
<toc-element topic="代码检索.md"/>
|
||||||
</toc-element>
|
</toc-element>
|
||||||
<toc-element topic="功能查询.md">
|
<toc-element topic="功能查询.md">
|
||||||
<toc-element topic="UFT3功能查询.md"/>
|
<toc-element topic="UFT3功能查询.md"/>
|
||||||
|
|
@ -21,7 +22,8 @@
|
||||||
</toc-element>
|
</toc-element>
|
||||||
<toc-element topic="元数据更新.md"/>
|
<toc-element topic="元数据更新.md"/>
|
||||||
<toc-element topic="更新记录.md">
|
<toc-element topic="更新记录.md">
|
||||||
<toc-element topic="1-3-0.md"/>
|
<toc-element topic="update-1-4-0.md"/>
|
||||||
|
<toc-element topic="update-1-3-0.md"/>
|
||||||
<toc-element topic="update-1-2-0.md"/>
|
<toc-element topic="update-1-2-0.md"/>
|
||||||
<toc-element topic="update-1-1-0.md"/>
|
<toc-element topic="update-1-1-0.md"/>
|
||||||
<toc-element topic="update-1-0-0.md"/>
|
<toc-element topic="update-1-0-0.md"/>
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
|
|
@ -0,0 +1,11 @@
|
||||||
|
# 1.4.0
|
||||||
|
|
||||||
|
1.提升业务转码性能;
|
||||||
|
|
||||||
|
2.元数据更新支持增量更新和强制全量更新;
|
||||||
|
|
||||||
|
3.新增代码检索功能;
|
||||||
|
|
||||||
|
4.支持函数调用链路分析;
|
||||||
|
|
||||||
|
5.支持全局文字可选中复制及表格Ctrl+C导出;
|
||||||
|
|
@ -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.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.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) |
|
| 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) |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# 代码检索
|
||||||
|
|
||||||
|
支持"转码业务"、"非转码业务"调用链路查询和导出
|
||||||
|
|
||||||
|
## 转码业务
|
||||||
|
支持查询调用链路和导出查询函数所涉及的文件以及调用链路导出
|
||||||
|
<img src="转码业务.png" alt="转码业务"/>
|
||||||
|
|
||||||
|
## 非转码业务
|
||||||
|
支持查询调用链路和调用链路导出
|
||||||
|
<img src="非转码业务.png" alt="非转码业务"/>
|
||||||
|
|
@ -30,3 +30,5 @@
|
||||||
|
|
||||||
## 快速跳转
|
## 快速跳转
|
||||||
[业务转码](http://10.20.163.105:6045/uft3changecode/业务转码.html)
|
[业务转码](http://10.20.163.105:6045/uft3changecode/业务转码.html)
|
||||||
|
|
||||||
|
[代码检索](http://10.20.163.105:6045/uft3changecode/业务转码.html)
|
||||||
|
|
|
||||||
87
main.cpp
87
main.cpp
|
|
@ -4,11 +4,96 @@
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QKeySequence>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QAbstractItemView>
|
||||||
|
#include <QModelIndex>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
// 全局文字可复制过滤器:
|
||||||
|
// 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<QLabel *>(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<QAbstractItemView *>(watched)) {
|
||||||
|
auto *ke = static_cast<QKeyEvent *>(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[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
QApplication a(argc, argv);
|
QApplication a(argc, argv);
|
||||||
a.setWindowIcon(QIcon(":resources/images/ChangeCode.png"));
|
a.setWindowIcon(QIcon(":resources/images/ChangeCode.png"));
|
||||||
|
|
||||||
|
// 安装全局过滤器:所有页面文字 / 表格树单元格允许用户复制
|
||||||
|
TextSelectableFilter textSelectableFilter;
|
||||||
|
a.installEventFilter(&textSelectableFilter);
|
||||||
|
|
||||||
eApp->init();
|
eApp->init();
|
||||||
|
|
||||||
QString basePath = QCoreApplication::applicationDirPath();
|
QString basePath = QCoreApplication::applicationDirPath();
|
||||||
|
|
@ -17,4 +102,4 @@ int main(int argc, char *argv[])
|
||||||
MainWindow w;
|
MainWindow w;
|
||||||
w.showMaximized();
|
w.showMaximized();
|
||||||
return a.exec();
|
return a.exec();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,7 @@
|
||||||
<file>resources/images/max.png</file>
|
<file>resources/images/max.png</file>
|
||||||
<file>resources/images/min.png</file>
|
<file>resources/images/min.png</file>
|
||||||
<file>resources/images/update.png</file>
|
<file>resources/images/update.png</file>
|
||||||
|
<file>resources/images/expand_all.png</file>
|
||||||
|
<file>resources/images/collapse_all.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -40,6 +40,9 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
, m_iconsSet(false)
|
, m_iconsSet(false)
|
||||||
, m_navigationButton(nullptr)
|
, m_navigationButton(nullptr)
|
||||||
, m_isNavigationExpanded(true)
|
, m_isNavigationExpanded(true)
|
||||||
|
, m_totalFunctions(0)
|
||||||
|
, m_currentFunctionIndex(0)
|
||||||
|
, m_lastProgress(0)
|
||||||
{
|
{
|
||||||
initWindow();
|
initWindow();
|
||||||
initContent();
|
initContent();
|
||||||
|
|
@ -133,7 +136,8 @@ void MainWindow::initContent()
|
||||||
{
|
{
|
||||||
addExpanderNode("功能转码", m_funcConvertKey, ElaIconType::FileCode);
|
addExpanderNode("功能转码", m_funcConvertKey, ElaIconType::FileCode);
|
||||||
addPageNode("业务转码", createBatchConvertPage(), m_funcConvertKey, ElaIconType::ArrowsRepeat);
|
addPageNode("业务转码", createBatchConvertPage(), m_funcConvertKey, ElaIconType::ArrowsRepeat);
|
||||||
|
addPageNode("代码检索", createCodeSearchPage(), m_funcConvertKey, ElaIconType::Cubes);
|
||||||
|
|
||||||
addExpanderNode("功能查询", m_funcSearchKey, ElaIconType::MagnifyingGlass);
|
addExpanderNode("功能查询", m_funcSearchKey, ElaIconType::MagnifyingGlass);
|
||||||
addPageNode("UF20功能查询", createUF20FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
|
addPageNode("UF20功能查询", createUF20FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
|
||||||
addPageNode("UFT3功能查询", createUFT3FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
|
addPageNode("UFT3功能查询", createUFT3FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
|
||||||
|
|
@ -157,6 +161,12 @@ QWidget* MainWindow::createBatchConvertPage()
|
||||||
return m_batchConvertPage;
|
return m_batchConvertPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QWidget* MainWindow::createCodeSearchPage()
|
||||||
|
{
|
||||||
|
_codeSearchPage = new CodeSearchPage;
|
||||||
|
return _codeSearchPage;
|
||||||
|
}
|
||||||
|
|
||||||
QWidget* MainWindow::createFunctionSearchPage()
|
QWidget* MainWindow::createFunctionSearchPage()
|
||||||
{
|
{
|
||||||
return new FunctionSearchPage;
|
return new FunctionSearchPage;
|
||||||
|
|
@ -222,6 +232,9 @@ void MainWindow::onBatchConvertStart()
|
||||||
m_currentFunctionIndex = 0;
|
m_currentFunctionIndex = 0;
|
||||||
m_functionList.clear();
|
m_functionList.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进度基准:BatchConvertPage 在开始时已将进度条置为 20(配置加载完成)
|
||||||
|
m_lastProgress = 20;
|
||||||
|
|
||||||
LogManager::instance()->log("========================================");
|
LogManager::instance()->log("========================================");
|
||||||
LogManager::instance()->log("开始执行 UFT2 到 UFT3 的代码转换...");
|
LogManager::instance()->log("开始执行 UFT2 到 UFT3 的代码转换...");
|
||||||
|
|
@ -255,6 +268,9 @@ void MainWindow::onPythonRunnerFinished(int exitCode, QProcess::ExitStatus exitS
|
||||||
LogManager::instance()->log("========================================");
|
LogManager::instance()->log("========================================");
|
||||||
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
|
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
|
||||||
LogManager::instance()->log("✓ 代码转换完成!");
|
LogManager::instance()->log("✓ 代码转换完成!");
|
||||||
|
if (_codeSearchPage) {
|
||||||
|
_codeSearchPage->refreshIndexSilent();
|
||||||
|
}
|
||||||
QMessageBox::information(this, "完成", "代码转换任务已成功完成!");
|
QMessageBox::information(this, "完成", "代码转换任务已成功完成!");
|
||||||
} else {
|
} else {
|
||||||
LogManager::instance()->logError(QString("✗ 转换失败! 退出码: %1").arg(exitCode));
|
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)
|
void MainWindow::onPythonRunnerOutput(const QString &output)
|
||||||
{
|
{
|
||||||
if (!output.trimmed().isEmpty()) {
|
if (output.trimmed().isEmpty()) {
|
||||||
LogManager::instance()->log(output);
|
return;
|
||||||
|
}
|
||||||
QString trimmedOutput = output.trimmed();
|
|
||||||
|
LogManager::instance()->log(output);
|
||||||
QRegularExpression progressRegExp(R"(progress\s*[:=]\s*(\d+))");
|
|
||||||
QRegularExpressionMatch match = progressRegExp.match(trimmedOutput);
|
const QString text = output.trimmed();
|
||||||
if (match.hasMatch()) {
|
|
||||||
int progress = match.captured(1).toInt();
|
// 计算本次输出对应的目标进度;-1 表示本次输出不含可识别的进度标记
|
||||||
QString status = trimmedOutput;
|
int targetProgress = -1;
|
||||||
if (status.contains("progress", Qt::CaseInsensitive)) {
|
QString status;
|
||||||
status = status.replace(progressRegExp, "").trimmed();
|
|
||||||
if (status.isEmpty()) {
|
// 1) 优先解析工具显式输出的 progress:xx / progress=xx
|
||||||
status = QString("转换中 %1%").arg(progress);
|
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);
|
|
||||||
}
|
// 3) 解析流水线阶段标记(关键字命中取最大值,保证进度稳定推进)
|
||||||
} else {
|
if (targetProgress < 0) {
|
||||||
if (m_batchConvertPage) {
|
struct Marker { const char* key; int prog; const char* label; };
|
||||||
QString currentFunction;
|
static const Marker markers[] = {
|
||||||
QRegularExpression funcRegExp(R"(([\w_]+))");
|
{"开始加载数据", 25, "正在加载数据..."},
|
||||||
QRegularExpressionMatch funcMatch = funcRegExp.match(trimmedOutput);
|
{"配置文件加载完成", 30, "配置文件加载完成"},
|
||||||
if (funcMatch.hasMatch()) {
|
{"元数据加载完成", 35, "元数据加载完成"},
|
||||||
currentFunction = funcMatch.captured(1);
|
{"全部数据加载完成", 42, "数据加载完成"},
|
||||||
}
|
{"数据初始化完成", 45, "数据初始化完成"},
|
||||||
|
{"校验完成", 52, "数据校验完成"},
|
||||||
if (!currentFunction.isEmpty() && m_totalFunctions > 0) {
|
{"翻译完成", 80, "代码翻译完成"},
|
||||||
int index = m_functionList.indexOf(currentFunction);
|
{"后处理完成", 90, "后处理完成"},
|
||||||
if (index >= 0 && index >= m_currentFunctionIndex) {
|
{"输出完成", 96, "结果输出完成"},
|
||||||
m_currentFunctionIndex = index + 1;
|
{"转换流水线执行成功", 98, "转换流水线执行成功"},
|
||||||
int progress = 20 + (m_currentFunctionIndex * 80) / m_totalFunctions;
|
{"转码完成", 98, "转码完成"},
|
||||||
QString status = QString("正在转换: %1").arg(currentFunction);
|
};
|
||||||
m_batchConvertPage->onProgressUpdate(progress, status);
|
for (const auto &m : markers) {
|
||||||
return;
|
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)
|
void MainWindow::onPythonRunnerError(const QString &error)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include "src/pythonrunner/PythonRunner.h"
|
#include "src/pythonrunner/PythonRunner.h"
|
||||||
#include "src/pages/batchconvert/batchconvertpage.h"
|
#include "src/pages/batchconvert/batchconvertpage.h"
|
||||||
|
#include "src/pages/codesearch/codesearchpage.h"
|
||||||
#include "src/pages/functionsearch/functionsearchpage.h"
|
#include "src/pages/functionsearch/functionsearchpage.h"
|
||||||
#include "src/pages/functionsearch/uf20functionsearchpage.h"
|
#include "src/pages/functionsearch/uf20functionsearchpage.h"
|
||||||
#include "src/pages/functionsearch/uft3functionsearchpage.h"
|
#include "src/pages/functionsearch/uft3functionsearchpage.h"
|
||||||
|
|
@ -30,6 +31,7 @@ private:
|
||||||
void initContent();
|
void initContent();
|
||||||
|
|
||||||
QWidget* createBatchConvertPage();
|
QWidget* createBatchConvertPage();
|
||||||
|
QWidget* createCodeSearchPage();
|
||||||
QWidget* createFunctionSearchPage();
|
QWidget* createFunctionSearchPage();
|
||||||
QWidget* createUF20FunctionSearchPage();
|
QWidget* createUF20FunctionSearchPage();
|
||||||
QWidget* createUFT3FunctionSearchPage();
|
QWidget* createUFT3FunctionSearchPage();
|
||||||
|
|
@ -58,9 +60,11 @@ private:
|
||||||
ElaToolButton* m_navigationButton;
|
ElaToolButton* m_navigationButton;
|
||||||
bool m_isNavigationExpanded;
|
bool m_isNavigationExpanded;
|
||||||
BatchConvertPage *m_batchConvertPage;
|
BatchConvertPage *m_batchConvertPage;
|
||||||
|
CodeSearchPage *_codeSearchPage;
|
||||||
|
|
||||||
int m_totalFunctions;
|
int m_totalFunctions;
|
||||||
int m_currentFunctionIndex;
|
int m_currentFunctionIndex;
|
||||||
|
int m_lastProgress; // 已上报的最大进度,保证进度单调递增,避免进度条来回跳动
|
||||||
QStringList m_functionList;
|
QStringList m_functionList;
|
||||||
QString m_helpKey;
|
QString m_helpKey;
|
||||||
QString m_aboutKey;
|
QString m_aboutKey;
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,20 @@
|
||||||
#include "utils/configmanager.h"
|
#include "utils/configmanager.h"
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QSet>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
|
||||||
MetadataProcessor::MetadataProcessor(QObject *parent) : QObject(parent)
|
MetadataProcessor::MetadataProcessor(QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MetadataProcessor::setForceFull(bool force)
|
||||||
|
{
|
||||||
|
m_forceFull = force;
|
||||||
|
}
|
||||||
|
|
||||||
void MetadataProcessor::initConfig(const QString& basePath)
|
void MetadataProcessor::initConfig(const QString& basePath)
|
||||||
{
|
{
|
||||||
m_basePath = basePath;
|
m_basePath = basePath;
|
||||||
|
|
@ -181,107 +188,194 @@ void MetadataProcessor::setBasePath(const QString& path)
|
||||||
m_basePath = 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)
|
bool MetadataProcessor::processUf2Interfaces(const QString& basePath)
|
||||||
{
|
{
|
||||||
LogManager::instance()->log("========== 开始处理 UF2.0 接口 ==========");
|
LogManager::instance()->log("========== 开始处理 UF2.0 接口 ==========");
|
||||||
LogManager::instance()->log(QString("基础路径(basePath): %1").arg(basePath));
|
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 completeNameList = {"module.xml", ".project", "冲突检测.wlua"};
|
||||||
QStringList containNameList = {".vbusiness", "/公共资源/", "/数据库/", "/脚本/", "/通用数据/", "/.svn/"};
|
QStringList containNameList = {".vbusiness", "/公共资源/", "/数据库/", "/脚本/", "/通用数据/", "/.svn/"};
|
||||||
|
|
||||||
QMap<QString, QStringList> filterDict;
|
QMap<QString, QStringList> filterDict;
|
||||||
filterDict["completeNameList"] = completeNameList;
|
filterDict["completeNameList"] = completeNameList;
|
||||||
filterDict["containNameList"] = containNameList;
|
filterDict["containNameList"] = containNameList;
|
||||||
|
|
||||||
QString dirPath = m_uf20Path;
|
QString dirPath = m_uf20Path;
|
||||||
QString ufAcct20ProjectDir = m_ufAcct20Path;
|
QString ufAcct20ProjectDir = m_ufAcct20Path;
|
||||||
QString uftDbSett = m_uftDbSettPath;
|
QString uftDbSett = m_uftDbSettPath;
|
||||||
|
|
||||||
QString supportUftdbSett = m_supportUftdbSett;
|
QString supportUftdbSett = m_supportUftdbSett;
|
||||||
if (supportUftdbSett == "0") {
|
if (supportUftdbSett == "0") {
|
||||||
uftDbSett.clear();
|
uftDbSett.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
LogManager::instance()->log(QString("UF20 项目路径: %1").arg(dirPath));
|
LogManager::instance()->log(QString("UF20 项目路径: %1").arg(dirPath));
|
||||||
LogManager::instance()->log(QString("UF20 会计项目路径: %1").arg(ufAcct20ProjectDir));
|
LogManager::instance()->log(QString("UF20 会计项目路径: %1").arg(ufAcct20ProjectDir));
|
||||||
LogManager::instance()->log(QString("UFT 数据库设置路径: %1").arg(uftDbSett));
|
LogManager::instance()->log(QString("UFT 数据库设置路径: %1").arg(uftDbSett));
|
||||||
|
|
||||||
QString custJsonPath = QDir(m_uf2touft3Path).filePath("cust.json");
|
QString custJsonPath = QDir(m_uf2touft3Path).filePath("cust.json");
|
||||||
LogManager::instance()->log(QString("客户配置文件路径: %1").arg(custJsonPath));
|
|
||||||
QJsonObject custJson = readJson(custJsonPath);
|
QJsonObject custJson = readJson(custJsonPath);
|
||||||
|
|
||||||
Uf2Interface uf2;
|
|
||||||
QMap<QString, InterfaceInfo> 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");
|
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2.json");
|
||||||
|
|
||||||
|
Uf2Interface uf2;
|
||||||
|
QJsonObject oldJson = readJson(outputPath);
|
||||||
|
QJsonObject oldCache = readCache();
|
||||||
|
|
||||||
|
// 仅收集文件路径与修改时间(不解析内容),用于增量判定
|
||||||
|
QList<Uf2FileEntry> 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<QString, QString> 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<QString, qint64> mtimeByPath;
|
||||||
|
for (const Uf2FileEntry& e : entries) mtimeByPath[e.path] = e.mtime;
|
||||||
|
|
||||||
|
QMap<QString, InterfaceInfo> 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<QString> 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)) {
|
if (!writeJson(outputPath, resultJson)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LogManager::instance()->log(QString("UF2.0 接口处理完成,结果已保存到: %1").arg(outputPath));
|
QJsonObject merged = oldCache;
|
||||||
if (!resultMap.isEmpty()) {
|
merged["uf2"] = newUf2Cache;
|
||||||
QString firstKey = resultMap.keys().first();
|
if (!writeCache(merged)) {
|
||||||
|
LogManager::instance()->logWarning("UF2.0 缓存写入失败(不影响本次结果)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogManager::instance()->log(QString("UF2.0 接口处理完成,结果已保存到: %1").arg(outputPath));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,62 +383,148 @@ bool MetadataProcessor::processUft3Interfaces(const QString& basePath)
|
||||||
{
|
{
|
||||||
LogManager::instance()->log("========== 开始处理 UFT3.0 接口 ==========");
|
LogManager::instance()->log("========== 开始处理 UFT3.0 接口 ==========");
|
||||||
LogManager::instance()->log(QString("基础路径(basePath): %1").arg(basePath));
|
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;
|
QString moduleGenerationCheck = m_moduleGenerationCheck;
|
||||||
LogManager::instance()->log(QString("模块生成检查: %1").arg(moduleGenerationCheck));
|
LogManager::instance()->log(QString("模块生成检查: %1").arg(moduleGenerationCheck));
|
||||||
|
|
||||||
QStringList completeNameList = {"module.xml", "project.xml", "冲突检测.wlua"};
|
QStringList completeNameList = {"module.xml", "project.xml", "冲突检测.wlua"};
|
||||||
QStringList containNameList = {".extinterface", ".uftatomfunction", ".uftatomservice", ".uftfunction", ".uftservice"};
|
QStringList containNameList = {".extinterface", ".uftatomfunction", ".uftatomservice", ".uftfunction", ".uftservice"};
|
||||||
|
|
||||||
QMap<QString, QStringList> filterDict;
|
QMap<QString, QStringList> filterDict;
|
||||||
filterDict["completeNameList"] = completeNameList;
|
filterDict["completeNameList"] = completeNameList;
|
||||||
filterDict["containNameList"] = containNameList;
|
filterDict["containNameList"] = containNameList;
|
||||||
|
|
||||||
QString dirPath = m_uft30Path;
|
QString dirPath = m_uft30Path;
|
||||||
LogManager::instance()->log(QString("UFT30 项目路径: %1").arg(dirPath));
|
LogManager::instance()->log(QString("UFT30 项目路径: %1").arg(dirPath));
|
||||||
|
|
||||||
Uft3Interface uft3;
|
|
||||||
auto result = uft3.scanDir(dirPath, filterDict, moduleGenerationCheck);
|
|
||||||
|
|
||||||
QMap<QString, InterfaceInfo> resultMap = result.first;
|
|
||||||
QMap<QString, InterfaceInfo> 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 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<Uft3FileEntry> 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<QString, qint64> mtimeByPath;
|
||||||
|
for (const Uft3FileEntry& e : entries) mtimeByPath[e.path] = e.mtime;
|
||||||
|
|
||||||
|
auto result = uft3.scanDir(dirPath, filterDict, moduleGenerationCheck);
|
||||||
|
QMap<QString, InterfaceInfo> resultMap = result.first;
|
||||||
|
QMap<QString, InterfaceInfo> 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<QString> 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)) {
|
if (!writeJson(outputPath, resultJson)) {
|
||||||
return false;
|
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)) {
|
if (!writeJson(transOutputPath, transCodeJson)) {
|
||||||
return false;
|
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));
|
LogManager::instance()->log(QString("UFT3.0 接口处理完成,结果已保存到: %1").arg(outputPath));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MetadataProcessor::processUf2DataType()
|
bool MetadataProcessor::processUf2DataType()
|
||||||
{
|
{
|
||||||
LogManager::instance()->log("========== 开始处理 UF2.0 数据类型 ==========");
|
LogManager::instance()->log("========== 开始处理 UF2.0 数据类型 ==========");
|
||||||
|
QElapsedTimer timer;
|
||||||
|
timer.start();
|
||||||
|
|
||||||
QString dirPath = m_uf20Path;
|
QString dirPath = m_uf20Path;
|
||||||
|
|
||||||
|
|
@ -364,10 +544,11 @@ bool MetadataProcessor::processUf2DataType()
|
||||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2dataType.json");
|
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2dataType.json");
|
||||||
|
|
||||||
if (!writeJson(outputPath, resultJson)) {
|
if (!writeJson(outputPath, resultJson)) {
|
||||||
|
LogManager::instance()->log(QString("UF2.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -375,6 +556,8 @@ bool MetadataProcessor::processUf2DataType()
|
||||||
bool MetadataProcessor::processUf2StdFields()
|
bool MetadataProcessor::processUf2StdFields()
|
||||||
{
|
{
|
||||||
LogManager::instance()->log("========== 开始处理 UF2.0 标准字段 ==========");
|
LogManager::instance()->log("========== 开始处理 UF2.0 标准字段 ==========");
|
||||||
|
QElapsedTimer timer;
|
||||||
|
timer.start();
|
||||||
|
|
||||||
QString dirPath = m_uf20Path;
|
QString dirPath = m_uf20Path;
|
||||||
|
|
||||||
|
|
@ -406,10 +589,11 @@ bool MetadataProcessor::processUf2StdFields()
|
||||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2stdfield.json");
|
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2stdfield.json");
|
||||||
|
|
||||||
if (!writeJson(outputPath, resultJson)) {
|
if (!writeJson(outputPath, resultJson)) {
|
||||||
|
LogManager::instance()->log(QString("UF2.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -417,6 +601,8 @@ bool MetadataProcessor::processUf2StdFields()
|
||||||
bool MetadataProcessor::processUf2ErrNumber()
|
bool MetadataProcessor::processUf2ErrNumber()
|
||||||
{
|
{
|
||||||
LogManager::instance()->log("========== 开始处理 UF2.0 标准错误号 ==========");
|
LogManager::instance()->log("========== 开始处理 UF2.0 标准错误号 ==========");
|
||||||
|
QElapsedTimer timer;
|
||||||
|
timer.start();
|
||||||
|
|
||||||
QString dirPath = m_uf20Path;
|
QString dirPath = m_uf20Path;
|
||||||
|
|
||||||
|
|
@ -436,10 +622,11 @@ bool MetadataProcessor::processUf2ErrNumber()
|
||||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2Error.json");
|
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2Error.json");
|
||||||
|
|
||||||
if (!writeJson(outputPath, resultJson)) {
|
if (!writeJson(outputPath, resultJson)) {
|
||||||
|
LogManager::instance()->log(QString("UF2.0 标准错误号处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -447,6 +634,8 @@ bool MetadataProcessor::processUf2ErrNumber()
|
||||||
bool MetadataProcessor::processUft3DataType()
|
bool MetadataProcessor::processUft3DataType()
|
||||||
{
|
{
|
||||||
LogManager::instance()->log("========== 开始处理 UFT3.0 数据类型 ==========");
|
LogManager::instance()->log("========== 开始处理 UFT3.0 数据类型 ==========");
|
||||||
|
QElapsedTimer timer;
|
||||||
|
timer.start();
|
||||||
|
|
||||||
QString dirPath = m_uft30PubPath;
|
QString dirPath = m_uft30PubPath;
|
||||||
|
|
||||||
|
|
@ -466,10 +655,11 @@ bool MetadataProcessor::processUft3DataType()
|
||||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3datatype.json");
|
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3datatype.json");
|
||||||
|
|
||||||
if (!writeJson(outputPath, resultJson)) {
|
if (!writeJson(outputPath, resultJson)) {
|
||||||
|
LogManager::instance()->log(QString("UFT3.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -477,6 +667,8 @@ bool MetadataProcessor::processUft3DataType()
|
||||||
bool MetadataProcessor::processUft3StdFields()
|
bool MetadataProcessor::processUft3StdFields()
|
||||||
{
|
{
|
||||||
LogManager::instance()->log("========== 开始处理 UFT3.0 标准字段 ==========");
|
LogManager::instance()->log("========== 开始处理 UFT3.0 标准字段 ==========");
|
||||||
|
QElapsedTimer timer;
|
||||||
|
timer.start();
|
||||||
|
|
||||||
QString dirPath = m_uft30PubPath;
|
QString dirPath = m_uft30PubPath;
|
||||||
|
|
||||||
|
|
@ -508,10 +700,11 @@ bool MetadataProcessor::processUft3StdFields()
|
||||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3stdfields.json");
|
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3stdfields.json");
|
||||||
|
|
||||||
if (!writeJson(outputPath, resultJson)) {
|
if (!writeJson(outputPath, resultJson)) {
|
||||||
|
LogManager::instance()->log(QString("UFT3.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -519,6 +712,8 @@ bool MetadataProcessor::processUft3StdFields()
|
||||||
bool MetadataProcessor::processUft3Table()
|
bool MetadataProcessor::processUft3Table()
|
||||||
{
|
{
|
||||||
LogManager::instance()->log("========== 开始处理 UFT3.0 表结构 ==========");
|
LogManager::instance()->log("========== 开始处理 UFT3.0 表结构 ==========");
|
||||||
|
QElapsedTimer timer;
|
||||||
|
timer.start();
|
||||||
|
|
||||||
QString dirPath = m_uft30PubPath;
|
QString dirPath = m_uft30PubPath;
|
||||||
|
|
||||||
|
|
@ -541,10 +736,11 @@ bool MetadataProcessor::processUft3Table()
|
||||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3Table.json");
|
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3Table.json");
|
||||||
|
|
||||||
if (!writeJson(outputPath, resultJson)) {
|
if (!writeJson(outputPath, resultJson)) {
|
||||||
|
LogManager::instance()->log(QString("UFT3.0 表结构处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,9 @@ public:
|
||||||
|
|
||||||
bool processUf2Interfaces(const QString& basePath);
|
bool processUf2Interfaces(const QString& basePath);
|
||||||
bool processUft3Interfaces(const QString& basePath);
|
bool processUft3Interfaces(const QString& basePath);
|
||||||
|
|
||||||
|
void setForceFull(bool force); // 强制全量更新(兼容 UI;原版每次均全量解析,无增量缓存时不影响行为)
|
||||||
|
|
||||||
bool processUf2StdFields();
|
bool processUf2StdFields();
|
||||||
bool processUf2DataType();
|
bool processUf2DataType();
|
||||||
bool processUf2ErrNumber();
|
bool processUf2ErrNumber();
|
||||||
|
|
@ -38,6 +40,11 @@ private:
|
||||||
bool writeJson(const QString& filePath, const QJsonObject& jsonObject);
|
bool writeJson(const QString& filePath, const QJsonObject& jsonObject);
|
||||||
QJsonObject readJson(const QString& filePath);
|
QJsonObject readJson(const QString& filePath);
|
||||||
QJsonObject interfaceInfoToUFT3Json(const InterfaceInfo& info);
|
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_basePath;
|
||||||
QString m_uf2touft3Path;
|
QString m_uf2touft3Path;
|
||||||
|
|
@ -51,6 +58,8 @@ private:
|
||||||
|
|
||||||
QString m_supportUftdbSett;
|
QString m_supportUftdbSett;
|
||||||
QString m_moduleGenerationCheck;
|
QString m_moduleGenerationCheck;
|
||||||
|
|
||||||
|
bool m_forceFull = false; // 强制全量更新标志(兼容 UI;原版每次均全量解析,无增量缓存时不影响行为)
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // METADATAPROCESSOR_H
|
#endif // METADATAPROCESSOR_H
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QStack>
|
#include <QStack>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
Uf2Interface::Uf2Interface(QObject *parent) : QObject(parent)
|
Uf2Interface::Uf2Interface(QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
|
|
@ -294,3 +295,97 @@ QMap<QString, InterfaceInfo> Uf2Interface::scanDir(const QString& dirPath,
|
||||||
|
|
||||||
return resultMap;
|
return resultMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QMap<QString, QString> Uf2Interface::buildModuleCodeMap(const QString& dirPath,
|
||||||
|
const QString& ufAcct20ProjectDir,
|
||||||
|
const QString& uftDbSett,
|
||||||
|
const QJsonObject& custJson)
|
||||||
|
{
|
||||||
|
QMap<QString, QString> moduleCodeMap;
|
||||||
|
moduleCodeMap["转融通"] = "ref";
|
||||||
|
|
||||||
|
// 预提取cust.json中的模块英文名映射
|
||||||
|
QMap<QString, QString> 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<void(const QString&)> 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<QString, QString>& 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<Uf2FileEntry> Uf2Interface::collectFiles(const QString& dirPath,
|
||||||
|
const QString& serverType,
|
||||||
|
const QMap<QString, QStringList>& fileFilterDict)
|
||||||
|
{
|
||||||
|
QList<Uf2FileEntry> result;
|
||||||
|
QStringList completeNameList = fileFilterDict.value("completeNameList");
|
||||||
|
QStringList containNameList = fileFilterDict.value("containNameList");
|
||||||
|
|
||||||
|
std::function<void(const QString&, const QString&)> 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
#include <QList>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QXmlStreamReader>
|
#include <QXmlStreamReader>
|
||||||
|
|
||||||
|
|
@ -41,6 +42,13 @@ struct InterfaceInfo {
|
||||||
QMap<QString, FieldInfo> internalFields;
|
QMap<QString, FieldInfo> internalFields;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 仅记录文件路径、归属类型、修改时间,用于轻量增量缓存
|
||||||
|
struct Uf2FileEntry {
|
||||||
|
QString path;
|
||||||
|
QString serverType; // trade / acct / dbsett
|
||||||
|
qint64 mtime = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class Uf2Interface : public QObject
|
class Uf2Interface : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -54,6 +62,18 @@ public:
|
||||||
const QMap<QString, QStringList>& fileFilterDict,
|
const QMap<QString, QStringList>& fileFilterDict,
|
||||||
const QJsonObject& custJson = QJsonObject());
|
const QJsonObject& custJson = QJsonObject());
|
||||||
|
|
||||||
|
// 以下为增量更新提供复用能力(与 scanDir 内部逻辑一致)
|
||||||
|
QMap<QString, QString> buildModuleCodeMap(const QString& dirPath,
|
||||||
|
const QString& ufAcct20ProjectDir,
|
||||||
|
const QString& uftDbSett,
|
||||||
|
const QJsonObject& custJson = QJsonObject());
|
||||||
|
InterfaceInfo assemble(const QString& filePath,
|
||||||
|
const QString& serverType,
|
||||||
|
const QMap<QString, QString>& moduleCodeMap);
|
||||||
|
QList<Uf2FileEntry> collectFiles(const QString& dirPath,
|
||||||
|
const QString& serverType,
|
||||||
|
const QMap<QString, QStringList>& fileFilterDict);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString getModuleEName(const QString& moduleXmlPath);
|
QString getModuleEName(const QString& moduleXmlPath);
|
||||||
bool shouldFilter(const QString& filePath,
|
bool shouldFilter(const QString& filePath,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
Uft3Interface::Uft3Interface(QObject *parent) : QObject(parent)
|
Uft3Interface::Uft3Interface(QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
|
|
@ -243,3 +244,57 @@ Uft3Interface::scanDir(const QString& dirPath,
|
||||||
|
|
||||||
return qMakePair(resultMap, transCodeMap);
|
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<Uft3FileEntry> Uft3Interface::collectFiles(const QString& dirPath,
|
||||||
|
const QMap<QString, QStringList>& fileFilterDict,
|
||||||
|
const QString& moduleGenerationCheck)
|
||||||
|
{
|
||||||
|
QList<Uft3FileEntry> result;
|
||||||
|
QStringList completeNameList = fileFilterDict.value("completeNameList");
|
||||||
|
QStringList containNameList = fileFilterDict.value("containNameList");
|
||||||
|
|
||||||
|
std::function<void(const QString&)> 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,17 @@
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QXmlStreamReader>
|
#include <QXmlStreamReader>
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
#include "uf2interface.h"
|
#include "uf2interface.h"
|
||||||
|
|
||||||
|
// 仅记录文件路径、修改时间、是否属于交易码,用于轻量增量缓存
|
||||||
|
struct Uft3FileEntry {
|
||||||
|
QString path;
|
||||||
|
qint64 mtime = 0;
|
||||||
|
bool isTrans = false;
|
||||||
|
};
|
||||||
|
|
||||||
class Uft3Interface : public QObject
|
class Uft3Interface : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -21,6 +29,12 @@ public:
|
||||||
const QMap<QString, QStringList>& fileFilterDict,
|
const QMap<QString, QStringList>& fileFilterDict,
|
||||||
const QString& moduleGenerationCheck);
|
const QString& moduleGenerationCheck);
|
||||||
|
|
||||||
|
// 以下为增量更新提供复用能力(与 scanDir 内部逻辑一致)
|
||||||
|
InterfaceInfo assemble(const QString& filePath);
|
||||||
|
QList<Uft3FileEntry> collectFiles(const QString& dirPath,
|
||||||
|
const QMap<QString, QStringList>& fileFilterDict,
|
||||||
|
const QString& moduleGenerationCheck);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool shouldFilter(const QString& filePath,
|
bool shouldFilter(const QString& filePath,
|
||||||
const QString& dirPath,
|
const QString& dirPath,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent)
|
AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent)
|
||||||
{
|
{
|
||||||
setFixedSize(354, 180);
|
setFixedSize(360, 210);
|
||||||
setWindowTitle("关于");
|
setWindowTitle("关于");
|
||||||
setStandardButtons(QDialogButtonBox::Ok);
|
setStandardButtons(QDialogButtonBox::Ok);
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent)
|
||||||
QFont nameTextFont = nameText->font();
|
QFont nameTextFont = nameText->font();
|
||||||
nameTextFont.setWeight(QFont::Bold);
|
nameTextFont.setWeight(QFont::Bold);
|
||||||
nameText->setFont(nameTextFont);
|
nameText->setFont(nameTextFont);
|
||||||
nameText->setTextPixelSize(17);
|
nameText->setTextPixelSize(15);
|
||||||
nameText->setText("UFT30 ChangeCode");
|
nameText->setText("UFT30 ChangeCode");
|
||||||
|
|
||||||
auto* versionText = new ElaText(this);
|
auto* versionText = new ElaText(this);
|
||||||
|
|
|
||||||
|
|
@ -151,12 +151,15 @@ void BatchConvertPage::initUI()
|
||||||
void BatchConvertPage::onProgressUpdate(int progress, const QString& status)
|
void BatchConvertPage::onProgressUpdate(int progress, const QString& status)
|
||||||
{
|
{
|
||||||
m_progressBar->setValue(progress);
|
m_progressBar->setValue(progress);
|
||||||
|
|
||||||
QString displayStatus = status;
|
// 仅当传入有效状态文字时才更新标签,避免无意义的日志行覆盖当前处理状态
|
||||||
if (displayStatus.length() > 80) {
|
if (!status.isEmpty()) {
|
||||||
displayStatus = displayStatus.left(80) + "...";
|
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)
|
bool BatchConvertPage::checkFunctionExists(const QString &funcName)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,127 @@
|
||||||
|
#ifndef CODESEARCHPAGE_H
|
||||||
|
#define CODESEARCHPAGE_H
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#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<int> &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<CallChainAnalyzer::TreeNode> &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<QString> &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<void(bool, const QString &)> 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<CallChainAnalyzer::TreeNode> m_currentChainRoots; // 当前调用链路森林(支持往前查),供导出文本用
|
||||||
|
QString m_searchRootPath; // 当前索引根路径(导出基准)
|
||||||
|
QString m_currentQueryName; // 当前查询的函数名称(导出目录名)
|
||||||
|
QString m_highlightName; // 当前查询的函数/功能名(调用链树中对应行蓝色高亮)
|
||||||
|
bool m_indexing = false; // 后台索引构建进行中,防止重复触发
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CODESEARCHPAGE_H
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "metadatupdatepage.h"
|
#include "metadatupdatepage.h"
|
||||||
#include "utils/Constants.h"
|
#include "utils/Constants.h"
|
||||||
|
#include "utils/logmanager.h"
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
|
|
@ -8,6 +9,7 @@
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QtConcurrent>
|
||||||
|
|
||||||
const int UPDATE_ITEM_SHOW_COLUMN_COUNT = 3;
|
const int UPDATE_ITEM_SHOW_COLUMN_COUNT = 3;
|
||||||
|
|
||||||
|
|
@ -17,13 +19,18 @@ MetadataUpdatePage::MetadataUpdatePage(QWidget *parent)
|
||||||
initUI();
|
initUI();
|
||||||
_processor = new MetadataProcessor(this);
|
_processor = new MetadataProcessor(this);
|
||||||
_processor->initConfig(Constants::toolDir());
|
_processor->initConfig(Constants::toolDir());
|
||||||
|
|
||||||
|
_watcher = new QFutureWatcher<UpdateResult>(this);
|
||||||
|
connect(_watcher, &QFutureWatcher<UpdateResult>::finished,
|
||||||
|
this, &MetadataUpdatePage::onWorkerFinished);
|
||||||
|
connect(this, &MetadataUpdatePage::sigProgress,
|
||||||
|
this, &MetadataUpdatePage::onUpdateProgress, Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
MetadataUpdatePage::~MetadataUpdatePage()
|
MetadataUpdatePage::~MetadataUpdatePage()
|
||||||
{
|
{
|
||||||
if (_workerThread && _workerThread->isRunning()) {
|
if (_watcher && _watcher->isRunning()) {
|
||||||
_workerThread->quit();
|
_watcher->waitForFinished();
|
||||||
_workerThread->wait();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,6 +79,13 @@ void MetadataUpdatePage::initUI()
|
||||||
gridLayout->addWidget(_uft3CancelBtn, 1, 3, 1, 1);
|
gridLayout->addWidget(_uft3CancelBtn, 1, 3, 1, 1);
|
||||||
|
|
||||||
_uft3UpdateBtn = new ElaPushButton("更新UFT30", contentWidget);
|
_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);
|
connect(_uft3UpdateBtn, &QPushButton::clicked, this, &MetadataUpdatePage::onUpdateUFT3);
|
||||||
gridLayout->addWidget(_uft3UpdateBtn, 1, 4, 1, 1);
|
gridLayout->addWidget(_uft3UpdateBtn, 1, 4, 1, 1);
|
||||||
|
|
||||||
|
|
@ -105,17 +119,42 @@ void MetadataUpdatePage::initUI()
|
||||||
gridLayout->addWidget(_uf2CancelBtn, 1, 8, 1, 1);
|
gridLayout->addWidget(_uf2CancelBtn, 1, 8, 1, 1);
|
||||||
|
|
||||||
_uf2UpdateBtn = new ElaPushButton("更新UF20", contentWidget);
|
_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);
|
connect(_uf2UpdateBtn, &QPushButton::clicked, this, &MetadataUpdatePage::onUpdateUF20);
|
||||||
gridLayout->addWidget(_uf2UpdateBtn, 1, 9, 1, 1);
|
gridLayout->addWidget(_uf2UpdateBtn, 1, 9, 1, 1);
|
||||||
|
|
||||||
gridLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Minimum), 0, 10, 2, 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->addWidget(contentWidget);
|
||||||
|
|
||||||
centerLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding));
|
centerLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding));
|
||||||
|
|
||||||
|
// 进度标签行:左侧显示状态文字,右侧放置强制全量更新复选框
|
||||||
|
QHBoxLayout* processHeaderLayout = new QHBoxLayout();
|
||||||
_processLabel = new ElaText(this);
|
_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 = new ElaProgressBar(this);
|
||||||
_progressBar->setValue(0);
|
_progressBar->setValue(0);
|
||||||
|
|
@ -209,8 +248,9 @@ void MetadataUpdatePage::onUpdateUF20()
|
||||||
_uf2AllBtn->setEnabled(false);
|
_uf2AllBtn->setEnabled(false);
|
||||||
_uf2CancelBtn->setEnabled(false);
|
_uf2CancelBtn->setEnabled(false);
|
||||||
_progressBar->setValue(0);
|
_progressBar->setValue(0);
|
||||||
|
_processor->setForceFull(_forceFullCheckBox->isChecked());
|
||||||
updateUf2Interfaces(selectedItems);
|
|
||||||
|
startUpdate(selectedItems, "UF20");
|
||||||
}
|
}
|
||||||
|
|
||||||
void MetadataUpdatePage::onUFT3SelectAll()
|
void MetadataUpdatePage::onUFT3SelectAll()
|
||||||
|
|
@ -245,22 +285,22 @@ void MetadataUpdatePage::onUpdateUFT3()
|
||||||
_uft3AllBtn->setEnabled(false);
|
_uft3AllBtn->setEnabled(false);
|
||||||
_uft3CancelBtn->setEnabled(false);
|
_uft3CancelBtn->setEnabled(false);
|
||||||
_progressBar->setValue(0);
|
_progressBar->setValue(0);
|
||||||
|
_processor->setForceFull(_forceFullCheckBox->isChecked());
|
||||||
updateUft3Interfaces(selectedItems);
|
|
||||||
|
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;
|
bool interfaceUpdated = false;
|
||||||
|
int itemCount = items.size();
|
||||||
foreach (const QString& item, items) {
|
int idx = 0;
|
||||||
_processLabel->setText(QString("正在更新UF20 - %1").arg(item));
|
|
||||||
int currentIndex = items.indexOf(item);
|
for (const QString& item : items) {
|
||||||
int progress = (itemCount == 1) ? 0 : (currentIndex * 100 / itemCount);
|
emit sigProgress((itemCount == 1) ? 0 : (idx * 100 / itemCount),
|
||||||
_progressBar->setValue(progress);
|
QString("正在更新UF20 - %1").arg(item));
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
if (item == "标准字段") {
|
if (item == "标准字段") {
|
||||||
processUf2StdFields();
|
processUf2StdFields();
|
||||||
} else if (item == "数据类型") {
|
} else if (item == "数据类型") {
|
||||||
|
|
@ -268,35 +308,29 @@ void MetadataUpdatePage::updateUf2Interfaces(const QStringList& items)
|
||||||
} else if (item == "标准错误号") {
|
} else if (item == "标准错误号") {
|
||||||
processUf2ErrNumber();
|
processUf2ErrNumber();
|
||||||
} else if (item == "接口数据") {
|
} else if (item == "接口数据") {
|
||||||
processUf2Interface(Constants::toolDir());
|
if (!processUf2Interface(Constants::toolDir())) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
interfaceUpdated = true;
|
interfaceUpdated = true;
|
||||||
}
|
}
|
||||||
|
++idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interfaceUpdated) {
|
// 数据缓存重载放到主线程执行(DataCache 单例非线程安全),见 onWorkerFinished
|
||||||
_processLabel->setText("正在更新数据缓存...");
|
return UpdateResult{success, interfaceUpdated};
|
||||||
QCoreApplication::processEvents();
|
|
||||||
UF2ConfigReader::reloadUF20Config();
|
|
||||||
}
|
|
||||||
|
|
||||||
_progressBar->setValue(100);
|
|
||||||
_processLabel->setText("UF20更新完成");
|
|
||||||
|
|
||||||
onUpdateFinished(true, "UF20更新完成");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items)
|
UpdateResult MetadataUpdatePage::runUpdateUft3(const QStringList& items)
|
||||||
{
|
{
|
||||||
int itemCount = items.size();
|
bool success = true;
|
||||||
bool interfaceUpdated = false;
|
bool interfaceUpdated = false;
|
||||||
|
int itemCount = items.size();
|
||||||
foreach (const QString& item, items) {
|
int idx = 0;
|
||||||
_processLabel->setText(QString("正在更新UFT30 - %1").arg(item));
|
|
||||||
int currentIndex = items.indexOf(item);
|
for (const QString& item : items) {
|
||||||
int progress = (itemCount == 1) ? 0 : (currentIndex * 100 / itemCount);
|
emit sigProgress((itemCount == 1) ? 0 : (idx * 100 / itemCount),
|
||||||
_progressBar->setValue(progress);
|
QString("正在更新UFT30 - %1").arg(item));
|
||||||
QCoreApplication::processEvents();
|
|
||||||
|
|
||||||
if (item == "标准字段") {
|
if (item == "标准字段") {
|
||||||
processUft3StdFields();
|
processUft3StdFields();
|
||||||
} else if (item == "数据类型") {
|
} else if (item == "数据类型") {
|
||||||
|
|
@ -304,21 +338,15 @@ void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items)
|
||||||
} else if (item == "表结构") {
|
} else if (item == "表结构") {
|
||||||
processUft3Table();
|
processUft3Table();
|
||||||
} else if (item == "接口数据") {
|
} else if (item == "接口数据") {
|
||||||
processUft3Interface(Constants::toolDir());
|
if (!processUft3Interface(Constants::toolDir())) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
interfaceUpdated = true;
|
interfaceUpdated = true;
|
||||||
}
|
}
|
||||||
|
++idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interfaceUpdated) {
|
return UpdateResult{success, interfaceUpdated};
|
||||||
_processLabel->setText("正在更新数据缓存...");
|
|
||||||
QCoreApplication::processEvents();
|
|
||||||
UFT3ConfigReader::reloadUFT3Config();
|
|
||||||
}
|
|
||||||
|
|
||||||
_progressBar->setValue(100);
|
|
||||||
_processLabel->setText("UFT30更新完成");
|
|
||||||
|
|
||||||
onUpdateFinished(true, "UFT30更新完成");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MetadataUpdatePage::processUf2StdFields()
|
void MetadataUpdatePage::processUf2StdFields()
|
||||||
|
|
@ -336,9 +364,9 @@ void MetadataUpdatePage::processUf2ErrNumber()
|
||||||
_processor->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()
|
void MetadataUpdatePage::processUft3StdFields()
|
||||||
|
|
@ -356,9 +384,60 @@ void MetadataUpdatePage::processUft3Table()
|
||||||
_processor->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)
|
void MetadataUpdatePage::onUpdateProgress(int progress, const QString& message)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,14 @@
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <QFutureWatcher>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
|
||||||
|
// 后台线程执行结果:success 表示整体是否成功,needReload 表示是否需要重新加载数据缓存
|
||||||
|
struct UpdateResult {
|
||||||
|
bool success;
|
||||||
|
bool needReload;
|
||||||
|
};
|
||||||
|
|
||||||
class MetadataUpdatePage : public ElaScrollPage
|
class MetadataUpdatePage : public ElaScrollPage
|
||||||
{
|
{
|
||||||
|
|
@ -35,22 +43,30 @@ private slots:
|
||||||
private slots:
|
private slots:
|
||||||
void onUpdateProgress(int progress, const QString& message);
|
void onUpdateProgress(int progress, const QString& message);
|
||||||
void onUpdateFinished(bool success, const QString& message);
|
void onUpdateFinished(bool success, const QString& message);
|
||||||
|
void onWorkerFinished(); // QFutureWatcher::finished 的回调(运行在主线程)
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void sigProgress(int progress, const QString& message); // 后台线程 -> 主线程回传进度
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initUI();
|
void initUI();
|
||||||
void showUpdateItems();
|
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 processUf2StdFields();
|
||||||
void processUf2DataType();
|
void processUf2DataType();
|
||||||
void processUf2ErrNumber();
|
void processUf2ErrNumber();
|
||||||
void processUf2Interface(const QString& basePath);
|
bool processUf2Interface(const QString& basePath);
|
||||||
|
|
||||||
void processUft3StdFields();
|
void processUft3StdFields();
|
||||||
void processUft3DataType();
|
void processUft3DataType();
|
||||||
void processUft3Table();
|
void processUft3Table();
|
||||||
void processUft3Interface(const QString& basePath);
|
bool processUft3Interface(const QString& basePath);
|
||||||
|
|
||||||
QGridLayout* _updateGridLayout{nullptr};
|
QGridLayout* _updateGridLayout{nullptr};
|
||||||
QGridLayout* _updateUf2GridLayout{nullptr};
|
QGridLayout* _updateUf2GridLayout{nullptr};
|
||||||
|
|
@ -60,14 +76,17 @@ private:
|
||||||
ElaPushButton* _uft3CancelBtn{nullptr};
|
ElaPushButton* _uft3CancelBtn{nullptr};
|
||||||
ElaPushButton* _uf2AllBtn{nullptr};
|
ElaPushButton* _uf2AllBtn{nullptr};
|
||||||
ElaPushButton* _uft3UpdateBtn{nullptr};
|
ElaPushButton* _uft3UpdateBtn{nullptr};
|
||||||
|
ElaCheckBox* _forceFullCheckBox{nullptr};
|
||||||
ElaText* _processLabel{nullptr};
|
ElaText* _processLabel{nullptr};
|
||||||
ElaProgressBar* _progressBar{nullptr};
|
ElaProgressBar* _progressBar{nullptr};
|
||||||
|
|
||||||
QList<ElaCheckBox*> uft3CheckBoxes;
|
QList<ElaCheckBox*> uft3CheckBoxes;
|
||||||
QList<ElaCheckBox*> uf20CheckBoxes;
|
QList<ElaCheckBox*> uf20CheckBoxes;
|
||||||
|
|
||||||
MetadataProcessor* _processor{nullptr};
|
MetadataProcessor* _processor{nullptr};
|
||||||
QThread* _workerThread{nullptr};
|
QFutureWatcher<UpdateResult>* _watcher{nullptr};
|
||||||
|
QElapsedTimer _updateTimer;
|
||||||
|
QString m_currentTaskName; // "UF20" / "UFT30",用于区分当前任务
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ namespace Constants {
|
||||||
// 应用版本号(发版时只需修改此处)
|
// 应用版本号(发版时只需修改此处)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
namespace App {
|
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 ToolDir = "uf2touft3"; // 工具主目录
|
||||||
inline const QString LogDir = "logs"; // 日志目录
|
inline const QString LogDir = "logs"; // 日志目录
|
||||||
inline const QString CacheDir = "cache"; // 缓存目录
|
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 cacheDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::CacheDir; }
|
||||||
inline QString cacheDb() { return cacheDir() + "/" + Cache::DbFile; }
|
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
|
} // namespace Constants
|
||||||
|
|
||||||
#endif // CONSTANTS_H
|
#endif // CONSTANTS_H
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,698 @@
|
||||||
|
#include "callchainanalyzer.h"
|
||||||
|
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDirIterator>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <QXmlStreamReader>
|
||||||
|
|
||||||
|
CallChainAnalyzer::CallChainAnalyzer(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
CallChainAnalyzer::~CallChainAnalyzer() = default;
|
||||||
|
|
||||||
|
// 同名冲突时优先保留“真正的实现”而非外部接口声明:
|
||||||
|
// 外部接口(.extinterface)通常无 <code>,若同名覆盖实现会让调用链路在叶子处断掉。
|
||||||
|
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)通常无 <code>,若同名覆盖实现会让调用链路在叶子处断掉。
|
||||||
|
auto insertPrefer = [&](QHash<QString, QSharedPointer<FunctionInfo>> &idx,
|
||||||
|
const QString &key, QSharedPointer<FunctionInfo> 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<FunctionInfo> 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<FunctionInfo> 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<const CallChainAnalyzer::FunctionInfo>
|
||||||
|
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<const CallChainAnalyzer::FunctionInfo>
|
||||||
|
CallChainAnalyzer::findByChineseName(const QString &name) const
|
||||||
|
{
|
||||||
|
auto it = m_chineseNameIndex.find(name);
|
||||||
|
if (it != m_chineseNameIndex.end()) {
|
||||||
|
return it.value();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<const CallChainAnalyzer::FunctionInfo>
|
||||||
|
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<QString> visited;
|
||||||
|
buildNode(startChineseName, root, visited, 0, maxDepth, true);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CallChainAnalyzer::resolveNode(const QString &name, TreeNode &node, bool isRoot) const
|
||||||
|
{
|
||||||
|
// 使用多策略查找,而非仅 chineseName 精确匹配
|
||||||
|
QSharedPointer<const FunctionInfo> 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<QString> &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<QString> 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<const FunctionInfo> info = it.value();
|
||||||
|
// 同一“被调用名”下,按调用方 canonicalName 去重,避免同一函数多次调用产生重复链
|
||||||
|
QHash<QString, QSet<QString>> seenByKey;
|
||||||
|
for (const QString &call : info->calledFunctions) {
|
||||||
|
// 用与正向查找一致的方式解析被调用名,使“往前查”时能正确命中
|
||||||
|
QSharedPointer<const FunctionInfo> 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::TreeNode>
|
||||||
|
CallChainAnalyzer::buildCallerForest(const QString &startName, const TreeNode &downTree,
|
||||||
|
int maxDepth) const
|
||||||
|
{
|
||||||
|
QList<TreeNode> roots;
|
||||||
|
QSet<QString> visited;
|
||||||
|
|
||||||
|
// 将 startName 归一化为其在索引中的 chineseName,保证与反向索引键一致。
|
||||||
|
// 否则当用户以 basename / 功能号前缀等形式查询(正向可命中)时,反向查找会因键不一致而失效。
|
||||||
|
QString startKey = startName;
|
||||||
|
{
|
||||||
|
QSharedPointer<const FunctionInfo> startInfo = lookupBest(startName);
|
||||||
|
if (startInfo) {
|
||||||
|
startKey = startInfo->chineseName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归构造“上游”链:children 为调用方(caller),链末端为 startName
|
||||||
|
auto recurse = [&](auto &self, const QString &name, QSet<QString> vis, int depth) -> QList<TreeNode> {
|
||||||
|
QList<TreeNode> out;
|
||||||
|
const auto it = m_reverseIndex.find(name);
|
||||||
|
QList<QSharedPointer<const FunctionInfo>> 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<const FunctionInfo> &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<QString> nextVis = vis;
|
||||||
|
nextVis.insert(name);
|
||||||
|
QList<TreeNode> 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::FunctionInfo>
|
||||||
|
CallChainAnalyzer::parseFile(const QString &filePath) const
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<FunctionInfo> 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) {
|
||||||
|
// 优先从根元素属性读取对象名/中文名,
|
||||||
|
// 兼容 <Function>/<Service> 以及 .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 无 <code>,仅作为调用声明)
|
||||||
|
info->isExternal = (fi.suffix().compare("extinterface", Qt::CaseInsensitive) == 0);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList CallChainAnalyzer::extractFunctionCalls(const QString &code)
|
||||||
|
{
|
||||||
|
QStringList result;
|
||||||
|
QSet<QString> seen;
|
||||||
|
|
||||||
|
// 先去除注释(// 行注释、/* */ 块注释),避免把注释里提到的函数当成真实调用
|
||||||
|
const QString cleaned = stripComments(code);
|
||||||
|
|
||||||
|
// 格式 1: <I>[函数名] 或 <MI>[函数名](.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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
#ifndef CALLCHAINANALYZER_H
|
||||||
|
#define CALLCHAINANALYZER_H
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
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<TreeNode> 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<const FunctionInfo> findByChineseName(const QString &name) const;
|
||||||
|
// 根据 objectId-chineseName 查找
|
||||||
|
QSharedPointer<const FunctionInfo> findByFullName(const QString &fullName) const;
|
||||||
|
|
||||||
|
// 多策略查找:先精确匹配 chineseName,再尝试 basename、去后缀等。
|
||||||
|
// 与 resolveNode 使用同一解析入口,供外部还原节点的展示名(如蓝底高亮匹配)。
|
||||||
|
QSharedPointer<const FunctionInfo> 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<TreeNode> buildCallerForest(const QString &startName, const TreeNode &downTree,
|
||||||
|
int maxDepth = 32) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QSharedPointer<FunctionInfo> parseFile(const QString &filePath) const;
|
||||||
|
void buildNode(const QString &name, TreeNode &node, QSet<QString> &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<QString, QSharedPointer<FunctionInfo>> m_chineseNameIndex;
|
||||||
|
QHash<QString, QSharedPointer<FunctionInfo>> m_fullNameIndex;
|
||||||
|
QHash<QString, QSharedPointer<FunctionInfo>> m_canonicalNameIndex;
|
||||||
|
// 反向索引:被调用函数名 -> 调用它的函数列表(用于“往前查”)
|
||||||
|
QHash<QString, QList<QSharedPointer<const FunctionInfo>>> m_reverseIndex;
|
||||||
|
bool m_indexed = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CALLCHAINANALYZER_H
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"V1.0.0": {
|
"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": {
|
"V1.1.0": {
|
||||||
"release_notes": "1.支持2.0账户功能查询及业务转码"
|
"release_notes": "1.支持2.0账户功能查询及业务转码"
|
||||||
|
|
@ -10,5 +10,8 @@
|
||||||
},
|
},
|
||||||
"V1.3.0": {
|
"V1.3.0": {
|
||||||
"release_notes": "1.修复设置路径不生效;\n2.修复元数据更新后业务转码模块对函数检查状态异常问题;"
|
"release_notes": "1.修复设置路径不生效;\n2.修复元数据更新后业务转码模块对函数检查状态异常问题;"
|
||||||
}
|
},
|
||||||
|
"V1.4.0": {
|
||||||
|
"release_notes": "1.提升业务转码性能;\n2.元数据更新支持增量更新和强制全量更新;\n3.新增代码检索功能;\n4.支持函数调用链路分析;\n5.支持全局文字可选中复制及表格Ctrl+C导出;"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue