新增代码检索功能、提升业务转码效率等

This commit is contained in:
taocong 2026-07-21 17:08:23 +08:00
parent b86fc071c2
commit 389c6eeecd
32 changed files with 2858 additions and 241 deletions

View File

@ -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

View File

@ -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 ==========================================

View File

@ -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

View File

@ -0,0 +1,11 @@
# 1.4.0
1.提升业务转码性能;
2.元数据更新支持增量更新和强制全量更新;
3.新增代码检索功能;
4.支持函数调用链路分析;
5.支持全局文字可选中复制及表格Ctrl+C导出;

View File

@ -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) |

View File

@ -0,0 +1,11 @@
# 代码检索
支持"转码业务"、"非转码业务"调用链路查询和导出
## 转码业务
支持查询调用链路和导出查询函数所涉及的文件以及调用链路导出
<img src="转码业务.png" alt="转码业务"/>
## 非转码业务
支持查询调用链路和调用链路导出
<img src="非转码业务.png" alt="非转码业务"/>

View File

@ -30,3 +30,5 @@
## 快速跳转
[业务转码](http://10.20.163.105:6045/uft3changecode/业务转码.html)
[代码检索](http://10.20.163.105:6045/uft3changecode/业务转码.html)

View File

@ -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();

View File

@ -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

View File

@ -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)

View File

@ -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;

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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,

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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 收尾,并将 downTreestartName 的下游树)挂在其末端。
// 例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

View File

@ -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导出;"
}
}