新增代码检索功能、提升业务转码效率等
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
|
||||
TEMPLATE = app
|
||||
|
|
@ -10,6 +10,7 @@ SOURCES += main.cpp \
|
|||
src/pythonrunner/PythonRunner.cpp \
|
||||
src/pages/about/aboutdialog.cpp \
|
||||
src/pages/batchconvert/batchconvertpage.cpp \
|
||||
src/pages/codesearch/codesearchpage.cpp \
|
||||
src/pages/functionsearch/functionsearchpage.cpp \
|
||||
src/pages/functionsearch/uf20functionsearchpage.cpp \
|
||||
src/pages/functionsearch/uft3functionsearchpage.cpp \
|
||||
|
|
@ -34,6 +35,7 @@ SOURCES += main.cpp \
|
|||
src/utils/uft3configreader.cpp \
|
||||
src/utils/logmanager.cpp \
|
||||
src/utils/configmanager.cpp \
|
||||
src/utils/callchainanalyzer.cpp \
|
||||
src/utils/version.cpp \
|
||||
src/utils/versionchecker.cpp
|
||||
|
||||
|
|
@ -41,6 +43,7 @@ HEADERS += src/mainwindow/mainwindow.h \
|
|||
src/pythonrunner/PythonRunner.h \
|
||||
src/pages/about/aboutdialog.h \
|
||||
src/pages/batchconvert/batchconvertpage.h \
|
||||
src/pages/codesearch/codesearchpage.h \
|
||||
src/pages/functionsearch/functionsearchpage.h \
|
||||
src/pages/functionsearch/uf20functionsearchpage.h \
|
||||
src/pages/functionsearch/uft3functionsearchpage.h \
|
||||
|
|
@ -65,6 +68,7 @@ HEADERS += src/mainwindow/mainwindow.h \
|
|||
src/utils/uft3configreader.h \
|
||||
src/utils/logmanager.h \
|
||||
src/utils/configmanager.h \
|
||||
src/utils/callchainanalyzer.h \
|
||||
src/utils/version.h \
|
||||
src/utils/versionchecker.h
|
||||
|
||||
|
|
|
|||
10
build.bat
10
build.bat
|
|
@ -76,6 +76,16 @@ if errorlevel 1 (
|
|||
)
|
||||
|
||||
echo.
|
||||
REM Deploy Qt runtime dependencies (auto-copy required Qt DLLs/plugins)
|
||||
echo Deploying Qt runtime dependencies...
|
||||
windeployqt --release bin\Uft30ChangeCode.exe
|
||||
if errorlevel 1 (
|
||||
echo [WARN] windeployqt failed, please deploy Qt DLLs manually
|
||||
) else (
|
||||
echo [OK] Qt dependencies deployed
|
||||
)
|
||||
echo.
|
||||
|
||||
echo ==========================================
|
||||
echo Build SUCCESSFUL!
|
||||
echo ==========================================
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
</toc-element>
|
||||
<toc-element topic="功能转码.md">
|
||||
<toc-element topic="业务转码.md"/>
|
||||
<toc-element topic="代码检索.md"/>
|
||||
</toc-element>
|
||||
<toc-element topic="功能查询.md">
|
||||
<toc-element topic="UFT3功能查询.md"/>
|
||||
|
|
@ -21,7 +22,8 @@
|
|||
</toc-element>
|
||||
<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-1-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.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) |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
85
main.cpp
85
main.cpp
|
|
@ -4,11 +4,96 @@
|
|||
#include <QApplication>
|
||||
#include <QIcon>
|
||||
#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[])
|
||||
{
|
||||
QApplication a(argc, argv);
|
||||
a.setWindowIcon(QIcon(":resources/images/ChangeCode.png"));
|
||||
|
||||
// 安装全局过滤器:所有页面文字 / 表格树单元格允许用户复制
|
||||
TextSelectableFilter textSelectableFilter;
|
||||
a.installEventFilter(&textSelectableFilter);
|
||||
|
||||
eApp->init();
|
||||
|
||||
QString basePath = QCoreApplication::applicationDirPath();
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@
|
|||
<file>resources/images/max.png</file>
|
||||
<file>resources/images/min.png</file>
|
||||
<file>resources/images/update.png</file>
|
||||
<file>resources/images/expand_all.png</file>
|
||||
<file>resources/images/collapse_all.png</file>
|
||||
</qresource>
|
||||
</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_navigationButton(nullptr)
|
||||
, m_isNavigationExpanded(true)
|
||||
, m_totalFunctions(0)
|
||||
, m_currentFunctionIndex(0)
|
||||
, m_lastProgress(0)
|
||||
{
|
||||
initWindow();
|
||||
initContent();
|
||||
|
|
@ -133,6 +136,7 @@ void MainWindow::initContent()
|
|||
{
|
||||
addExpanderNode("功能转码", m_funcConvertKey, ElaIconType::FileCode);
|
||||
addPageNode("业务转码", createBatchConvertPage(), m_funcConvertKey, ElaIconType::ArrowsRepeat);
|
||||
addPageNode("代码检索", createCodeSearchPage(), m_funcConvertKey, ElaIconType::Cubes);
|
||||
|
||||
addExpanderNode("功能查询", m_funcSearchKey, ElaIconType::MagnifyingGlass);
|
||||
addPageNode("UF20功能查询", createUF20FunctionSearchPage(), m_funcSearchKey, ElaIconType::MagnifyingGlassPlus);
|
||||
|
|
@ -157,6 +161,12 @@ QWidget* MainWindow::createBatchConvertPage()
|
|||
return m_batchConvertPage;
|
||||
}
|
||||
|
||||
QWidget* MainWindow::createCodeSearchPage()
|
||||
{
|
||||
_codeSearchPage = new CodeSearchPage;
|
||||
return _codeSearchPage;
|
||||
}
|
||||
|
||||
QWidget* MainWindow::createFunctionSearchPage()
|
||||
{
|
||||
return new FunctionSearchPage;
|
||||
|
|
@ -223,6 +233,9 @@ void MainWindow::onBatchConvertStart()
|
|||
m_functionList.clear();
|
||||
}
|
||||
|
||||
// 进度基准:BatchConvertPage 在开始时已将进度条置为 20(配置加载完成)
|
||||
m_lastProgress = 20;
|
||||
|
||||
LogManager::instance()->log("========================================");
|
||||
LogManager::instance()->log("开始执行 UFT2 到 UFT3 的代码转换...");
|
||||
LogManager::instance()->log(QString("工具路径: %1").arg(exePath));
|
||||
|
|
@ -255,6 +268,9 @@ void MainWindow::onPythonRunnerFinished(int exitCode, QProcess::ExitStatus exitS
|
|||
LogManager::instance()->log("========================================");
|
||||
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
|
||||
LogManager::instance()->log("✓ 代码转换完成!");
|
||||
if (_codeSearchPage) {
|
||||
_codeSearchPage->refreshIndexSilent();
|
||||
}
|
||||
QMessageBox::information(this, "完成", "代码转换任务已成功完成!");
|
||||
} else {
|
||||
LogManager::instance()->logError(QString("✗ 转换失败! 退出码: %1").arg(exitCode));
|
||||
|
|
@ -265,59 +281,82 @@ void MainWindow::onPythonRunnerFinished(int exitCode, QProcess::ExitStatus exitS
|
|||
|
||||
void MainWindow::onPythonRunnerOutput(const QString &output)
|
||||
{
|
||||
if (!output.trimmed().isEmpty()) {
|
||||
LogManager::instance()->log(output);
|
||||
if (output.trimmed().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString trimmedOutput = output.trimmed();
|
||||
LogManager::instance()->log(output);
|
||||
|
||||
QRegularExpression progressRegExp(R"(progress\s*[:=]\s*(\d+))");
|
||||
QRegularExpressionMatch match = progressRegExp.match(trimmedOutput);
|
||||
if (match.hasMatch()) {
|
||||
int progress = match.captured(1).toInt();
|
||||
QString status = trimmedOutput;
|
||||
if (status.contains("progress", Qt::CaseInsensitive)) {
|
||||
status = status.replace(progressRegExp, "").trimmed();
|
||||
if (status.isEmpty()) {
|
||||
status = QString("转换中 %1%").arg(progress);
|
||||
}
|
||||
const QString text = output.trimmed();
|
||||
|
||||
// 计算本次输出对应的目标进度;-1 表示本次输出不含可识别的进度标记
|
||||
int targetProgress = -1;
|
||||
QString status;
|
||||
|
||||
// 1) 优先解析工具显式输出的 progress:xx / progress=xx
|
||||
static const QRegularExpression progressRegExp(
|
||||
R"(progress\s*[:=]\s*(\d+))", QRegularExpression::CaseInsensitiveOption);
|
||||
QRegularExpressionMatch pm = progressRegExp.match(text);
|
||||
if (pm.hasMatch()) {
|
||||
targetProgress = pm.captured(1).toInt();
|
||||
status = QString("转换中 %1%").arg(targetProgress);
|
||||
} else {
|
||||
// 2) 解析并行翻译进度 [ n/m](翻译阶段映射到 55~78)
|
||||
// 注意时间戳 [2026-07-17 ...] 不含 '/' 不会误匹配
|
||||
static const QRegularExpression stepRegExp(R"(\[\s*(\d+)\s*/\s*(\d+)\s*\])");
|
||||
QRegularExpressionMatch stepMatch;
|
||||
auto it = stepRegExp.globalMatch(text);
|
||||
while (it.hasNext()) {
|
||||
stepMatch = it.next(); // 取本段输出里最后一个 [n/m]
|
||||
}
|
||||
if (stepMatch.hasMatch()) {
|
||||
int cur = stepMatch.captured(1).toInt();
|
||||
int tot = stepMatch.captured(2).toInt();
|
||||
if (tot > 0) {
|
||||
targetProgress = 55 + (cur * 23) / tot;
|
||||
status = QString("正在翻译文件 %1/%2").arg(cur).arg(tot);
|
||||
}
|
||||
if (m_batchConvertPage) {
|
||||
m_batchConvertPage->onProgressUpdate(progress, status);
|
||||
}
|
||||
} else {
|
||||
if (m_batchConvertPage) {
|
||||
QString currentFunction;
|
||||
QRegularExpression funcRegExp(R"(([\w_]+))");
|
||||
QRegularExpressionMatch funcMatch = funcRegExp.match(trimmedOutput);
|
||||
if (funcMatch.hasMatch()) {
|
||||
currentFunction = funcMatch.captured(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentFunction.isEmpty() && m_totalFunctions > 0) {
|
||||
int index = m_functionList.indexOf(currentFunction);
|
||||
if (index >= 0 && index >= m_currentFunctionIndex) {
|
||||
m_currentFunctionIndex = index + 1;
|
||||
int progress = 20 + (m_currentFunctionIndex * 80) / m_totalFunctions;
|
||||
QString status = QString("正在转换: %1").arg(currentFunction);
|
||||
m_batchConvertPage->onProgressUpdate(progress, status);
|
||||
return;
|
||||
// 3) 解析流水线阶段标记(关键字命中取最大值,保证进度稳定推进)
|
||||
if (targetProgress < 0) {
|
||||
struct Marker { const char* key; int prog; const char* label; };
|
||||
static const Marker markers[] = {
|
||||
{"开始加载数据", 25, "正在加载数据..."},
|
||||
{"配置文件加载完成", 30, "配置文件加载完成"},
|
||||
{"元数据加载完成", 35, "元数据加载完成"},
|
||||
{"全部数据加载完成", 42, "数据加载完成"},
|
||||
{"数据初始化完成", 45, "数据初始化完成"},
|
||||
{"校验完成", 52, "数据校验完成"},
|
||||
{"翻译完成", 80, "代码翻译完成"},
|
||||
{"后处理完成", 90, "后处理完成"},
|
||||
{"输出完成", 96, "结果输出完成"},
|
||||
{"转换流水线执行成功", 98, "转换流水线执行成功"},
|
||||
{"转码完成", 98, "转码完成"},
|
||||
};
|
||||
for (const auto &m : markers) {
|
||||
if (text.contains(QString::fromUtf8(m.key))) {
|
||||
if (m.prog > targetProgress) {
|
||||
targetProgress = m.prog;
|
||||
status = QString::fromUtf8(m.label);
|
||||
}
|
||||
}
|
||||
|
||||
int currentProgress = 20;
|
||||
if (trimmedOutput.contains("开始转换", Qt::CaseInsensitive)) {
|
||||
currentProgress = 25;
|
||||
} else if (trimmedOutput.contains("处理文件", Qt::CaseInsensitive)) {
|
||||
currentProgress = 30;
|
||||
} else if (trimmedOutput.contains("生成代码", Qt::CaseInsensitive)) {
|
||||
currentProgress = 75;
|
||||
} else if (trimmedOutput.contains("完成", Qt::CaseInsensitive)) {
|
||||
currentProgress = 95;
|
||||
}
|
||||
m_batchConvertPage->onProgressUpdate(currentProgress, trimmedOutput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_batchConvertPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 进度单调递增:只有更大的进度才会推进进度条,避免"一下长一下短"
|
||||
if (targetProgress > m_lastProgress) {
|
||||
m_lastProgress = targetProgress;
|
||||
}
|
||||
|
||||
// 状态文字:仅展示已识别的处理阶段;未识别的日志行不显示(保持上一次状态)
|
||||
// 空 status 表示当前行无有效状态,BatchConvertPage 会保留上一次文字
|
||||
m_batchConvertPage->onProgressUpdate(m_lastProgress, status);
|
||||
}
|
||||
|
||||
void MainWindow::onPythonRunnerError(const QString &error)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <QWidget>
|
||||
#include "src/pythonrunner/PythonRunner.h"
|
||||
#include "src/pages/batchconvert/batchconvertpage.h"
|
||||
#include "src/pages/codesearch/codesearchpage.h"
|
||||
#include "src/pages/functionsearch/functionsearchpage.h"
|
||||
#include "src/pages/functionsearch/uf20functionsearchpage.h"
|
||||
#include "src/pages/functionsearch/uft3functionsearchpage.h"
|
||||
|
|
@ -30,6 +31,7 @@ private:
|
|||
void initContent();
|
||||
|
||||
QWidget* createBatchConvertPage();
|
||||
QWidget* createCodeSearchPage();
|
||||
QWidget* createFunctionSearchPage();
|
||||
QWidget* createUF20FunctionSearchPage();
|
||||
QWidget* createUFT3FunctionSearchPage();
|
||||
|
|
@ -58,9 +60,11 @@ private:
|
|||
ElaToolButton* m_navigationButton;
|
||||
bool m_isNavigationExpanded;
|
||||
BatchConvertPage *m_batchConvertPage;
|
||||
CodeSearchPage *_codeSearchPage;
|
||||
|
||||
int m_totalFunctions;
|
||||
int m_currentFunctionIndex;
|
||||
int m_lastProgress; // 已上报的最大进度,保证进度单调递增,避免进度条来回跳动
|
||||
QStringList m_functionList;
|
||||
QString m_helpKey;
|
||||
QString m_aboutKey;
|
||||
|
|
|
|||
|
|
@ -13,13 +13,20 @@
|
|||
#include "utils/configmanager.h"
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QDir>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
MetadataProcessor::MetadataProcessor(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void MetadataProcessor::setForceFull(bool force)
|
||||
{
|
||||
m_forceFull = force;
|
||||
}
|
||||
|
||||
void MetadataProcessor::initConfig(const QString& basePath)
|
||||
{
|
||||
m_basePath = basePath;
|
||||
|
|
@ -181,14 +188,79 @@ void MetadataProcessor::setBasePath(const QString& path)
|
|||
m_basePath = path;
|
||||
}
|
||||
|
||||
QJsonObject MetadataProcessor::interfaceInfoToUF2Json(const InterfaceInfo& info)
|
||||
{
|
||||
QJsonObject interfaceJson;
|
||||
interfaceJson["cname"] = info.cname;
|
||||
interfaceJson["eName"] = info.eName;
|
||||
interfaceJson["functionNo"] = info.functionNo;
|
||||
interfaceJson["flag"] = info.flag;
|
||||
interfaceJson["path"] = info.path;
|
||||
interfaceJson["code"] = info.code;
|
||||
interfaceJson["moudle"] = info.moudle;
|
||||
interfaceJson["moudleEName"] = info.moudleEName;
|
||||
interfaceJson["serverType"] = info.serverType;
|
||||
interfaceJson["returnResultSet"] = info.returnResultSet;
|
||||
interfaceJson["needTransMonitor"] = info.needTransMonitor;
|
||||
interfaceJson["checkLicence"] = info.checkLicence;
|
||||
|
||||
QJsonObject inputFieldsJson;
|
||||
for (const QString& fieldName : info.inputFields.keys()) {
|
||||
const FieldInfo& field = info.inputFields[fieldName];
|
||||
QJsonObject fieldJson;
|
||||
fieldJson["name"] = field.name;
|
||||
fieldJson["flag"] = field.flag;
|
||||
fieldJson["desc"] = field.desc;
|
||||
inputFieldsJson[fieldName] = fieldJson;
|
||||
}
|
||||
interfaceJson["inputFileds"] = inputFieldsJson;
|
||||
|
||||
QJsonObject outputFieldsJson;
|
||||
for (const QString& fieldName : info.outputFields.keys()) {
|
||||
const FieldInfo& field = info.outputFields[fieldName];
|
||||
QJsonObject fieldJson;
|
||||
fieldJson["name"] = field.name;
|
||||
fieldJson["flag"] = field.flag;
|
||||
fieldJson["desc"] = field.desc;
|
||||
outputFieldsJson[fieldName] = fieldJson;
|
||||
}
|
||||
interfaceJson["outputFileds"] = outputFieldsJson;
|
||||
|
||||
QJsonObject variableFieldsJson;
|
||||
for (const QString& fieldName : info.variableFields.keys()) {
|
||||
const FieldInfo& field = info.variableFields[fieldName];
|
||||
QJsonObject fieldJson;
|
||||
fieldJson["name"] = field.name;
|
||||
fieldJson["cname"] = field.cname;
|
||||
fieldJson["hsType"] = field.hsType;
|
||||
fieldJson["desc"] = field.desc;
|
||||
variableFieldsJson[fieldName] = fieldJson;
|
||||
}
|
||||
interfaceJson["variableFileds"] = variableFieldsJson;
|
||||
|
||||
return interfaceJson;
|
||||
}
|
||||
|
||||
QJsonObject MetadataProcessor::readCache()
|
||||
{
|
||||
QString cachePath = QDir(m_uf2touft3Path).filePath("metadata_cache.json");
|
||||
if (!QFile::exists(cachePath)) {
|
||||
return QJsonObject();
|
||||
}
|
||||
return readJson(cachePath);
|
||||
}
|
||||
|
||||
bool MetadataProcessor::writeCache(const QJsonObject& cache)
|
||||
{
|
||||
QString cachePath = QDir(m_uf2touft3Path).filePath("metadata_cache.json");
|
||||
return writeJson(cachePath, cache);
|
||||
}
|
||||
|
||||
bool MetadataProcessor::processUf2Interfaces(const QString& basePath)
|
||||
{
|
||||
LogManager::instance()->log("========== 开始处理 UF2.0 接口 ==========");
|
||||
LogManager::instance()->log(QString("基础路径(basePath): %1").arg(basePath));
|
||||
|
||||
QString configPath = QDir(m_uf2touft3Path).filePath("config.ini");
|
||||
LogManager::instance()->log(QString("配置文件路径: %1").arg(configPath));
|
||||
|
||||
QStringList completeNameList = {"module.xml", ".project", "冲突检测.wlua"};
|
||||
QStringList containNameList = {".vbusiness", "/公共资源/", "/数据库/", "/脚本/", "/通用数据/", "/.svn/"};
|
||||
|
||||
|
|
@ -210,78 +282,100 @@ bool MetadataProcessor::processUf2Interfaces(const QString& basePath)
|
|||
LogManager::instance()->log(QString("UFT 数据库设置路径: %1").arg(uftDbSett));
|
||||
|
||||
QString custJsonPath = QDir(m_uf2touft3Path).filePath("cust.json");
|
||||
LogManager::instance()->log(QString("客户配置文件路径: %1").arg(custJsonPath));
|
||||
QJsonObject custJson = readJson(custJsonPath);
|
||||
|
||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2.json");
|
||||
|
||||
Uf2Interface uf2;
|
||||
QMap<QString, InterfaceInfo> resultMap = uf2.scanDir(dirPath, ufAcct20ProjectDir, uftDbSett, filterDict, custJson);
|
||||
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;
|
||||
for (const QString& key : resultMap.keys()) {
|
||||
const InterfaceInfo& info = resultMap[key];
|
||||
QJsonObject newUf2Cache;
|
||||
|
||||
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;
|
||||
if (full) {
|
||||
LogManager::instance()->log("UF2.0 全量扫描模式");
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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; // 未变化:直接沿用旧缓存记录
|
||||
}
|
||||
}
|
||||
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;
|
||||
// 删除已从磁盘移除的文件对应的条目
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
interfaceJson["variableFileds"] = variableFieldsJson;
|
||||
|
||||
resultJson[key] = interfaceJson;
|
||||
LogManager::instance()->log(QString("UF2.0 增量扫描耗时: %1ms").arg(timer.elapsed()));
|
||||
}
|
||||
|
||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2.json");
|
||||
|
||||
if (!writeJson(outputPath, resultJson)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UF2.0 接口处理完成,结果已保存到: %1").arg(outputPath));
|
||||
if (!resultMap.isEmpty()) {
|
||||
QString firstKey = resultMap.keys().first();
|
||||
QJsonObject merged = oldCache;
|
||||
merged["uf2"] = newUf2Cache;
|
||||
if (!writeCache(merged)) {
|
||||
LogManager::instance()->logWarning("UF2.0 缓存写入失败(不影响本次结果)");
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UF2.0 接口处理完成,结果已保存到: %1").arg(outputPath));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -290,9 +384,6 @@ bool MetadataProcessor::processUft3Interfaces(const QString& basePath)
|
|||
LogManager::instance()->log("========== 开始处理 UFT3.0 接口 ==========");
|
||||
LogManager::instance()->log(QString("基础路径(basePath): %1").arg(basePath));
|
||||
|
||||
QString configPath = QDir(m_uf2touft3Path).filePath("config.ini");
|
||||
LogManager::instance()->log(QString("配置文件路径: %1").arg(configPath));
|
||||
|
||||
QString moduleGenerationCheck = m_moduleGenerationCheck;
|
||||
LogManager::instance()->log(QString("模块生成检查: %1").arg(moduleGenerationCheck));
|
||||
|
||||
|
|
@ -306,45 +397,134 @@ bool MetadataProcessor::processUft3Interfaces(const QString& basePath)
|
|||
QString dirPath = m_uft30Path;
|
||||
LogManager::instance()->log(QString("UFT30 项目路径: %1").arg(dirPath));
|
||||
|
||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3.json");
|
||||
QString transOutputPath = QDir(m_uf2touft3Path).filePath("uft3Trans.json");
|
||||
|
||||
Uft3Interface uft3;
|
||||
auto result = uft3.scanDir(dirPath, filterDict, moduleGenerationCheck);
|
||||
QJsonObject oldMainJson = readJson(outputPath);
|
||||
QJsonObject oldTransJson = readJson(transOutputPath);
|
||||
QJsonObject oldCache = readCache();
|
||||
|
||||
QMap<QString, InterfaceInfo> resultMap = result.first;
|
||||
QMap<QString, InterfaceInfo> transCodeMap = result.second;
|
||||
QList<Uft3FileEntry> entries = uft3.collectFiles(dirPath, filterDict, moduleGenerationCheck);
|
||||
|
||||
bool full = m_forceFull || oldMainJson.isEmpty()
|
||||
|| !oldCache.contains("uft3") || oldCache["uft3"].toObject().isEmpty();
|
||||
|
||||
QJsonObject resultJson;
|
||||
for (const QString& key : resultMap.keys()) {
|
||||
const InterfaceInfo& info = resultMap[key];
|
||||
resultJson[key] = interfaceInfoToUFT3Json(info);
|
||||
}
|
||||
QJsonObject transCodeJson;
|
||||
QJsonObject newUft3Cache;
|
||||
|
||||
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3.json");
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject transCodeJson;
|
||||
for (const QString& key : transCodeMap.keys()) {
|
||||
const InterfaceInfo& info = transCodeMap[key];
|
||||
transCodeJson[key] = interfaceInfoToUFT3Json(info);
|
||||
}
|
||||
|
||||
QString transOutputPath = QDir(m_uf2touft3Path).filePath("uft3Trans.json");
|
||||
|
||||
if (!writeJson(transOutputPath, transCodeJson)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UFT3.0 接口处理完成,结果已保存到: %1").arg(outputPath));
|
||||
QJsonObject merged = oldCache;
|
||||
merged["uft3"] = newUft3Cache;
|
||||
if (!writeCache(merged)) {
|
||||
LogManager::instance()->logWarning("UFT3.0 缓存写入失败(不影响本次结果)");
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UFT3.0 接口处理完成,结果已保存到: %1").arg(outputPath));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MetadataProcessor::processUf2DataType()
|
||||
{
|
||||
LogManager::instance()->log("========== 开始处理 UF2.0 数据类型 ==========");
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
QString dirPath = m_uf20Path;
|
||||
|
||||
|
|
@ -364,10 +544,11 @@ bool MetadataProcessor::processUf2DataType()
|
|||
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2dataType.json");
|
||||
|
||||
if (!writeJson(outputPath, resultJson)) {
|
||||
LogManager::instance()->log(QString("UF2.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||
return false;
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UF2.0 数据类型处理完成,结果已保存到: %1").arg(outputPath));
|
||||
LogManager::instance()->log(QString("UF2.0 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -375,6 +556,8 @@ bool MetadataProcessor::processUf2DataType()
|
|||
bool MetadataProcessor::processUf2StdFields()
|
||||
{
|
||||
LogManager::instance()->log("========== 开始处理 UF2.0 标准字段 ==========");
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
QString dirPath = m_uf20Path;
|
||||
|
||||
|
|
@ -406,10 +589,11 @@ bool MetadataProcessor::processUf2StdFields()
|
|||
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2stdfield.json");
|
||||
|
||||
if (!writeJson(outputPath, resultJson)) {
|
||||
LogManager::instance()->log(QString("UF2.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||
return false;
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UF2.0 标准字段处理完成,结果已保存到: %1").arg(outputPath));
|
||||
LogManager::instance()->log(QString("UF2.0 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -417,6 +601,8 @@ bool MetadataProcessor::processUf2StdFields()
|
|||
bool MetadataProcessor::processUf2ErrNumber()
|
||||
{
|
||||
LogManager::instance()->log("========== 开始处理 UF2.0 标准错误号 ==========");
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
QString dirPath = m_uf20Path;
|
||||
|
||||
|
|
@ -436,10 +622,11 @@ bool MetadataProcessor::processUf2ErrNumber()
|
|||
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2Error.json");
|
||||
|
||||
if (!writeJson(outputPath, resultJson)) {
|
||||
LogManager::instance()->log(QString("UF2.0 标准错误号处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||
return false;
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UF2.0 标准错误号处理完成,结果已保存到: %1").arg(outputPath));
|
||||
LogManager::instance()->log(QString("UF2.0 标准错误号处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -447,6 +634,8 @@ bool MetadataProcessor::processUf2ErrNumber()
|
|||
bool MetadataProcessor::processUft3DataType()
|
||||
{
|
||||
LogManager::instance()->log("========== 开始处理 UFT3.0 数据类型 ==========");
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
QString dirPath = m_uft30PubPath;
|
||||
|
||||
|
|
@ -466,10 +655,11 @@ bool MetadataProcessor::processUft3DataType()
|
|||
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3datatype.json");
|
||||
|
||||
if (!writeJson(outputPath, resultJson)) {
|
||||
LogManager::instance()->log(QString("UFT3.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||
return false;
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UFT3.0 数据类型处理完成,结果已保存到: %1").arg(outputPath));
|
||||
LogManager::instance()->log(QString("UFT3.0 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -477,6 +667,8 @@ bool MetadataProcessor::processUft3DataType()
|
|||
bool MetadataProcessor::processUft3StdFields()
|
||||
{
|
||||
LogManager::instance()->log("========== 开始处理 UFT3.0 标准字段 ==========");
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
QString dirPath = m_uft30PubPath;
|
||||
|
||||
|
|
@ -508,10 +700,11 @@ bool MetadataProcessor::processUft3StdFields()
|
|||
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3stdfields.json");
|
||||
|
||||
if (!writeJson(outputPath, resultJson)) {
|
||||
LogManager::instance()->log(QString("UFT3.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||
return false;
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UFT3.0 标准字段处理完成,结果已保存到: %1").arg(outputPath));
|
||||
LogManager::instance()->log(QString("UFT3.0 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -519,6 +712,8 @@ bool MetadataProcessor::processUft3StdFields()
|
|||
bool MetadataProcessor::processUft3Table()
|
||||
{
|
||||
LogManager::instance()->log("========== 开始处理 UFT3.0 表结构 ==========");
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
QString dirPath = m_uft30PubPath;
|
||||
|
||||
|
|
@ -541,10 +736,11 @@ bool MetadataProcessor::processUft3Table()
|
|||
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3Table.json");
|
||||
|
||||
if (!writeJson(outputPath, resultJson)) {
|
||||
LogManager::instance()->log(QString("UFT3.0 表结构处理失败,耗时: %1ms").arg(timer.elapsed()));
|
||||
return false;
|
||||
}
|
||||
|
||||
LogManager::instance()->log(QString("UFT3.0 表结构处理完成,结果已保存到: %1").arg(outputPath));
|
||||
LogManager::instance()->log(QString("UFT3.0 表结构处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ public:
|
|||
bool processUf2Interfaces(const QString& basePath);
|
||||
bool processUft3Interfaces(const QString& basePath);
|
||||
|
||||
void setForceFull(bool force); // 强制全量更新(兼容 UI;原版每次均全量解析,无增量缓存时不影响行为)
|
||||
|
||||
bool processUf2StdFields();
|
||||
bool processUf2DataType();
|
||||
bool processUf2ErrNumber();
|
||||
|
|
@ -38,6 +40,11 @@ private:
|
|||
bool writeJson(const QString& filePath, const QJsonObject& jsonObject);
|
||||
QJsonObject readJson(const QString& filePath);
|
||||
QJsonObject interfaceInfoToUFT3Json(const InterfaceInfo& info);
|
||||
QJsonObject interfaceInfoToUF2Json(const InterfaceInfo& info);
|
||||
|
||||
// 轻量增量缓存(仅记录 文件路径->修改时间(mtime)+该文件在 JSON 中的 key)
|
||||
QJsonObject readCache();
|
||||
bool writeCache(const QJsonObject& cache);
|
||||
|
||||
QString m_basePath;
|
||||
QString m_uf2touft3Path;
|
||||
|
|
@ -51,6 +58,8 @@ private:
|
|||
|
||||
QString m_supportUftdbSett;
|
||||
QString m_moduleGenerationCheck;
|
||||
|
||||
bool m_forceFull = false; // 强制全量更新标志(兼容 UI;原版每次均全量解析,无增量缓存时不影响行为)
|
||||
};
|
||||
|
||||
#endif // METADATAPROCESSOR_H
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
#include <QDir>
|
||||
#include <QStack>
|
||||
#include <functional>
|
||||
#include <functional>
|
||||
|
||||
Uf2Interface::Uf2Interface(QObject *parent) : QObject(parent)
|
||||
{
|
||||
|
|
@ -294,3 +295,97 @@ QMap<QString, InterfaceInfo> Uf2Interface::scanDir(const QString& dirPath,
|
|||
|
||||
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 <QString>
|
||||
#include <QMap>
|
||||
#include <QList>
|
||||
#include <QJsonObject>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
|
|
@ -41,6 +42,13 @@ struct InterfaceInfo {
|
|||
QMap<QString, FieldInfo> internalFields;
|
||||
};
|
||||
|
||||
// 仅记录文件路径、归属类型、修改时间,用于轻量增量缓存
|
||||
struct Uf2FileEntry {
|
||||
QString path;
|
||||
QString serverType; // trade / acct / dbsett
|
||||
qint64 mtime = 0;
|
||||
};
|
||||
|
||||
class Uf2Interface : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -54,6 +62,18 @@ public:
|
|||
const QMap<QString, QStringList>& fileFilterDict,
|
||||
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:
|
||||
QString getModuleEName(const QString& moduleXmlPath);
|
||||
bool shouldFilter(const QString& filePath,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QTextStream>
|
||||
#include <functional>
|
||||
|
||||
Uft3Interface::Uft3Interface(QObject *parent) : QObject(parent)
|
||||
{
|
||||
|
|
@ -243,3 +244,57 @@ Uft3Interface::scanDir(const QString& dirPath,
|
|||
|
||||
return qMakePair(resultMap, transCodeMap);
|
||||
}
|
||||
|
||||
InterfaceInfo Uft3Interface::assemble(const QString& filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
fileInfo.setFile(fileInfo.absoluteFilePath()); // 标准化路径
|
||||
QString modName = fileInfo.dir().dirName();
|
||||
InterfaceInfo interfaceInfo = loadInterface(filePath);
|
||||
interfaceInfo.moudle = modName;
|
||||
return interfaceInfo;
|
||||
}
|
||||
|
||||
QList<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 <QXmlStreamReader>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
|
||||
#include "uf2interface.h"
|
||||
|
||||
// 仅记录文件路径、修改时间、是否属于交易码,用于轻量增量缓存
|
||||
struct Uft3FileEntry {
|
||||
QString path;
|
||||
qint64 mtime = 0;
|
||||
bool isTrans = false;
|
||||
};
|
||||
|
||||
class Uft3Interface : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -21,6 +29,12 @@ public:
|
|||
const QMap<QString, QStringList>& fileFilterDict,
|
||||
const QString& moduleGenerationCheck);
|
||||
|
||||
// 以下为增量更新提供复用能力(与 scanDir 内部逻辑一致)
|
||||
InterfaceInfo assemble(const QString& filePath);
|
||||
QList<Uft3FileEntry> collectFiles(const QString& dirPath,
|
||||
const QMap<QString, QStringList>& fileFilterDict,
|
||||
const QString& moduleGenerationCheck);
|
||||
|
||||
private:
|
||||
bool shouldFilter(const QString& filePath,
|
||||
const QString& dirPath,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent)
|
||||
{
|
||||
setFixedSize(354, 180);
|
||||
setFixedSize(360, 210);
|
||||
setWindowTitle("关于");
|
||||
setStandardButtons(QDialogButtonBox::Ok);
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ AboutDialog::AboutDialog(QWidget* parent) : InterDialog(parent)
|
|||
QFont nameTextFont = nameText->font();
|
||||
nameTextFont.setWeight(QFont::Bold);
|
||||
nameText->setFont(nameTextFont);
|
||||
nameText->setTextPixelSize(17);
|
||||
nameText->setTextPixelSize(15);
|
||||
nameText->setText("UFT30 ChangeCode");
|
||||
|
||||
auto* versionText = new ElaText(this);
|
||||
|
|
|
|||
|
|
@ -152,11 +152,14 @@ void BatchConvertPage::onProgressUpdate(int progress, const QString& status)
|
|||
{
|
||||
m_progressBar->setValue(progress);
|
||||
|
||||
QString displayStatus = status;
|
||||
if (displayStatus.length() > 80) {
|
||||
displayStatus = displayStatus.left(80) + "...";
|
||||
// 仅当传入有效状态文字时才更新标签,避免无意义的日志行覆盖当前处理状态
|
||||
if (!status.isEmpty()) {
|
||||
QString displayStatus = status;
|
||||
if (displayStatus.length() > 80) {
|
||||
displayStatus = displayStatus.left(80) + "...";
|
||||
}
|
||||
m_progressLabel->setText(displayStatus);
|
||||
}
|
||||
m_progressLabel->setText(displayStatus);
|
||||
}
|
||||
|
||||
bool BatchConvertPage::checkFunctionExists(const QString &funcName)
|
||||
|
|
|
|||
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 "utils/Constants.h"
|
||||
#include "utils/logmanager.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGridLayout>
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
const int UPDATE_ITEM_SHOW_COLUMN_COUNT = 3;
|
||||
|
||||
|
|
@ -17,13 +19,18 @@ MetadataUpdatePage::MetadataUpdatePage(QWidget *parent)
|
|||
initUI();
|
||||
_processor = new MetadataProcessor(this);
|
||||
_processor->initConfig(Constants::toolDir());
|
||||
|
||||
_watcher = new QFutureWatcher<UpdateResult>(this);
|
||||
connect(_watcher, &QFutureWatcher<UpdateResult>::finished,
|
||||
this, &MetadataUpdatePage::onWorkerFinished);
|
||||
connect(this, &MetadataUpdatePage::sigProgress,
|
||||
this, &MetadataUpdatePage::onUpdateProgress, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
MetadataUpdatePage::~MetadataUpdatePage()
|
||||
{
|
||||
if (_workerThread && _workerThread->isRunning()) {
|
||||
_workerThread->quit();
|
||||
_workerThread->wait();
|
||||
if (_watcher && _watcher->isRunning()) {
|
||||
_watcher->waitForFinished();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +79,13 @@ void MetadataUpdatePage::initUI()
|
|||
gridLayout->addWidget(_uft3CancelBtn, 1, 3, 1, 1);
|
||||
|
||||
_uft3UpdateBtn = new ElaPushButton("更新UFT30", contentWidget);
|
||||
_uft3UpdateBtn->setMinimumWidth(140);
|
||||
_uft3UpdateBtn->setMinimumHeight(38);
|
||||
{
|
||||
QFont f = _uft3UpdateBtn->font();
|
||||
f.setPointSize(11);
|
||||
_uft3UpdateBtn->setFont(f);
|
||||
}
|
||||
connect(_uft3UpdateBtn, &QPushButton::clicked, this, &MetadataUpdatePage::onUpdateUFT3);
|
||||
gridLayout->addWidget(_uft3UpdateBtn, 1, 4, 1, 1);
|
||||
|
||||
|
|
@ -105,17 +119,42 @@ void MetadataUpdatePage::initUI()
|
|||
gridLayout->addWidget(_uf2CancelBtn, 1, 8, 1, 1);
|
||||
|
||||
_uf2UpdateBtn = new ElaPushButton("更新UF20", contentWidget);
|
||||
_uf2UpdateBtn->setMinimumWidth(140);
|
||||
_uf2UpdateBtn->setMinimumHeight(38);
|
||||
{
|
||||
QFont f = _uf2UpdateBtn->font();
|
||||
f.setPointSize(11);
|
||||
_uf2UpdateBtn->setFont(f);
|
||||
}
|
||||
connect(_uf2UpdateBtn, &QPushButton::clicked, this, &MetadataUpdatePage::onUpdateUF20);
|
||||
gridLayout->addWidget(_uf2UpdateBtn, 1, 9, 1, 1);
|
||||
|
||||
gridLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Minimum), 0, 10, 2, 1);
|
||||
|
||||
_forceFullCheckBox = new ElaCheckBox("强制全量更新", contentWidget);
|
||||
_forceFullCheckBox->setToolTip("勾选后本次将重新扫描并解析全部接口文件、重建增量缓存;不勾选时仅处理有变化的文件");
|
||||
connect(_forceFullCheckBox, &QCheckBox::toggled, this, [this](bool checked){
|
||||
if (checked) {
|
||||
auto ret = QMessageBox::question(this, "提示",
|
||||
"全量更新接口耗时较久,请确认是否继续?",
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (ret != QMessageBox::Yes) {
|
||||
_forceFullCheckBox->setChecked(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
centerLayout->addWidget(contentWidget);
|
||||
|
||||
centerLayout->addItem(new QSpacerItem(5, 5, QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Expanding));
|
||||
|
||||
// 进度标签行:左侧显示状态文字,右侧放置强制全量更新复选框
|
||||
QHBoxLayout* processHeaderLayout = new QHBoxLayout();
|
||||
_processLabel = new ElaText(this);
|
||||
centerLayout->addWidget(_processLabel);
|
||||
_processLabel->setTextPixelSize(14);
|
||||
processHeaderLayout->addWidget(_processLabel, 1);
|
||||
processHeaderLayout->addWidget(_forceFullCheckBox);
|
||||
centerLayout->addLayout(processHeaderLayout);
|
||||
|
||||
_progressBar = new ElaProgressBar(this);
|
||||
_progressBar->setValue(0);
|
||||
|
|
@ -209,8 +248,9 @@ void MetadataUpdatePage::onUpdateUF20()
|
|||
_uf2AllBtn->setEnabled(false);
|
||||
_uf2CancelBtn->setEnabled(false);
|
||||
_progressBar->setValue(0);
|
||||
_processor->setForceFull(_forceFullCheckBox->isChecked());
|
||||
|
||||
updateUf2Interfaces(selectedItems);
|
||||
startUpdate(selectedItems, "UF20");
|
||||
}
|
||||
|
||||
void MetadataUpdatePage::onUFT3SelectAll()
|
||||
|
|
@ -245,21 +285,21 @@ void MetadataUpdatePage::onUpdateUFT3()
|
|||
_uft3AllBtn->setEnabled(false);
|
||||
_uft3CancelBtn->setEnabled(false);
|
||||
_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;
|
||||
int itemCount = items.size();
|
||||
int idx = 0;
|
||||
|
||||
foreach (const QString& item, items) {
|
||||
_processLabel->setText(QString("正在更新UF20 - %1").arg(item));
|
||||
int currentIndex = items.indexOf(item);
|
||||
int progress = (itemCount == 1) ? 0 : (currentIndex * 100 / itemCount);
|
||||
_progressBar->setValue(progress);
|
||||
QCoreApplication::processEvents();
|
||||
for (const QString& item : items) {
|
||||
emit sigProgress((itemCount == 1) ? 0 : (idx * 100 / itemCount),
|
||||
QString("正在更新UF20 - %1").arg(item));
|
||||
|
||||
if (item == "标准字段") {
|
||||
processUf2StdFields();
|
||||
|
|
@ -268,34 +308,28 @@ void MetadataUpdatePage::updateUf2Interfaces(const QStringList& items)
|
|||
} else if (item == "标准错误号") {
|
||||
processUf2ErrNumber();
|
||||
} else if (item == "接口数据") {
|
||||
processUf2Interface(Constants::toolDir());
|
||||
if (!processUf2Interface(Constants::toolDir())) {
|
||||
success = false;
|
||||
}
|
||||
interfaceUpdated = true;
|
||||
}
|
||||
++idx;
|
||||
}
|
||||
|
||||
if (interfaceUpdated) {
|
||||
_processLabel->setText("正在更新数据缓存...");
|
||||
QCoreApplication::processEvents();
|
||||
UF2ConfigReader::reloadUF20Config();
|
||||
}
|
||||
|
||||
_progressBar->setValue(100);
|
||||
_processLabel->setText("UF20更新完成");
|
||||
|
||||
onUpdateFinished(true, "UF20更新完成");
|
||||
// 数据缓存重载放到主线程执行(DataCache 单例非线程安全),见 onWorkerFinished
|
||||
return UpdateResult{success, interfaceUpdated};
|
||||
}
|
||||
|
||||
void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items)
|
||||
UpdateResult MetadataUpdatePage::runUpdateUft3(const QStringList& items)
|
||||
{
|
||||
int itemCount = items.size();
|
||||
bool success = true;
|
||||
bool interfaceUpdated = false;
|
||||
int itemCount = items.size();
|
||||
int idx = 0;
|
||||
|
||||
foreach (const QString& item, items) {
|
||||
_processLabel->setText(QString("正在更新UFT30 - %1").arg(item));
|
||||
int currentIndex = items.indexOf(item);
|
||||
int progress = (itemCount == 1) ? 0 : (currentIndex * 100 / itemCount);
|
||||
_progressBar->setValue(progress);
|
||||
QCoreApplication::processEvents();
|
||||
for (const QString& item : items) {
|
||||
emit sigProgress((itemCount == 1) ? 0 : (idx * 100 / itemCount),
|
||||
QString("正在更新UFT30 - %1").arg(item));
|
||||
|
||||
if (item == "标准字段") {
|
||||
processUft3StdFields();
|
||||
|
|
@ -304,21 +338,15 @@ void MetadataUpdatePage::updateUft3Interfaces(const QStringList& items)
|
|||
} else if (item == "表结构") {
|
||||
processUft3Table();
|
||||
} else if (item == "接口数据") {
|
||||
processUft3Interface(Constants::toolDir());
|
||||
if (!processUft3Interface(Constants::toolDir())) {
|
||||
success = false;
|
||||
}
|
||||
interfaceUpdated = true;
|
||||
}
|
||||
++idx;
|
||||
}
|
||||
|
||||
if (interfaceUpdated) {
|
||||
_processLabel->setText("正在更新数据缓存...");
|
||||
QCoreApplication::processEvents();
|
||||
UFT3ConfigReader::reloadUFT3Config();
|
||||
}
|
||||
|
||||
_progressBar->setValue(100);
|
||||
_processLabel->setText("UFT30更新完成");
|
||||
|
||||
onUpdateFinished(true, "UFT30更新完成");
|
||||
return UpdateResult{success, interfaceUpdated};
|
||||
}
|
||||
|
||||
void MetadataUpdatePage::processUf2StdFields()
|
||||
|
|
@ -336,9 +364,9 @@ void MetadataUpdatePage::processUf2ErrNumber()
|
|||
_processor->processUf2ErrNumber();
|
||||
}
|
||||
|
||||
void MetadataUpdatePage::processUf2Interface(const QString& basePath)
|
||||
bool MetadataUpdatePage::processUf2Interface(const QString& basePath)
|
||||
{
|
||||
_processor->processUf2Interfaces(basePath);
|
||||
return _processor->processUf2Interfaces(basePath);
|
||||
}
|
||||
|
||||
void MetadataUpdatePage::processUft3StdFields()
|
||||
|
|
@ -356,9 +384,60 @@ void MetadataUpdatePage::processUft3Table()
|
|||
_processor->processUft3Table();
|
||||
}
|
||||
|
||||
void MetadataUpdatePage::processUft3Interface(const QString& basePath)
|
||||
bool MetadataUpdatePage::processUft3Interface(const QString& basePath)
|
||||
{
|
||||
_processor->processUft3Interfaces(basePath);
|
||||
return _processor->processUft3Interfaces(basePath);
|
||||
}
|
||||
|
||||
void MetadataUpdatePage::startUpdate(const QStringList& items, const QString& taskName)
|
||||
{
|
||||
m_currentTaskName = taskName;
|
||||
_updateTimer.start();
|
||||
_progressBar->setValue(0);
|
||||
_processLabel->setText(QString("正在准备更新%1...").arg(taskName));
|
||||
|
||||
// 防御:若上一个任务仍在运行则不再启动(按钮已禁用,正常情况下不会触发)
|
||||
if (_watcher->isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (taskName == "UF20") {
|
||||
_watcher->setFuture(QtConcurrent::run([this, items]() {
|
||||
return runUpdateUf2(items);
|
||||
}));
|
||||
} else {
|
||||
_watcher->setFuture(QtConcurrent::run([this, items]() {
|
||||
return runUpdateUft3(items);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void MetadataUpdatePage::onWorkerFinished()
|
||||
{
|
||||
UpdateResult r = _watcher->result();
|
||||
|
||||
// 计算总耗时
|
||||
qint64 elapsedMs = _updateTimer.elapsed();
|
||||
QString elapsedStr;
|
||||
if (elapsedMs < 1000) {
|
||||
elapsedStr = QString::number(elapsedMs) + "ms";
|
||||
} else {
|
||||
elapsedStr = QString("%1s (%2ms)").arg(elapsedMs / 1000.0, 0, 'f', 1).arg(elapsedMs);
|
||||
}
|
||||
LogManager::instance()->log(QString("%1 元数据更新总耗时: %2").arg(m_currentTaskName, elapsedStr));
|
||||
|
||||
// 数据缓存重载必须在主线程执行(DataCache 单例与静态 cnameCache 非线程安全)
|
||||
if (r.needReload) {
|
||||
_processLabel->setText("正在更新数据缓存...");
|
||||
if (m_currentTaskName == "UF20") {
|
||||
UF2ConfigReader::reloadUF20Config();
|
||||
} else {
|
||||
UFT3ConfigReader::reloadUFT3Config();
|
||||
}
|
||||
}
|
||||
|
||||
onUpdateFinished(r.success, QString("%1更新完成").arg(m_currentTaskName));
|
||||
_processLabel->setText(QString("%1更新完成(总耗时: %2)").arg(m_currentTaskName, elapsedStr));
|
||||
}
|
||||
|
||||
void MetadataUpdatePage::onUpdateProgress(int progress, const QString& message)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,14 @@
|
|||
#include <QGridLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QThread>
|
||||
#include <QFutureWatcher>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
// 后台线程执行结果:success 表示整体是否成功,needReload 表示是否需要重新加载数据缓存
|
||||
struct UpdateResult {
|
||||
bool success;
|
||||
bool needReload;
|
||||
};
|
||||
|
||||
class MetadataUpdatePage : public ElaScrollPage
|
||||
{
|
||||
|
|
@ -35,22 +43,30 @@ private slots:
|
|||
private slots:
|
||||
void onUpdateProgress(int progress, const QString& message);
|
||||
void onUpdateFinished(bool success, const QString& message);
|
||||
void onWorkerFinished(); // QFutureWatcher::finished 的回调(运行在主线程)
|
||||
|
||||
signals:
|
||||
void sigProgress(int progress, const QString& message); // 后台线程 -> 主线程回传进度
|
||||
|
||||
private:
|
||||
void initUI();
|
||||
void showUpdateItems();
|
||||
void updateUf2Interfaces(const QStringList& items);
|
||||
void updateUft3Interfaces(const QStringList& items);
|
||||
|
||||
// 后台线程执行体(由 QtConcurrent 调用,禁止直接操作 UI)
|
||||
UpdateResult runUpdateUf2(const QStringList& items);
|
||||
UpdateResult runUpdateUft3(const QStringList& items);
|
||||
// 在主线程启动后台任务
|
||||
void startUpdate(const QStringList& items, const QString& taskName);
|
||||
|
||||
void processUf2StdFields();
|
||||
void processUf2DataType();
|
||||
void processUf2ErrNumber();
|
||||
void processUf2Interface(const QString& basePath);
|
||||
bool processUf2Interface(const QString& basePath);
|
||||
|
||||
void processUft3StdFields();
|
||||
void processUft3DataType();
|
||||
void processUft3Table();
|
||||
void processUft3Interface(const QString& basePath);
|
||||
bool processUft3Interface(const QString& basePath);
|
||||
|
||||
QGridLayout* _updateGridLayout{nullptr};
|
||||
QGridLayout* _updateUf2GridLayout{nullptr};
|
||||
|
|
@ -60,6 +76,7 @@ private:
|
|||
ElaPushButton* _uft3CancelBtn{nullptr};
|
||||
ElaPushButton* _uf2AllBtn{nullptr};
|
||||
ElaPushButton* _uft3UpdateBtn{nullptr};
|
||||
ElaCheckBox* _forceFullCheckBox{nullptr};
|
||||
ElaText* _processLabel{nullptr};
|
||||
ElaProgressBar* _progressBar{nullptr};
|
||||
|
||||
|
|
@ -67,7 +84,9 @@ private:
|
|||
QList<ElaCheckBox*> uf20CheckBoxes;
|
||||
|
||||
MetadataProcessor* _processor{nullptr};
|
||||
QThread* _workerThread{nullptr};
|
||||
QFutureWatcher<UpdateResult>* _watcher{nullptr};
|
||||
QElapsedTimer _updateTimer;
|
||||
QString m_currentTaskName; // "UF20" / "UFT30",用于区分当前任务
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -14,7 +14,7 @@ namespace Constants {
|
|||
// 应用版本号(发版时只需修改此处)
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace App {
|
||||
constexpr char Version[] = "1.3.0";
|
||||
constexpr char Version[] = "1.4.0";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -35,6 +35,8 @@ namespace Paths {
|
|||
inline const QString ToolDir = "uf2touft3"; // 工具主目录
|
||||
inline const QString LogDir = "logs"; // 日志目录
|
||||
inline const QString CacheDir = "cache"; // 缓存目录
|
||||
inline const QString ChangeCodeDir = "change_code"; // 代码检索根目录(toolDir 下)
|
||||
inline const QString FunctionOutput = "function_output"; // 调用链/文件导出目录
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -67,6 +69,19 @@ inline QString logDir() { return QCoreApplication::applicationDirPath() + "
|
|||
inline QString cacheDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::CacheDir; }
|
||||
inline QString cacheDb() { return cacheDir() + "/" + Cache::DbFile; }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 代码检索(CodeSearch)相关常量与路径
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace CodeSearch {
|
||||
constexpr char ChangeCodeIni[] = "change_code.ini"; // 路径持久化配置文件名
|
||||
constexpr char ConvertPathKey[] = "codeSearch/convertPath"; // QSettings 键:转码业务路径
|
||||
constexpr char SearchPathKey[] = "codeSearch/searchPath"; // QSettings 键:非转码业务路径
|
||||
}
|
||||
|
||||
inline QString changeCodeDir() { return toolDir() + "/" + Paths::ChangeCodeDir; } // 默认代码检索根目录
|
||||
inline QString changeCodeIni() { return toolDir() + "/" + QString::fromLatin1(CodeSearch::ChangeCodeIni); }
|
||||
inline QString functionOutput() { return Paths::FunctionOutput; } // 调用链/文件导出目录名
|
||||
|
||||
} // namespace Constants
|
||||
|
||||
#endif // CONSTANTS_H
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
"release_notes": "1. 支持业务转码;\n2. 支持代码uf20、uft3功能查找;\n3.支持帮助菜单和在线帮助文档;\n4.支持接口元数据更新;\n5.设置菜单UFT30项目环境路径、UF20项目环境路径、UF20账户环境路径、转码生成路径、支持弹窗选择目录。"
|
||||
"release_notes": "1.支持业务转码;\n2.支持代码uf20、uft3功能查找;\n3.支持帮助菜单和在线帮助文档;\n4.支持接口元数据更新;\n5.设置菜单UFT30项目环境路径、UF20项目环境路径、UF20账户环境路径、转码生成路径、支持弹窗选择目录。"
|
||||
},
|
||||
"V1.1.0": {
|
||||
"release_notes": "1.支持2.0账户功能查询及业务转码"
|
||||
|
|
@ -10,5 +10,8 @@
|
|||
},
|
||||
"V1.3.0": {
|
||||
"release_notes": "1.修复设置路径不生效;\n2.修复元数据更新后业务转码模块对函数检查状态异常问题;"
|
||||
},
|
||||
"V1.4.0": {
|
||||
"release_notes": "1.提升业务转码性能;\n2.元数据更新支持增量更新和强制全量更新;\n3.新增代码检索功能;\n4.支持函数调用链路分析;\n5.支持全局文字可选中复制及表格Ctrl+C导出;"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue