重构转码逻辑;元数据更新逻辑优化;元数据更新存储模式重构

This commit is contained in:
taocong
2026-09-03 10:27:43 +08:00
parent cb738cef09
commit 9a0abc0f12
20 changed files with 1233 additions and 773 deletions
+8
View File
@@ -27,9 +27,13 @@ SOURCES += main.cpp \
src/metadataupdate/uf2datatype.cpp \
src/metadataupdate/uf2stdfields.cpp \
src/metadataupdate/uf2errnumber.cpp \
src/metadataupdate/uf2constants.cpp \
src/metadataupdate/uf2menuandfunction.cpp \
src/metadataupdate/uf2module.cpp \
src/metadataupdate/uft3datatype.cpp \
src/metadataupdate/uft3stdfields.cpp \
src/metadataupdate/uft3table.cpp \
src/metadataupdate/uf2table.cpp \
src/utils/datacache.cpp \
src/utils/business_strategy.cpp \
src/utils/configreaderbase.cpp \
@@ -62,9 +66,13 @@ HEADERS += src/mainwindow/mainwindow.h \
src/metadataupdate/uf2datatype.h \
src/metadataupdate/uf2stdfields.h \
src/metadataupdate/uf2errnumber.h \
src/metadataupdate/uf2constants.h \
src/metadataupdate/uf2menuandfunction.h \
src/metadataupdate/uf2module.h \
src/metadataupdate/uft3datatype.h \
src/metadataupdate/uft3stdfields.h \
src/metadataupdate/uft3table.h \
src/metadataupdate/uf2table.h \
src/utils/datacache.h \
src/utils/business_strategy.h \
src/utils/configreaderbase.h \
+1 -1
View File
@@ -219,7 +219,7 @@ void MainWindow::onBatchConvertStart()
if (!m_pythonRunner->canRun()) {
LogManager::instance()->logError("错误: 找不到 uf2touft3/uf2touft3.exe!");
QMessageBox::critical(this, "错误",
QString("无法启动转换任务!\n\n请确保:\nuf2touft3/uf2touft3.exe 存在,且该目录下包含 config.ini\n\n期望路径:\n%1")
QString("无法启动转换任务!\n\n请确保:\nuf2touft3/uf2touft3.exe 存在,且该目录下包含 config/config.ini\n\n期望路径:\n%1")
.arg(exePath));
return;
}
+240 -209
View File
@@ -6,6 +6,10 @@
#include "uf2stdfields.h"
#include "uf2datatype.h"
#include "uf2errnumber.h"
#include "uf2constants.h"
#include "uf2menuandfunction.h"
#include "uf2module.h"
#include "uf2table.h"
#include "uft3stdfields.h"
#include "uft3datatype.h"
#include "uft3table.h"
@@ -17,6 +21,7 @@
#include <QSet>
#include <QSettings>
#include <QDir>
#include <QDirIterator>
#include <QElapsedTimer>
MetadataProcessor::MetadataProcessor(QObject *parent) : QObject(parent)
@@ -86,23 +91,8 @@ bool MetadataProcessor::isValidUtf8(const QByteArray& data)
bool MetadataProcessor::writeJson(const QString& filePath, const QJsonObject& jsonObject)
{
LogManager::instance()->log(QString("准备写入JSON文件: %1, 对象大小: %2").arg(filePath).arg(jsonObject.size()));
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
LogManager::instance()->logError(QString("无法打开文件: %1").arg(filePath));
return false;
}
QJsonDocument doc(jsonObject);
qint64 bytesWritten = file.write(doc.toJson(QJsonDocument::Indented));
file.close();
LogManager::instance()->log(QString("成功写入JSON文件: %1, 写入字节数: %2").arg(filePath).arg(bytesWritten));
return true;
}
// 元数据解析结果统一落双库(cache/uf20_cache.db / cache/uft3_cache.db),
// 不再生成任何中间 JSON 文件, 详见 saveRowsFromJson / DataCache::saveDataset。
QJsonObject MetadataProcessor::interfaceInfoToUFT3Json(const InterfaceInfo& info)
{
@@ -242,48 +232,76 @@ QJsonObject MetadataProcessor::interfaceInfoToUF2Json(const InterfaceInfo& info)
return interfaceJson;
}
QJsonObject MetadataProcessor::readCache()
// 递归取目录内最新文件的修改时间(秒级时间戳, 对齐 Python initdata._dir_mtime)。
// 路径为空或不存在返回 0.0。用于 _meta.src_mtime 缓存失效判断。
namespace {
double dirMtime(const QString& path)
{
QString cachePath = QDir(m_uf2touft3Path).filePath("metadata_cache.json");
if (!QFile::exists(cachePath)) {
return QJsonObject();
if (path.isEmpty()) {
LogManager::instance()->logWarning("[dirMtime] 源路径为空, src_mtime 记为 0");
return 0.0;
}
return readJson(cachePath);
QFileInfo fi(path);
if (!fi.exists()) {
LogManager::instance()->logWarning(QString("[dirMtime] 源路径不存在: %1").arg(path));
return 0.0;
}
if (fi.isFile()) {
double m = static_cast<double>(fi.lastModified().toSecsSinceEpoch());
LogManager::instance()->logInfo(QString("[dirMtime] 文件 %1 -> mtime=%2").arg(path).arg(m, 0, 'f', 3));
return m;
}
double latest = 0.0;
qint64 count = 0;
QDirIterator it(path, QDir::Files | QDir::Readable, QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
++count;
double m = static_cast<double>(it.fileInfo().lastModified().toSecsSinceEpoch());
if (m > latest) {
latest = m;
}
}
if (count == 0) {
// 未扫到任何文件(空目录/无权限/遍历异常): 回退为目录自身 mtime,
// 避免 src_mtime 记为 0 导致缓存失效判断始终认为"源已变更"。
latest = static_cast<double>(fi.lastModified().toSecsSinceEpoch());
LogManager::instance()->logWarning(QString("[dirMtime] 目录 %1 未扫到文件, 回退为目录自身 mtime=%2")
.arg(path).arg(latest, 0, 'f', 3));
return latest;
}
LogManager::instance()->logInfo(QString("[dirMtime] 目录 %1 扫描 %2 个文件 -> 最新 mtime=%3")
.arg(path).arg(count).arg(latest, 0, 'f', 3));
return latest;
}
}
bool MetadataProcessor::writeCache(const QJsonObject& cache)
{
QString cachePath = QDir(m_uf2touft3Path).filePath("metadata_cache.json");
return writeJson(cachePath, cache);
}
// 将缓存库 (name -> data JSON 字符串) 行集还原为 QJsonObject(key -> 子对象/子数组)
// srcMtime: 源目录最新修改时间(多源时调用方相加), 写入 _meta.src_mtime 供缓存失效判断。
void MetadataProcessor::syncJsonToCache(const QString& jsonPath, bool isUft3)
void MetadataProcessor::saveRowsFromJson(const QString& db, const QString& table,
const QJsonObject& resultJson, double srcMtime)
{
if (!DataCache::instance()->initDatabase()) {
LogManager::instance()->logError("元数据同步缓存失败 - 数据库初始化失败");
QMap<QString, QString> rows;
for (auto it = resultJson.begin(); it != resultJson.end(); ++it) {
QJsonObject val = it.value().toObject();
rows[it.key()] = QString::fromUtf8(QJsonDocument(val).toJson(QJsonDocument::Compact));
}
if (rows.isEmpty()) {
LogManager::instance()->logWarning(QString("元数据落库跳过 - %1.%2 为空").arg(db, table));
return;
}
QJsonObject json = readJson(jsonPath);
if (json.isEmpty()) {
LogManager::instance()->logWarning(
QString("元数据同步缓存跳过 - 读取为空: %1").arg(jsonPath));
// 注意: 此处不可调用 DataCache::initDatabase() 做前置可行性判断。
// initDatabase() 是"只读探测"(库文件不存在时返回 false), 若在此依赖它,
// 首次元数据更新(缓存库尚不存在)会被误判为失败而跳过落库, 导致只生成 0KB 空库、
// 需要第二次更新才有数据。写入路径直接调用 saveDataset 即可, 它内部会
// 自动创建 cache 目录、DB 文件与数据表。
LogManager::instance()->logInfo(QString("元数据落库 %1.%2 共 %3 行, src_mtime=%4")
.arg(db, table).arg(rows.size()).arg(srcMtime, 0, 'f', 3));
if (!DataCache::instance()->saveDataset(db, table, rows, srcMtime)) {
LogManager::instance()->logError(QString("元数据落库失败: %1.%2").arg(db, table));
return;
}
bool ok = isUft3 ? DataCache::instance()->saveUFT3Config(json)
: DataCache::instance()->saveUF20Config(json);
if (!ok) {
LogManager::instance()->logError(
QString("元数据同步缓存失败 - 写入%1失败").arg(isUft3 ? "UFT3" : "UF20"));
return;
}
// 记录当前工程路径指纹,供启动比对(详见 configreaderbase.cpp)
QStringList parts = {m_uf20Path, m_ufAcct20Path, m_uftDbSettPath,
m_uft30PubPath, m_uft30CbptransPath, m_uft30Path};
QStringList norm;
for (const QString& p : parts) norm.append(PathUtils::normPath(p));
DataCache::instance()->saveProjectPaths({{"fingerprint", norm.join("|")}});
LogManager::instance()->log(
QString("元数据同步缓存成功 - %1 已写入 config_cache.db").arg(isUft3 ? "UFT3" : "UF20"));
}
bool MetadataProcessor::processUf2Interfaces(const QString& basePath)
@@ -299,17 +317,12 @@ bool MetadataProcessor::processUf2Interfaces(const QString& basePath)
ctx.fileFilterDict = filterDict;
ctx.ufAcct20ProjectDir = m_ufAcct20Path;
ctx.uftDbSett = (m_supportUftdbSett == "0") ? QString() : m_uftDbSettPath;
QString custJsonPath = QDir(m_uf2touft3Path).filePath("cust.json");
QString custJsonPath = Constants::custJson();
ctx.custJson = readJson(custJsonPath);
Uf2Interface uf2;
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2.json");
bool ok = processInterfaces(&uf2, ctx, m_uf20Path, outputPath, QString(), "uf2", false);
// 生成成功后立即把新 json 同步进 config_cache.db(功能查询的数据源),
// 避免“换机器/换路径重新生成元数据后,db 仍停留在旧工程数据”导致功能查询查不到
if (ok) {
syncJsonToCache(outputPath, false);
}
// 解析结果直接落库 uf20.uf2_interface, 不再生成中间 JSON(uf2.json)
bool ok = processInterfaces(&uf2, ctx, m_uf20Path, "uf2", false);
return ok;
}
@@ -327,47 +340,35 @@ bool MetadataProcessor::processUft3Interfaces(const QString& basePath)
ctx.moduleGenerationCheck = m_moduleGenerationCheck;
Uft3Interface uft3;
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3.json");
QString transOutputPath = QDir(m_uf2touft3Path).filePath("uft3Trans.json");
bool ok = processInterfaces(&uft3, ctx, m_uft30Path, outputPath, transOutputPath, "uft3", true);
// 生成成功后立即把新 json 同步进 config_cache.db(功能查询的数据源),
// 避免“换机器/换路径重新生成元数据后,db 仍停留在旧工程数据”导致功能查询查不到
if (ok) {
syncJsonToCache(outputPath, true);
}
// 解析结果直接落库 uft3.uft3_interface / uft3.uft3_transcode, 不再生成中间 JSON(uft3.json/uft3Trans.json)
bool ok = processInterfaces(&uft3, ctx, m_uft30Path, "uft3", true);
return ok;
}
bool MetadataProcessor::processInterfaces(IInterfaceScanner* scanner,
const ScanContext& baseCtx,
const QString& dirPath,
const QString& outputPath,
const QString& transOutputPath,
const QString& cacheKey,
bool isUft3)
{
QList<FileEntry> entries = scanner->collectFiles(dirPath, baseCtx);
(void)entries; // 全量模式下扫描在 scanDir 内完成, entries 仅供增量模式遍历使用
QMap<QString, QString> moduleCodeMap = scanner->buildModuleCodeMap(dirPath, baseCtx);
(void)moduleCodeMap;
QJsonObject oldJson = readJson(outputPath);
QJsonObject oldTransJson = transOutputPath.isEmpty() ? QJsonObject() : readJson(transOutputPath);
QJsonObject oldCache = readCache();
bool full = m_forceFull || oldJson.isEmpty()
|| !oldCache.contains(cacheKey) || oldCache[cacheKey].toObject().isEmpty();
// 不再落中间 JSON / __file_cache 表, 元数据更新改为每次全量重新扫描所有接口文件并落库覆盖。
const QString& db = isUft3 ? "uft3" : "uf20";
const QString interfaceTable = isUft3 ? Constants::Cache::TblUft3Interface : Constants::Cache::TblUf2Interface;
// 无增量基线(__file_cache 已移除), 始终全量重扫。
QJsonObject resultJson;
QJsonObject transCodeJson;
QJsonObject newCache;
if (full) {
{
QElapsedTimer timer;
timer.start();
QMap<QString, qint64> mtimeByPath;
for (const FileEntry& e : entries) mtimeByPath[e.path] = e.mtime;
ScanResult scanResult = scanner->scanDir(dirPath, baseCtx);
QMap<QString, InterfaceInfo> resultMap = scanResult.resultMap;
QMap<QString, InterfaceInfo> transCodeMap = scanResult.transCodeMap;
@@ -375,96 +376,29 @@ bool MetadataProcessor::processInterfaces(IInterfaceScanner* scanner,
for (const QString& key : resultMap.keys()) {
const InterfaceInfo& info = resultMap[key];
resultJson[key] = isUft3 ? interfaceInfoToUFT3Json(info) : interfaceInfoToUF2Json(info);
QJsonObject rec;
rec["mtime"] = QString::number(mtimeByPath.value(info.path, 0));
rec["key"] = key;
rec["trans"] = false;
newCache[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;
newCache[info.path] = rec;
}
LogManager::instance()->log(QString("%1 全量扫描耗时: %2ms").arg(cacheKey).arg(timer.elapsed()));
}
// 解析结果直接落库, 不再生成中间 JSON 文件
// src_mtime 对齐 Python: UF2 接口源 = uf20 目录 + ufAcct20 目录(相加);
// UFT3 接口源 = 传入的工程目录 dirPath(更新 UFT3 时即 uft30 目录)。
const double srcMtime = isUft3 ? dirMtime(dirPath)
: (dirMtime(m_uf20Path) + dirMtime(m_ufAcct20Path));
if (isUft3) {
saveRowsFromJson("uft3", Constants::Cache::TblUft3Interface, resultJson, srcMtime);
if (!transCodeJson.isEmpty()) {
saveRowsFromJson("uft3", Constants::Cache::TblUft3TransCode, transCodeJson, srcMtime);
}
} else {
QElapsedTimer timer;
timer.start();
QJsonObject oldCacheForVer = oldCache[cacheKey].toObject();
QSet<QString> currentPaths;
resultJson = oldJson;
transCodeJson = oldTransJson;
for (const FileEntry& e : entries) {
currentPaths.insert(e.path);
QJsonObject rec = oldCacheForVer.value(e.path).toObject();
bool changed = rec.isEmpty() || rec["mtime"].toString().toLongLong() != e.mtime;
if (changed) {
ScanContext actx = baseCtx;
actx.serverType = e.serverType;
actx.moduleCodeMap = moduleCodeMap;
InterfaceInfo info = scanner->assemble(e.path, actx);
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);
}
saveRowsFromJson("uf20", Constants::Cache::TblUf2Interface, resultJson, srcMtime);
}
QJsonObject val = isUft3 ? interfaceInfoToUFT3Json(info) : interfaceInfoToUF2Json(info);
if (newTrans && !transOutputPath.isEmpty()) transCodeJson[newKey] = val;
else resultJson[newKey] = val;
QJsonObject crec;
crec["mtime"] = QString::number(e.mtime);
crec["key"] = newKey;
crec["trans"] = newTrans;
newCache[e.path] = crec;
} else {
newCache[e.path] = rec; // 未变化:直接沿用旧缓存记录
}
}
// 删除已从磁盘移除的文件对应的条目
for (auto it = oldCacheForVer.begin(); it != oldCacheForVer.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("%1 增量扫描耗时: %2ms").arg(cacheKey).arg(timer.elapsed()));
}
if (!writeJson(outputPath, resultJson)) return false;
if (!transOutputPath.isEmpty()) {
if (!writeJson(transOutputPath, transCodeJson)) return false;
}
QJsonObject merged = oldCache;
merged[cacheKey] = newCache;
if (!writeCache(merged)) {
LogManager::instance()->logWarning(QString("%1 缓存写入失败(不影响本次结果)").arg(cacheKey));
}
LogManager::instance()->log(QString("%1 接口处理完成,结果已保存到: %2").arg(cacheKey).arg(outputPath));
LogManager::instance()->log(QString("%1 接口处理完成,已落库: %2.%3").arg(cacheKey).arg(db, interfaceTable));
return true;
}
@@ -489,14 +423,11 @@ bool MetadataProcessor::processUf2DataType()
resultJson[key] = typeJson;
}
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2dataType.json");
// 解析结果直接落库 uf20.uf2_datatype, 不再生成中间 JSON(uf2dataType.json)
saveRowsFromJson("uf20", Constants::Cache::TblUf2DataType, resultJson, dirMtime(m_uf20Path));
if (!writeJson(outputPath, resultJson)) {
LogManager::instance()->log(QString("UF2.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
LogManager::instance()->log(QString("UF2.0 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
LogManager::instance()->log(QString("UF2.0 数据类型处理完成,耗时: %1ms,已落库: uf20.%2")
.arg(timer.elapsed()).arg(Constants::Cache::TblUf2DataType));
return true;
}
@@ -509,16 +440,19 @@ bool MetadataProcessor::processUf2StdFields()
QString dirPath = m_uf20Path;
// 数据类型依赖从缓存库 uf20.uf2_datatype 读取, 不再读中间 JSON(uf2dataType.json)
QMap<QString, QMap<QString, QString>> uf2datatypeMap;
QString datatypeJsonPath = QDir(m_uf2touft3Path).filePath("uf2dataType.json");
QJsonObject datatypeJson = readJson(datatypeJsonPath);
for (const QString& key : datatypeJson.keys()) {
QJsonObject typeObj = datatypeJson[key].toObject();
if (DataCache::instance()->initDatabase()) {
QMap<QString, QString> datatypeRows =
DataCache::instance()->getAllRows("uf20", Constants::Cache::TblUf2DataType);
for (auto it = datatypeRows.begin(); it != datatypeRows.end(); ++it) {
QJsonObject typeObj = QJsonDocument::fromJson(it.value().toUtf8()).object();
QMap<QString, QString> typeInfo;
for (const QString& attrKey : typeObj.keys()) {
typeInfo[attrKey] = typeObj[attrKey].toString();
}
uf2datatypeMap[key] = typeInfo;
uf2datatypeMap[it.key()] = typeInfo;
}
}
Uf2StdFields uf2StdFields;
@@ -534,14 +468,11 @@ bool MetadataProcessor::processUf2StdFields()
resultJson[key] = fieldJson;
}
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2stdfield.json");
// 解析结果直接落库 uf20.uf2_stdfield, 不再生成中间 JSON(uf2stdfield.json)
saveRowsFromJson("uf20", Constants::Cache::TblUf2StdField, resultJson, dirMtime(m_uf20Path));
if (!writeJson(outputPath, resultJson)) {
LogManager::instance()->log(QString("UF2.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
LogManager::instance()->log(QString("UF2.0 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
LogManager::instance()->log(QString("UF2.0 标准字段处理完成,耗时: %1ms,已落库: uf20.%2")
.arg(timer.elapsed()).arg(Constants::Cache::TblUf2StdField));
return true;
}
@@ -567,15 +498,121 @@ bool MetadataProcessor::processUf2ErrNumber()
resultJson[key] = errorJson;
}
QString outputPath = QDir(m_uf2touft3Path).filePath("uf2Error.json");
// 解析结果直接落库 uf20.uf2_error, 不再生成中间 JSON(uf2Error.json)
saveRowsFromJson("uf20", Constants::Cache::TblUf2Error, resultJson, dirMtime(m_uf20Path));
if (!writeJson(outputPath, resultJson)) {
LogManager::instance()->log(QString("UF2.0 标准错误号处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
LogManager::instance()->log(QString("UF2.0 标准错误号处理完成,耗时: %1ms,已落库: uf20.%2")
.arg(timer.elapsed()).arg(Constants::Cache::TblUf2Error));
return true;
}
bool MetadataProcessor::processUf2Constants()
{
LogManager::instance()->log("========== 开始处理 UF2.0 常量 ==========");
QElapsedTimer timer;
timer.start();
Uf2Constants uf2Constants;
auto resultMap = uf2Constants.loadData(m_uf20Path);
QJsonObject resultJson;
for (const QString& key : resultMap.keys()) {
const QMap<QString, QString>& info = resultMap[key];
QJsonObject entry;
for (const QString& attrKey : info.keys()) {
entry[attrKey] = info[attrKey];
}
resultJson[key] = entry;
}
LogManager::instance()->log(QString("UF2.0 标准错误号处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
// 解析结果直接落库 uf20.uf2_constants, 不再生成中间 JSON(uf2constants.json)
saveRowsFromJson("uf20", Constants::Cache::TblUf2Constants, resultJson, dirMtime(m_uf20Path));
LogManager::instance()->log(QString("UF2.0 常量处理完成,耗时: %1ms,已落库: uf20.%2")
.arg(timer.elapsed()).arg(Constants::Cache::TblUf2Constants));
return true;
}
bool MetadataProcessor::processUf2MenuFunc()
{
LogManager::instance()->log("========== 开始处理 UF2.0 菜单功能 ==========");
QElapsedTimer timer;
timer.start();
Uf2MenuAndFunction parser;
auto resultMap = parser.loadData(m_uf20Path);
QJsonObject resultJson;
for (const QString& key : resultMap.keys()) {
const QMap<QString, QString>& info = resultMap[key];
QJsonObject entry;
for (const QString& attrKey : info.keys()) {
entry[attrKey] = info[attrKey];
}
resultJson[key] = entry;
}
// 解析结果只落库 uf20.uf2_menu_func(不再生成 uf2_menu 表与中间 JSON)。
// Python 侧 schema 也只定义 uf2_menu_func 一个数据集, 两者保持对齐。
saveRowsFromJson("uf20", Constants::Cache::TblUf2MenuFunc, resultJson, dirMtime(m_uf20Path));
LogManager::instance()->log(QString("UF2.0 菜单功能处理完成,共 %1 条,耗时: %2ms,已落库: uf20.%3")
.arg(resultJson.size())
.arg(timer.elapsed())
.arg(Constants::Cache::TblUf2MenuFunc));
return true;
}
bool MetadataProcessor::processUf2Module()
{
LogManager::instance()->log("========== 开始处理 UF2.0 模块 ==========");
QElapsedTimer timer;
timer.start();
Uf2Module parser;
auto resultMap = parser.loadData(m_uf20Path);
QJsonObject resultJson;
for (const QString& key : resultMap.keys()) {
const QMap<QString, QString>& info = resultMap[key];
QJsonObject entry;
for (const QString& attrKey : info.keys()) {
entry[attrKey] = info[attrKey];
}
resultJson[key] = entry;
}
// 解析结果直接落库 uf20.uf2_module, 不再生成中间 JSON(uf2module.json)
saveRowsFromJson("uf20", Constants::Cache::TblUf2Module, resultJson, dirMtime(m_uf20Path));
LogManager::instance()->log(QString("UF2.0 模块处理完成,耗时: %1ms,已落库: uf20.%2")
.arg(timer.elapsed()).arg(Constants::Cache::TblUf2Module));
return true;
}
bool MetadataProcessor::processUf2Table()
{
LogManager::instance()->log("========== 开始处理 UF2.0 表结构 ==========");
QElapsedTimer timer;
timer.start();
Uf2Table uf2Table;
QString settPath = (m_supportUftdbSett == "0") ? QString() : m_uftDbSettPath;
auto resultMap = uf2Table.loadData(m_uf20Path, settPath);
QJsonObject resultJson;
for (auto it = resultMap.begin(); it != resultMap.end(); ++it) {
resultJson[it.key()] = it.value();
}
// 解析结果直接落库 uf20.uf2_table, 不再生成中间 JSON(uf2table.json)
// src_mtime 对齐 Python: uf2_table 源 = uf20 目录 + 数据库设置目录(相加)
saveRowsFromJson("uf20", Constants::Cache::TblUf2Table, resultJson,
dirMtime(m_uf20Path) + dirMtime(m_uftDbSettPath));
LogManager::instance()->log(QString("UF2.0 表结构处理完成,耗时: %1ms,已落库: uf20.%2")
.arg(timer.elapsed()).arg(Constants::Cache::TblUf2Table));
return true;
}
@@ -600,14 +637,11 @@ bool MetadataProcessor::processUft3DataType()
resultJson[key] = typeJson;
}
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3datatype.json");
// 解析结果直接落库 uft3.uft3_datatype, 不再生成中间 JSON(uft3datatype.json)
saveRowsFromJson("uft3", Constants::Cache::TblUft3DataType, resultJson, dirMtime(m_uft30PubPath));
if (!writeJson(outputPath, resultJson)) {
LogManager::instance()->log(QString("UFT3.0 数据类型处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
LogManager::instance()->log(QString("UFT3.0 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
LogManager::instance()->log(QString("UFT3.0 数据类型处理完成,耗时: %1ms,已落库: uft3.%2")
.arg(timer.elapsed()).arg(Constants::Cache::TblUft3DataType));
return true;
}
@@ -620,16 +654,19 @@ bool MetadataProcessor::processUft3StdFields()
QString dirPath = m_uft30PubPath;
// 数据类型依赖从缓存库 uft3.uft3_datatype 读取, 不再读中间 JSON(uft3datatype.json)
QMap<QString, QMap<QString, QString>> uf3datatypeMap;
QString datatypeJsonPath = QDir(m_uf2touft3Path).filePath("uft3datatype.json");
QJsonObject datatypeJson = readJson(datatypeJsonPath);
for (const QString& key : datatypeJson.keys()) {
QJsonObject typeObj = datatypeJson[key].toObject();
if (DataCache::instance()->initDatabase()) {
QMap<QString, QString> datatypeRows =
DataCache::instance()->getAllRows("uft3", Constants::Cache::TblUft3DataType);
for (auto it = datatypeRows.begin(); it != datatypeRows.end(); ++it) {
QJsonObject typeObj = QJsonDocument::fromJson(it.value().toUtf8()).object();
QMap<QString, QString> typeInfo;
for (const QString& attrKey : typeObj.keys()) {
typeInfo[attrKey] = typeObj[attrKey].toString();
}
uf3datatypeMap[key] = typeInfo;
uf3datatypeMap[it.key()] = typeInfo;
}
}
Uft3StdFields uft3StdFields;
@@ -645,14 +682,11 @@ bool MetadataProcessor::processUft3StdFields()
resultJson[key] = fieldJson;
}
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3stdfields.json");
// 解析结果直接落库 uft3.uft3_stdfield, 不再生成中间 JSON(uft3stdfields.json)
saveRowsFromJson("uft3", Constants::Cache::TblUft3StdField, resultJson, dirMtime(m_uft30PubPath));
if (!writeJson(outputPath, resultJson)) {
LogManager::instance()->log(QString("UFT3.0 标准字段处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
LogManager::instance()->log(QString("UFT3.0 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
LogManager::instance()->log(QString("UFT3.0 标准字段处理完成,耗时: %1ms,已落库: uft3.%2")
.arg(timer.elapsed()).arg(Constants::Cache::TblUft3StdField));
return true;
}
@@ -681,14 +715,11 @@ bool MetadataProcessor::processUft3Table()
resultJson[key] = tableJson;
}
QString outputPath = QDir(m_uf2touft3Path).filePath("uft3Table.json");
// 解析结果直接落库 uft3.uft3_table, 不再生成中间 JSON(uft3Table.json)
saveRowsFromJson("uft3", Constants::Cache::TblUft3Table, resultJson, dirMtime(m_uft30PubPath));
if (!writeJson(outputPath, resultJson)) {
LogManager::instance()->log(QString("UFT3.0 表结构处理失败,耗时: %1ms").arg(timer.elapsed()));
return false;
}
LogManager::instance()->log(QString("UFT3.0 表结构处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath));
LogManager::instance()->log(QString("UFT3.0 表结构处理完成,耗时: %1ms,已落库: uft3.%2")
.arg(timer.elapsed()).arg(Constants::Cache::TblUft3Table));
return true;
}
+15 -11
View File
@@ -23,6 +23,10 @@ public:
bool processUf2StdFields();
bool processUf2DataType();
bool processUf2ErrNumber();
bool processUf2Constants();
bool processUf2MenuFunc();
bool processUf2Module();
bool processUf2Table();
bool processUft3StdFields();
bool processUft3DataType();
@@ -37,27 +41,27 @@ public:
private:
bool isValidUtf8(const QByteArray& data);
bool writeJson(const QString& filePath, const QJsonObject& jsonObject);
QJsonObject readJson(const QString& filePath);
QJsonObject readJson(const QString& filePath); // 仅用于读取输入文件 cust.json
QJsonObject interfaceInfoToUFT3Json(const InterfaceInfo& info);
QJsonObject interfaceInfoToUF2Json(const InterfaceInfo& info);
// 轻量增量缓存(仅记录 文件路径->修改时间(mtime)+该文件在 JSON 中的 key)
QJsonObject readCache();
bool writeCache(const QJsonObject& cache);
// 把解析得到的 QJsonObject(key -> 子对象) 直接落库到双库对应表(name 列=key, data 列=子对象JSON)。
// 不再生成任何中间 JSON 文件。db 选 "uf20"/"uft3", table 为 Constants::Cache 表名。
// srcMtime 为源目录最新修改时间(多源时相加), 写入 _meta.src_mtime 供缓存失效判断。
void saveRowsFromJson(const QString& db, const QString& table, const QJsonObject& resultJson,
double srcMtime = 0.0);
// 统一处理入口:通过 IInterfaceScanner 策略多态处理,消除 UF2/UFT3 平行实现
// 统一处理入口:通过 IInterfaceScanner 策略多态处理,消除 UF2/UFT3 平行实现。
// 解析结果直接落双库(uf20.uf2_interface / uft3.uft3_interface / uft3.uft3_transcode)。
bool processInterfaces(IInterfaceScanner* scanner,
const ScanContext& baseCtx,
const QString& dirPath,
const QString& outputPath,
const QString& transOutputPath,
const QString& cacheKey,
bool isUft3);
// 生成元数据后,把 json 同步进 config_cache.db(功能查询数据源)并刷新工程路径指纹;
// isUft3 区分 UF2/UFT3 两套 cname 表
void syncJsonToCache(const QString& jsonPath, bool isUft3);
// 注: C++ 直接解析源文件并落双库对应表(name 列=key, data 列=JSON 序列化字符串),
// 落库路径 <exe>/cache/uf20_cache.db 与 <exe>/cache/uft3_cache.db(见 DataCache)。
// 各 processUf2Xxx/processUft3Xxx 解析后调用 saveRowsFromJson 落库, 不再生成中间 JSON 文件。
QString m_basePath;
QString m_uf2touft3Path;
+41
View File
@@ -0,0 +1,41 @@
#include "uf2constants.h"
#include "utils/Constants.h"
#include <QFile>
#include <QXmlStreamReader>
#include <QDir>
Uf2Constants::Uf2Constants(QObject *parent) : QObject(parent)
{
}
QMap<QString, QMap<QString, QString>> Uf2Constants::loadData(const QString& dirPath)
{
QMap<QString, QMap<QString, QString>> resultMap;
QString filePath = dirPath + Constants::MetaPaths::Uf2Constants;
QFile file(filePath);
if (!file.exists()) {
return resultMap;
}
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return resultMap;
}
QXmlStreamReader reader(&file);
while (!reader.atEnd()) {
reader.readNext();
if (reader.isStartElement() && reader.name().compare("constant", Qt::CaseInsensitive) == 0) {
QXmlStreamAttributes attrs = reader.attributes();
QString name = attrs.value("name").toString().trimmed();
QString value = attrs.value("constant_value").toString().trimmed();
if (name.isEmpty()) {
continue;
}
QMap<QString, QString> entry;
entry["cname"] = name;
entry["value"] = value;
resultMap[name] = entry; // key = constant_name
}
}
file.close();
return resultMap;
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef UF2CONSTANTS_H
#define UF2CONSTANTS_H
#include <QObject>
#include <QString>
#include <QMap>
// 解析 UF2.0 常量文件: 公共资源/constants.xml
// key = constant_name, data = {cname, value}
class Uf2Constants : public QObject
{
Q_OBJECT
public:
explicit Uf2Constants(QObject *parent = nullptr);
QMap<QString, QMap<QString, QString>> loadData(const QString& dirPath);
private:
};
#endif
+119
View File
@@ -0,0 +1,119 @@
#include "uf2menuandfunction.h"
#include "utils/Constants.h"
#include <QFile>
#include <QXmlStreamReader>
#include <QDir>
Uf2MenuAndFunction::Uf2MenuAndFunction(QObject *parent) : QObject(parent)
{
}
QMap<QString, QMap<QString, QString>> Uf2MenuAndFunction::loadData(const QString& dirPath)
{
QMap<QString, QMap<QString, QString>> resultMap;
QString filePath = dirPath + Constants::MetaPaths::Uf2MenuFunc;
QFile file(filePath);
if (!file.exists()) {
return resultMap;
}
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return resultMap;
}
// 解析要点(对齐 Python uf2_menu_and_function.py):
// 1) 不可使用 QXmlStreamReader::readElementText()。hsmenuitem 内含
// extendsFields/entry/string 等复合嵌套元素, readElementText() 只适用于叶子节点,
// 遇到复合元素会提前停止并吞掉 EndElement 令牌, 导致解析出 0 条。
// 2) Python 的 xml_util.by_tag() 基于 elem.iter(), 是"递归查找所有后代",
// 因此 childMenus/hsmenuitem 这类"嵌套菜单"也必须采集。
// 故此处遇到 hsmenuitem/capabilityitem 时, 即使在某个 item 内部也要
// 开启新的 item(用栈保存父 item 状态), 否则会漏掉嵌套菜单导致数据不全。
struct ItemState {
QString item; // "hsmenuitem" / "capabilityitem"
QMap<QString, QString> entry;// 直接子元素名 -> 文本
int depth = 0; // item 内部相对深度, item 自身为 0
QString tag; // 当前直接子元素名
QString text; // 当前直接子元素文本缓冲
};
ItemState cur;
bool inItem = false;
QList<ItemState> stack; // 父 item 状态栈(支持嵌套)
auto isItemTag = [](const QString& t) {
return t.compare("hsmenuitem", Qt::CaseInsensitive) == 0 ||
t.compare("capabilityitem", Qt::CaseInsensitive) == 0;
};
auto flush = [&]() {
if (!inItem) return;
QString macroName = cur.item.compare("hsmenuitem", Qt::CaseInsensitive) == 0
? cur.entry.value("menuMacroName")
: cur.entry.value("marcoDefine");
if (!macroName.isEmpty()) {
QMap<QString, QString> entry;
if (cur.item.compare("hsmenuitem", Qt::CaseInsensitive) == 0) {
entry["cName"] = cur.entry.value("menuCaption");
entry["menuCode"] = cur.entry.value("menuCode");
} else { // capabilityitem
entry["cName"] = cur.entry.value("capabilityName");
entry["menuCode"] = cur.entry.value("nowCapabilityCode");
}
entry["name"] = macroName; // 关键: 以宏名为 key
resultMap[macroName] = entry;
}
// item 结束: 恢复父 item 状态(若存在)
if (!stack.isEmpty()) {
cur = stack.takeLast();
} else {
inItem = false;
cur = ItemState();
}
};
QXmlStreamReader reader(&file);
while (!reader.atEnd()) {
QXmlStreamReader::TokenType token = reader.readNext();
if (token == QXmlStreamReader::StartElement) {
QString tag = reader.name().toString();
if (isItemTag(tag)) {
// 开启新 item: 若当前已在 item 内(嵌套菜单), 先压栈保存父状态
if (inItem) {
stack.append(cur);
}
inItem = true;
cur = ItemState();
cur.item = tag;
} else if (inItem) {
++cur.depth;
if (cur.depth == 1) {
cur.tag = tag;
cur.text.clear();
}
}
} else if (token == QXmlStreamReader::Characters) {
if (inItem && cur.depth == 1) {
cur.text += reader.text();
}
} else if (token == QXmlStreamReader::EndElement) {
if (!inItem) continue;
if (cur.depth == 1) {
cur.entry[cur.tag] = cur.text.trimmed();
cur.tag.clear();
cur.text.clear();
--cur.depth;
} else if (cur.depth > 1) {
--cur.depth;
} else {
// depth == 0 -> 当前 item 自身结束
flush();
}
}
}
// 正常 XML 已由 EndElement 收尾; 此处兜底处理未闭合的残留
while (inItem) {
flush();
}
file.close();
return resultMap;
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef UF2MENUANDFUNCTION_H
#define UF2MENUANDFUNCTION_H
#include <QObject>
#include <QString>
#include <QMap>
// 解析 UF2.0 菜单与功能: 公共资源/menuandfunction.xml
// 含 <hsmenuitem> 与 <capabilityitem>, key = macro_name, data = {cName, name, menuCode}
// 只落库 uf20.uf2_menu_func 一张表(不再生成 uf2_menu), 对齐 Python 侧 schema 定义。
// 解析须递归采集所有层级的 item(含 childMenus 下的嵌套 hsmenuitem), 否则数据不全。
class Uf2MenuAndFunction : public QObject
{
Q_OBJECT
public:
explicit Uf2MenuAndFunction(QObject *parent = nullptr);
QMap<QString, QMap<QString, QString>> loadData(const QString& dirPath);
private:
};
#endif
+59
View File
@@ -0,0 +1,59 @@
#include "uf2module.h"
#include "utils/logmanager.h"
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QXmlStreamReader>
Uf2Module::Uf2Module(QObject *parent) : QObject(parent)
{
}
void Uf2Module::scanDir(const QString& dirPath)
{
QDir dir(dirPath);
if (!dir.exists()) {
return;
}
QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
for (const QFileInfo& info : entries) {
if (info.isDir()) {
scanDir(info.filePath());
} else if (info.fileName().compare("module.xml", Qt::CaseInsensitive) == 0) {
// 与 Python 端对齐: 跳过 "数据库" 子目录下的 module.xml
if (info.path().replace('\\', '/').contains("/数据库/", Qt::CaseInsensitive)) {
continue;
}
QFile file(info.filePath());
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
continue;
}
QXmlStreamReader reader(&file);
while (!reader.atEnd()) {
reader.readNext();
if (reader.isStartElement() && reader.name().compare("info", Qt::CaseInsensitive) == 0) {
QXmlStreamAttributes attrs = reader.attributes();
// cname 取自 module.xml 所在目录名 (与 Python uf2_module.py 一致),
// 而非 <info> 的 cname 属性 (该属性通常为空)。
QString cname = QDir(info.path()).dirName();
QString ename = attrs.value("ename").toString().trimmed();
if (ename.isEmpty()) {
continue;
}
QMap<QString, QString> entry;
entry["cname"] = cname;
entry["path"] = info.path(); // 所在目录
m_resultMap[ename] = entry; // key = ename
}
}
file.close();
}
}
}
QMap<QString, QMap<QString, QString>> Uf2Module::loadData(const QString& dirPath)
{
m_resultMap.clear();
scanDir(dirPath);
return m_resultMap;
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef UF2MODULE_H
#define UF2MODULE_H
#include <QObject>
#include <QString>
#include <QMap>
// 解析 UF2.0 模块: 工程各目录下 module.xml
// key = ename, data = {cname, path}
class Uf2Module : public QObject
{
Q_OBJECT
public:
explicit Uf2Module(QObject *parent = nullptr);
QMap<QString, QMap<QString, QString>> loadData(const QString& dirPath);
private:
void scanDir(const QString& dirPath);
QMap<QString, QMap<QString, QString>> m_resultMap;
};
#endif
+128
View File
@@ -0,0 +1,128 @@
#include "uf2table.h"
#include "utils/logmanager.h"
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QXmlStreamReader>
#include <QJsonArray>
#include <QJsonObject>
Uf2Table::Uf2Table(QObject *parent) : QObject(parent)
{
}
QString schemaToUser(const QString& schema)
{
static const QMap<QString, QString> map = {
{"HS_USER_DATA", "hs_user"},
{"HS_ASSET_DATA", "hs_asset"},
{"HS_FUND_DATA", "hs_fund"},
{"HS_SECU_DATA", "hs_secu"},
{"HS_AUTH_DATA", "hs_auth"},
{"HS_CRDT_DATA", "hs_crdt"},
{"HS_OFUND_DATA", "hs_ofund"},
{"HS_BOND_DATA", "hs_bond"},
{"HS_FUTU_DATA", "hs_futu"},
{"HS_DATA_DATA", "hs_data"},
{"HS_HIS_DATA", "hs_his"},
{"HS_CBS_DATA", "hs_cbs"},
{"HS_CASH_DATA", "hs_cash"},
{"HS_SETTPLUS_DATA", "hs_settplus"},
{"HS_SETTARG_DATA", "hs_settarg"},
};
return map.value(schema, QString());
}
void Uf2Table::scanDir(const QString& dirPath)
{
// dirPath 已是“数据库”目录(或其子目录), 直接遍历, 不再重复拼 “/数据库”
QDir dbDir(dirPath);
if (!dbDir.exists()) {
return;
}
QFileInfoList entries = dbDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
for (const QFileInfo& info : entries) {
if (info.isDir()) {
scanDir(info.filePath());
} else if (info.fileName().endsWith(".table_design", Qt::CaseInsensitive) &&
info.fileName().compare("module.xml", Qt::CaseInsensitive) != 0) {
QString tableCName = info.baseName(); // 文件名去掉扩展名
QJsonObject obj = loadTable(info.filePath(), tableCName);
QString tableName = obj.value("name").toString();
if (!tableName.isEmpty()) {
m_resultMap[tableName] = obj;
}
}
}
}
QJsonObject Uf2Table::loadTable(const QString& filePath, const QString& tableCName)
{
QJsonObject result;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return result;
}
QString tableName;
QString user;
QJsonArray filedList;
QJsonObject indexList;
QXmlStreamReader reader(&file);
while (!reader.atEnd()) {
reader.readNext();
if (!reader.isStartElement()) {
continue;
}
QString tag = reader.name().toString();
if (tag.compare("tablePrimaryInfo", Qt::CaseInsensitive) == 0) {
QXmlStreamAttributes attrs = reader.attributes();
tableName = attrs.value("tableName").toString().trimmed();
user = schemaToUser(attrs.value("schema").toString().trimmed());
} else if (tag.compare("tablefield", Qt::CaseInsensitive) == 0) {
QXmlStreamAttributes attrs = reader.attributes();
QString flag = attrs.value("flag").toString();
if (!flag.contains("H", Qt::CaseInsensitive)) {
filedList.append(attrs.value("name").toString().trimmed());
}
} else if (tag.compare("tableIndex", Qt::CaseInsensitive) == 0) {
QXmlStreamAttributes attrs = reader.attributes();
QString flag = attrs.value("flag").toString();
if (!flag.contains("H", Qt::CaseInsensitive)) {
QJsonObject idx;
QString idxName = attrs.value("name").toString().trimmed();
QStringList cols = attrs.value("columnNames").toString().split(",", Qt::SkipEmptyParts);
QJsonArray colArr;
for (const QString& c : cols) colArr.append(c.trimmed());
idx["name"] = idxName;
idx["filedList"] = colArr;
idx["unique"] = (attrs.value("unique").toString().compare("true", Qt::CaseInsensitive) == 0);
idx["flag"] = flag;
indexList[idxName] = idx;
}
}
}
file.close();
result["name"] = tableName;
result["cname"] = tableCName;
result["filedList"] = filedList;
result["indexList"] = indexList;
result["user"] = user;
return result;
}
QMap<QString, QJsonObject> Uf2Table::loadData(const QString& dirPath, const QString& settDirPath)
{
m_resultMap.clear();
// 对齐 Python: 扫描 dirPath/数据库 与 settDirPath(整个工程, 仅取路径含 /数据库/ 的 .table_design)
QDir dbDir(dirPath + "/数据库");
if (dbDir.exists()) {
scanDir(dbDir.path());
}
if (!settDirPath.isEmpty() && QDir::isAbsolutePath(settDirPath)) {
scanDir(settDirPath);
}
return m_resultMap;
}
+25
View File
@@ -0,0 +1,25 @@
#ifndef UF2TABLE_H
#define UF2TABLE_H
#include <QObject>
#include <QString>
#include <QMap>
#include <QJsonObject>
// 解析 UF2.0 表结构: 各工程目录下 数据库/*.table_design
// key = tableName, data = {name, cname, filedList, indexList, user}
class Uf2Table : public QObject
{
Q_OBJECT
public:
explicit Uf2Table(QObject *parent = nullptr);
QMap<QString, QJsonObject> loadData(const QString& dirPath, const QString& settDirPath = QString());
private:
void scanDir(const QString& dirPath);
QJsonObject loadTable(const QString& filePath, const QString& tableCName);
QMap<QString, QJsonObject> m_resultMap;
};
#endif
+35 -3
View File
@@ -1,6 +1,7 @@
#include "metadatupdatepage.h"
#include "utils/Constants.h"
#include "utils/logmanager.h"
#include "utils/datacache.h"
#include "utils/business_strategy.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -185,11 +186,12 @@ void MetadataUpdatePage::showEvent(QShowEvent* event)
void MetadataUpdatePage::showUpdateItems()
{
QStringList uft3Items = {"标准字段", "数据类型", "表结构",
// 注意: "数据类型" 需排在 "标准字段" 之前, 标准字段解析依赖数据类型(从缓存库读取)。
QStringList uft3Items = {"数据类型", "标准字段", "表结构",
"接口数据"};
QStringList uf20Items = {"标准字段", "数据类型", "标准错误号",
"接口数据"};
QStringList uf20Items = {"数据类型", "标准字段", "标准错误号",
"接口数据", "常量", "菜单功能", "模块", "表结构"};
for (int i = 0; i < uft3Items.size(); ++i) {
auto* cbx = new ElaCheckBox(uft3Items[i]);
@@ -313,6 +315,14 @@ UpdateResult MetadataUpdatePage::runUpdateUf2(const QStringList& items)
success = false;
}
interfaceUpdated = true;
} else if (item == "常量") {
processUf2Constants();
} else if (item == "菜单功能") {
processUf2MenuFunc();
} else if (item == "模块") {
processUf2Module();
} else if (item == "表结构") {
processUf2Table();
}
++idx;
}
@@ -370,6 +380,26 @@ bool MetadataUpdatePage::processUf2Interface(const QString& basePath)
return _processor->processUf2Interfaces(basePath);
}
void MetadataUpdatePage::processUf2Constants()
{
_processor->processUf2Constants();
}
void MetadataUpdatePage::processUf2MenuFunc()
{
_processor->processUf2MenuFunc();
}
void MetadataUpdatePage::processUf2Module()
{
_processor->processUf2Module();
}
void MetadataUpdatePage::processUf2Table()
{
_processor->processUf2Table();
}
void MetadataUpdatePage::processUft3StdFields()
{
_processor->processUft3StdFields();
@@ -432,6 +462,8 @@ void MetadataUpdatePage::onWorkerFinished()
_processLabel->setText("正在更新数据缓存...");
// 通过业务策略工厂按当前任务选择对应版本的配置重载,取代 UF20/UFT3 if/else
createBusinessStrategy(m_currentTaskName)->reloadConfig();
// 元数据落库后重建功能查询内存缓存, 使后续 UF20/UFT3 功能搜索走缓存而非全表扫描
DataCache::instance()->refreshFunctionCache();
}
onUpdateFinished(r.success, QString("%1更新完成").arg(m_currentTaskName));
@@ -61,6 +61,10 @@ private:
void processUf2StdFields();
void processUf2DataType();
void processUf2ErrNumber();
void processUf2Constants();
void processUf2MenuFunc();
void processUf2Module();
void processUf2Table();
bool processUf2Interface(const QString& basePath);
void processUft3StdFields();
+39 -6
View File
@@ -35,6 +35,7 @@ namespace Paths {
inline const QString ToolDir = "uf2touft3"; // 工具主目录
inline const QString LogDir = "logs"; // 日志目录
inline const QString CacheDir = "cache"; // 缓存目录
inline const QString ConfigDir = "config"; // 配置文件目录(toolDir 下)
inline const QString ChangeCodeDir = "change_code"; // 代码检索根目录(toolDir 下)
inline const QString FunctionOutput = "function_output"; // 调用链/文件导出目录
}
@@ -44,18 +45,47 @@ namespace Paths {
// ---------------------------------------------------------------------------
namespace ToolFiles {
inline const QString Exe = "uf2touft3.exe";
inline const QString ConfigIni = "config.ini";
inline const QString ConfigIni = "config/config.ini";
inline const QString Uf2Json = "uf2.json";
inline const QString Uft3Json = "uft3.json";
inline const QString Uft3Trans = "uft3Trans.json";
inline const QString CustJson = "cust.json";
inline const QString CustJson = "config/cust.json";
inline const QString Cust2Json = "config/cust2.json";
}
// ---------------------------------------------------------------------------
// Cache 相关
// 缓存库采用双库 + 键值表结构(对齐 Python src/store/schema.py),
// C++ 直接解析业务工程源文件并写库(方向 A), 表结构统一为:
// (name TEXT PRIMARY KEY, data TEXT), data 为 JSON 序列化字符串。
// 另含 _meta 表(dataset TEXT PRIMARY KEY, row_count INTEGER, src_mtime REAL)。
// ---------------------------------------------------------------------------
namespace Cache {
inline const QString DbFile = "config_cache.db";
// 双库文件(置于 cacheDir() 下)
inline const QString Uf20DbFile = "uf20_cache.db";
inline const QString Uft3DbFile = "uft3_cache.db";
// uf20 库表名
inline const QString TblUf2Interface = "uf2_interface";
inline const QString TblUf2Table = "uf2_table";
inline const QString TblUf2Module = "uf2_module";
inline const QString TblUf2StdField = "uf2_stdfield";
inline const QString TblUf2DataType = "uf2_datatype";
inline const QString TblUf2Error = "uf2_error";
inline const QString TblUf2Constants = "uf2_constants";
inline const QString TblUf2Menu = "uf2_menu";
inline const QString TblUf2MenuFunc = "uf2_menu_func";
// uft3 库表名 (func_bak 由 Python 生成, 但本工具不读取)
inline const QString TblUft3Interface = "uft3_interface";
inline const QString TblUft3TransCode = "uft3_trans_code";
inline const QString TblUft3Table = "uft3_table";
inline const QString TblUft3StdField = "uft3_stdfield";
inline const QString TblUft3DataType = "uft3_datatype";
// 功能查询用的接口表(本工具只用这两张表)
inline const QString Uf20InterfaceTable = TblUf2Interface;
inline const QString Uft3InterfaceTable = TblUft3Interface;
}
// ---------------------------------------------------------------------------
@@ -70,6 +100,8 @@ namespace MetaPaths {
inline const QString Uf2StdField = "/公共资源/stdfields.xml"; // UF2 标准字段文件
inline const QString Uf2DataType = "/公共资源/datatypes.xml"; // UF2 数据类型文件
inline const QString Uf2ErrNumber = "/公共资源/errNumbers.xml"; // UF2 错误号文件
inline const QString Uf2Constants = "/公共资源/constants.xml"; // UF2 常量文件
inline const QString Uf2MenuFunc = "/公共资源/menuandfunction.xml"; // UF2 菜单与功能文件
// 文件扩展名
inline const QString Uft3StructExt = ".uftstructure"; // UFT3 表结构文件扩展名
}
@@ -82,8 +114,9 @@ inline QString toolExe() { return toolDir() + "/" + ToolFiles::Exe; }
inline QString configIni() { return toolDir() + "/" + ToolFiles::ConfigIni; }
inline QString custJson() { return toolDir() + "/" + ToolFiles::CustJson; }
inline QString logDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::LogDir; }
inline QString cacheDir() { return QCoreApplication::applicationDirPath() + "/" + Paths::CacheDir; }
inline QString cacheDb() { return cacheDir() + "/" + Cache::DbFile; }
inline QString cacheDir() { return toolDir() + "/" + Paths::CacheDir; } // bin/uf2touft3/cache
inline QString uf20CacheDb() { return cacheDir() + "/" + Cache::Uf20DbFile; }
inline QString uft3CacheDb() { return cacheDir() + "/" + Cache::Uft3DbFile; }
// ---------------------------------------------------------------------------
// 代码检索(CodeSearch)相关常量与路径
@@ -95,7 +128,7 @@ namespace CodeSearch {
}
inline QString changeCodeDir() { return toolDir() + "/" + Paths::ChangeCodeDir; } // 默认代码检索根目录
inline QString changeCodeIni() { return toolDir() + "/" + QString::fromLatin1(CodeSearch::ChangeCodeIni); }
inline QString changeCodeIni() { return toolDir() + "/" + Paths::ConfigDir + "/" + QString::fromLatin1(CodeSearch::ChangeCodeIni); }
inline QString functionOutput() { return Paths::FunctionOutput; } // 调用链/文件导出目录名
} // namespace Constants
+2 -2
View File
@@ -1,4 +1,5 @@
#include "configmanager.h"
#include "utils/Constants.h"
#include "logmanager.h"
#include <QFile>
#include <QDir>
@@ -31,8 +32,7 @@ bool ConfigManager::isValidUtf8(const QByteArray& data)
bool ConfigManager::loadConfig(const QString& basePath)
{
m_basePath = basePath;
QString uf2touft3Path = QDir(basePath).filePath("uf2touft3");
m_configFilePath = QDir(uf2touft3Path).filePath("config.ini");
m_configFilePath = Constants::configIni();
LogManager::instance()->log(QString("========== 加载配置文件 =========="));
LogManager::instance()->log(QString("配置文件路径: %1").arg(m_configFilePath));
+11 -105
View File
@@ -6,19 +6,6 @@
#include <QFile>
#include <QJsonDocument>
// 计算当前工程路径指纹(含盘符/路径变化即视为不同工程),用于识别
// “换机器/换路径后旧缓存 db 仍非空”导致功能查询读到旧数据的情况
static QString currentProjectPathFingerprint()
{
QStringList parts;
const QStringList keys = {"uf20", "ufAcct20", "UftdbSett", "uft30pub", "uft30cbptrans", "uft30"};
ConfigManager* cfg = ConfigManager::instance();
for (const QString& k : keys) {
parts.append(PathUtils::normPath(cfg->getValue("projectPath", k)));
}
return parts.join("|");
}
bool ConfigReaderBase::doLoadAllConfig(const ConfigReaderTraits& traits)
{
m_failedFiles.clear();
@@ -30,119 +17,38 @@ bool ConfigReaderBase::doLoadAllConfig(const ConfigReaderTraits& traits)
return false;
}
QString filePath = Constants::toolDir() + "/" + traits.mainFile;
// 元数据解析结果直接落双库(cache/uf20_cache.db / cache/uft3_cache.db),
// 不再生成中间 JSON(uf2.json/uft3.json)。因此只需校验接口表非空即可加载。
if (traits.hasConfig(traits.mainFile)) {
// 缓存表非空:检测工程路径是否与上次写入时一致,
// 不一致(如 A 电脑的缓存 db 打包给 B 电脑)则强制重新从 json 同步,
// 避免功能查询读到旧工程的 cname 数据
QMap<QString, QString> meta = DataCache::instance()->loadProjectPaths();
const QString stored = meta.value("fingerprint");
const QString current = currentProjectPathFingerprint();
if (stored != current) {
LogManager::instance()->log(QString("%1配置加载 - 工程路径已变化(旧:%2 新:%3),强制重新同步缓存")
.arg(traits.logTag).arg(stored).arg(current));
} else {
m_loaded = true;
DataCache::instance()->setDataLoaded(true);
return true;
}
}
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
// 接口表为空: 缓存库尚无数据, 需先执行元数据更新。
LogManager::instance()->logError(
QString("%1配置加载失败 - 无法打开文件: %2").arg(traits.logTag).arg(filePath));
QString("%1配置加载失败 - 接口表为空, 请先执行元数据更新").arg(traits.logTag));
m_failedFiles.append(traits.mainFile);
return false;
}
QByteArray data = file.readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
file.close();
if (doc.isNull() || !doc.isObject()) {
LogManager::instance()->logError(
QString("%1配置加载失败 - JSON解析失败: %2").arg(traits.logTag).arg(filePath));
m_failedFiles.append(traits.mainFile);
return false;
}
QJsonObject config = doc.object();
if (!traits.saveConfig(config)) {
LogManager::instance()->logError(
QString("%1配置加载失败 - 保存失败").arg(traits.logTag));
m_failedFiles.append(traits.mainFile);
return false;
}
// 记录当前工程路径指纹,供下次启动比对(详见 doLoadAllConfig 开头)
DataCache::instance()->saveProjectPaths({{"fingerprint", currentProjectPathFingerprint()}});
m_loaded = true;
DataCache::instance()->setDataLoaded(true);
return true;
}
bool ConfigReaderBase::doReloadConfig(const ConfigReaderTraits& traits)
{
QString binPath = Constants::toolDir();
QJsonObject mergedConfig;
for (const auto& entry : traits.reloadFiles) {
const QString& fileName = entry.first;
const bool required = entry.second;
QString filePath = binPath + "/" + fileName;
LogManager::instance()->logDebug(
QString("%1配置重新加载 - 正在处理文件: %2 (required=%3)")
.arg(traits.logTag).arg(filePath).arg(required));
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString msg = QString("%1配置重新加载%2 - 无法打开文件: %3")
.arg(traits.logTag)
.arg(required ? QStringLiteral("失败") : QString())
.arg(filePath);
if (required) {
LogManager::instance()->logError(msg);
return false;
}
LogManager::instance()->logWarning(msg);
continue;
}
QByteArray data = file.readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
file.close();
if (doc.isNull() || !doc.isObject()) {
if (required) {
// 元数据更新完成后数据已直接落双库, 重新加载仅需校验接口表非空并刷新工程路径指纹。
if (!DataCache::instance()->initDatabase()) {
LogManager::instance()->logError(
QString("%1配置重新加载失败 - JSON解析失败: %2").arg(traits.logTag).arg(filePath));
QString("%1配置重新加载失败 - 数据库初始化失败").arg(traits.logTag));
return false;
}
continue;
}
QJsonObject obj = doc.object();
for (auto it = obj.begin(); it != obj.end(); ++it) {
mergedConfig[it.key()] = it.value();
}
}
if (mergedConfig.isEmpty()) {
if (!traits.hasConfig(traits.mainFile)) {
LogManager::instance()->logError(
QString("%1配置重新加载失败 - 配置为空").arg(traits.logTag));
return false;
}
if (!traits.saveConfig(mergedConfig)) {
LogManager::instance()->logError(
QString("%1配置重新加载失败 - 保存失败").arg(traits.logTag));
QString("%1配置重新加载失败 - 接口表为空, 请先执行元数据更新").arg(traits.logTag));
return false;
}
LogManager::instance()->log(
QString("%1配置重新加载成功 - 数据源: cache/uf20_cache.db / cache/uft3_cache.db").arg(traits.logTag));
return true;
}
+322 -366
View File
@@ -4,8 +4,11 @@
#include <QSqlQuery>
#include <QSqlError>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QDateTime>
#include <QElapsedTimer>
@@ -17,9 +20,11 @@ DataCache::DataCache(QObject *parent) : QObject(parent), m_dataLoaded(false)
DataCache::~DataCache()
{
if (m_db.isOpen()) {
m_db.close();
}
QMutexLocker locker(&m_connMutex);
for (auto& c : m_uf20Db) { if (c.isOpen()) c.close(); }
for (auto& c : m_uft3Db) { if (c.isOpen()) c.close(); }
m_uf20Db.clear();
m_uft3Db.clear();
}
DataCache* DataCache::instance()
@@ -30,135 +35,69 @@ DataCache* DataCache::instance()
return m_instance;
}
QString DataCache::getDatabasePath()
// 打开指定库连接(只读/读写均走此函数)。库文件不存在时仅创建 cache/ 目录并打开 SQLite,
// 由 SQLite 自动生成空 DB 文件; _meta 表在 saveDataset 落库时按需创建, 此处不主动建表。
QSqlDatabase DataCache::database(const QString& db) const
{
QString dir = Constants::cacheDir();
QDir dbDir(dir);
if (!dbDir.exists()) {
if (!dbDir.mkpath(dir)) {
LogManager::instance()->logError(QString("数据缓存 - 创建缓存目录失败: %1").arg(dir));
const bool isUft3 = (db == "uft3");
QHash<QThread*, QSqlDatabase>& conns = isUft3 ? m_uft3Db : m_uf20Db;
// QSqlDatabase 连接只能在创建它的线程中使用。元数据更新在 QtConcurrent 后台线程写库,
// 功能查询在主线程读库, 因此按线程各自维护一条连接, 避免跨线程使用导致写入静默失败。
QThread* self = QThread::currentThread();
{
// QHash 非线程安全, 主线程查询与后台线程落库会并发访问, 需加锁保护。
// QSqlDatabase 为值语义, 取出副本后即可解锁, 后续 SQL 不持锁。
QMutexLocker locker(&m_connMutex);
auto it = conns.constFind(self);
if (it != conns.constEnd() && it.value().isOpen()) {
return it.value();
}
}
return dbDir.absoluteFilePath(Constants::Cache::DbFile);
}
bool DataCache::createTables()
{
QSqlQuery query(m_db);
query.exec("PRAGMA synchronous = OFF");
query.exec("PRAGMA journal_mode = MEMORY");
QString createUF20Table = R"(
CREATE TABLE IF NOT EXISTS uf20_cname_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cname TEXT UNIQUE NOT NULL,
e_name TEXT,
function_no TEXT,
update_time TEXT NOT NULL
)
)";
QString createUFT3Table = R"(
CREATE TABLE IF NOT EXISTS uft3_cname_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cname TEXT UNIQUE NOT NULL,
e_name TEXT,
function_no TEXT,
update_time TEXT NOT NULL
)
)";
if (!query.exec(createUF20Table)) {
LogManager::instance()->logError(QString("数据缓存 - 创建uf20_cname_table失败: %1").arg(query.lastError().text()));
return false;
}
if (!query.exec(createUFT3Table)) {
LogManager::instance()->logError(QString("数据缓存 - 创建uft3_cname_table失败: %1").arg(query.lastError().text()));
return false;
QString path = isUft3 ? Constants::uft3CacheDb() : Constants::uf20CacheDb();
QFileInfo fi(path);
if (!fi.dir().exists() && !QDir().mkpath(fi.absolutePath())) {
LogManager::instance()->logError(QString("数据缓存 - 创建缓存目录失败: %1").arg(fi.absolutePath()));
return QSqlDatabase();
}
// 工程路径指纹表:记录上次写入 cname 表时使用的工程路径,
// 用于在换机器/换路径场景下识别旧缓存
QString createMetaTable = R"(
CREATE TABLE IF NOT EXISTS cache_meta (
key TEXT PRIMARY KEY,
value TEXT
)
)";
if (!query.exec(createMetaTable)) {
LogManager::instance()->logError(QString("数据缓存 - 创建cache_meta失败: %1").arg(query.lastError().text()));
return false;
// 连接名需全局唯一: 库名 + 线程地址
QString connName = QString("%1_cache_connection_%2")
.arg(isUft3 ? "uft3" : "uf20")
.arg(reinterpret_cast<quintptr>(self), 0, 16);
QSqlDatabase conn = QSqlDatabase::addDatabase("QSQLITE", connName);
conn.setDatabaseName(path);
if (!conn.open()) {
LogManager::instance()->logError(QString("数据缓存 - 打开 %1 缓存库失败: %2")
.arg(db, conn.lastError().text()));
return QSqlDatabase();
}
LogManager::instance()->log("数据缓存 - 表创建成功");
return true;
}
bool DataCache::saveProjectPaths(const QMap<QString, QString>& paths)
{
if (!m_db.isOpen()) {
return false;
{
QMutexLocker locker(&m_connMutex);
conns.insert(self, conn);
}
QSqlQuery query(m_db);
query.exec("DELETE FROM cache_meta");
for (auto it = paths.begin(); it != paths.end(); ++it) {
query.prepare("INSERT OR REPLACE INTO cache_meta (key, value) VALUES (?, ?)");
query.addBindValue(it.key());
query.addBindValue(it.value());
if (!query.exec()) {
LogManager::instance()->logError(QString("数据缓存 - 写入cache_meta失败: %1").arg(query.lastError().text()));
return false;
}
}
return true;
}
QMap<QString, QString> DataCache::loadProjectPaths()
{
QMap<QString, QString> result;
if (!m_db.isOpen()) {
return result;
}
QSqlQuery query(m_db);
if (query.exec("SELECT key, value FROM cache_meta")) {
while (query.next()) {
result[query.value(0).toString()] = query.value(1).toString();
}
}
return result;
LogManager::instance()->logInfo(QString("数据缓存 - 已打开 %1 缓存库(线程 0x%2): %3")
.arg(db).arg(reinterpret_cast<quintptr>(self), 0, 16).arg(path));
return conn;
}
bool DataCache::initDatabase()
{
// 已打开则直接返回,避免重复打印“数据库路径”(该函数启动期会被多次调用)
if (QSqlDatabase::contains("qt_sql_default_connection")) {
m_db = QSqlDatabase::database("qt_sql_default_connection");
if (m_db.isOpen()) {
// 纯只读探测: 仅当缓存库文件已存在时才打开, 不创建空库文件、不建 _meta 表。
// 这样首次启动(尚无缓存库)时返回 false, 提示用户先执行元数据更新, 避免生成空的 uf20_cache.db。
// 已打开则直接返回 true(启动期会被多次调用)。
bool ok20 = QFile::exists(Constants::uf20CacheDb());
bool ok3 = QFile::exists(Constants::uft3CacheDb());
if (ok20 || ok3) {
// 仅打开已存在的库文件, 避免为尚未生成的库创建空 DB 文件
if (ok20) database("uf20");
if (ok3) database("uft3");
LogManager::instance()->log(QString("数据缓存 - 已加载缓存库: uf20=%1, uft3=%2")
.arg(ok20 ? "是" : "否").arg(ok3 ? "是" : "否"));
return true;
}
}
QString dbPath = getDatabasePath();
LogManager::instance()->log(QString("数据缓存 - 数据库路径: %1").arg(dbPath));
m_db = QSqlDatabase::addDatabase("QSQLITE");
m_db.setDatabaseName(dbPath);
if (!m_db.open()) {
LogManager::instance()->logError(QString("数据缓存 - 打开数据库失败: %1").arg(m_db.lastError().text()));
QString drivers = QSqlDatabase::drivers().join(", ");
LogManager::instance()->logError(QString("数据缓存 - 可用的数据库驱动: %1").arg(drivers));
LogManager::instance()->logWarning("数据缓存 - 缓存库不存在(uf20_cache.db / uft3_cache.db), 请先执行元数据更新");
return false;
}
QSqlQuery query(m_db);
query.exec("PRAGMA synchronous = OFF");
query.exec("PRAGMA journal_mode = MEMORY");
LogManager::instance()->log("数据缓存 - 数据库打开成功");
return createTables();
}
bool DataCache::isDataLoaded() const
@@ -171,340 +110,357 @@ void DataCache::setDataLoaded(bool loaded)
m_dataLoaded = loaded;
}
bool DataCache::saveUF20Config(const QJsonObject& data)
// ---- 通用只读 API ----
bool DataCache::datasetExists(const QString& db, const QString& table) const
{
if (!m_db.isOpen()) {
QSqlDatabase conn = database(db);
if (!conn.isOpen()) {
return false;
}
QSqlQuery query(conn);
query.prepare(QString("SELECT COUNT(*) FROM %1").arg(table));
if (query.exec() && query.next()) {
return query.value(0).toInt() > 0;
}
return false;
}
QMap<QString, QString> DataCache::getAllRows(const QString& db, const QString& table) const
{
QMap<QString, QString> result;
QSqlDatabase conn = database(db);
if (!conn.isOpen()) {
return result;
}
QSqlQuery query(conn);
query.prepare(QString("SELECT name, data FROM %1").arg(table));
if (query.exec()) {
while (query.next()) {
result[query.value(0).toString()] = query.value(1).toString();
}
}
return result;
}
QString DataCache::getRowData(const QString& db, const QString& table, const QString& key) const
{
QSqlDatabase conn = database(db);
if (!conn.isOpen()) {
return QString();
}
QSqlQuery query(conn);
query.prepare(QString("SELECT data FROM %1 WHERE name = :k").arg(table));
query.bindValue(":k", key);
if (query.exec() && query.next()) {
return query.value(0).toString();
}
return QString();
}
int DataCache::rowCount(const QString& db, const QString& table) const
{
QSqlDatabase conn = database(db);
if (!conn.isOpen()) {
return 0;
}
QSqlQuery query(conn);
query.prepare(QString("SELECT COUNT(*) FROM %1").arg(table));
if (query.exec() && query.next()) {
return query.value(0).toInt();
}
return 0;
}
// ---- 通用写入: 原子落库到双库对应表(对齐 Python store.save) ----
// 步骤: 建临时表 t_<table>_new -> 写入 -> DROP 旧表 -> RENAME 临时表 -> 更新 _meta
bool DataCache::saveDataset(const QString& db, const QString& table,
const QMap<QString, QString>& rows, double srcMtime)
{
QSqlDatabase conn = database(db);
if (!conn.isOpen()) {
LogManager::instance()->logError(QString("数据缓存 - 写入 %1.%2 失败: 库未打开").arg(db, table));
return false;
}
QElapsedTimer timer;
timer.start();
QString tmpTable = "t_" + table + "_new";
QSqlQuery query(m_db);
query.exec("DELETE FROM uf20_cname_table");
QStringList cnames;
QStringList eNames;
QStringList functionNos;
for (auto it = data.begin(); it != data.end(); ++it) {
if (it.value().isObject()) {
QJsonObject obj = it.value().toObject();
QString cname = obj.value("cname").toString();
if (!cname.isEmpty()) {
cnames.append(cname);
eNames.append(obj.value("eName").toString());
functionNos.append(obj.value("functionNo").toString());
}
}
}
if (cnames.isEmpty()) {
LogManager::instance()->logWarning("数据缓存 - UF20没有找到cname记录");
return true;
}
m_db.transaction();
query.prepare("INSERT INTO uf20_cname_table (cname, e_name, function_no, update_time) VALUES (?, ?, ?, ?)");
QString updateTime = QDateTime::currentDateTime().toString(Qt::ISODate);
for (int i = 0; i < cnames.size(); ++i) {
query.addBindValue(cnames[i]);
query.addBindValue(eNames[i]);
query.addBindValue(functionNos[i]);
query.addBindValue(updateTime);
query.exec();
}
m_db.commit();
LogManager::instance()->logInfo(QString("数据缓存 - UF20成功保存 %1 个cname记录,耗时: %2ms").arg(cnames.size()).arg(timer.elapsed()));
return true;
}
bool DataCache::saveUFT3Config(const QJsonObject& data)
{
if (!m_db.isOpen()) {
if (!conn.transaction()) {
LogManager::instance()->logError(QString("数据缓存 - 写入 %1.%2 开启事务失败").arg(db, table));
return false;
}
QElapsedTimer timer;
timer.start();
QSqlQuery query(m_db);
query.exec("DELETE FROM uft3_cname_table");
QStringList cnames;
QStringList eNames;
QStringList functionNos;
for (auto it = data.begin(); it != data.end(); ++it) {
if (it.value().isObject()) {
QJsonObject obj = it.value().toObject();
QString cname = obj.value("cname").toString();
if (!cname.isEmpty()) {
cnames.append(cname);
eNames.append(obj.value("eName").toString());
functionNos.append(obj.value("functionNo").toString());
QSqlQuery q(conn);
// 临时表
q.exec(QString("DROP TABLE IF EXISTS %1").arg(tmpTable));
if (!q.exec(QString("CREATE TABLE %1 (name TEXT PRIMARY KEY, data TEXT)").arg(tmpTable))) {
conn.rollback();
LogManager::instance()->logError(QString("数据缓存 - 建临时表失败: %1").arg(q.lastError().text()));
return false;
}
// 用 bindValue(索引) 而非 addBindValue: 后者是"追加"绑定, 在循环里会不断累积,
// 按索引赋值才是幂等的, 避免残留旧绑定值。
q.prepare(QString("INSERT OR REPLACE INTO %1 (name, data) VALUES (?, ?)").arg(tmpTable));
for (auto it = rows.begin(); it != rows.end(); ++it) {
q.bindValue(0, it.key());
q.bindValue(1, it.value());
if (!q.exec()) {
conn.rollback();
LogManager::instance()->logError(QString("数据缓存 - 写入 %1.%2 行失败: %3")
.arg(db, table, q.lastError().text()));
return false;
}
}
// 原子替换
q.exec(QString("DROP TABLE IF EXISTS %1").arg(table));
if (!q.exec(QString("ALTER TABLE %1 RENAME TO %2").arg(tmpTable, table))) {
conn.rollback();
LogManager::instance()->logError(QString("数据缓存 - 重命名 %1 失败: %2")
.arg(table, q.lastError().text()));
return false;
}
// 更新 _meta
q.exec("CREATE TABLE IF NOT EXISTS _meta (dataset TEXT PRIMARY KEY, row_count INTEGER, src_mtime REAL)");
q.prepare("INSERT OR REPLACE INTO _meta (dataset, row_count, src_mtime) VALUES (?, ?, ?)");
q.bindValue(0, table);
q.bindValue(1, rows.size());
q.bindValue(2, srcMtime);
if (!q.exec()) {
conn.rollback();
LogManager::instance()->logError(QString("数据缓存 - 更新 _meta 失败: %1").arg(q.lastError().text()));
return false;
}
if (cnames.isEmpty()) {
LogManager::instance()->logWarning("数据缓存 - UFT3没有找到cname记录");
return true;
if (!conn.commit()) {
conn.rollback();
LogManager::instance()->logError(QString("数据缓存 - 提交 %1.%2 失败").arg(db, table));
return false;
}
m_db.transaction();
query.prepare("INSERT INTO uft3_cname_table (cname, e_name, function_no, update_time) VALUES (?, ?, ?, ?)");
QString updateTime = QDateTime::currentDateTime().toString(Qt::ISODate);
for (int i = 0; i < cnames.size(); ++i) {
query.addBindValue(cnames[i]);
query.addBindValue(eNames[i]);
query.addBindValue(functionNos[i]);
query.addBindValue(updateTime);
query.exec();
}
m_db.commit();
LogManager::instance()->logInfo(QString("数据缓存 - UFT3成功保存 %1 个cname记录,耗时: %2ms").arg(cnames.size()).arg(timer.elapsed()));
LogManager::instance()->logInfo(QString("数据缓存 - 已写入 %1.%2 共 %3 行")
.arg(db, table).arg(rows.size()));
// 接口表落库后使功能查询内存缓存失效, 下次查询自动重建
if (table == Constants::Cache::Uf20InterfaceTable) m_uf20FuncCacheValid = false;
if (table == Constants::Cache::Uft3InterfaceTable) m_uft3FuncCacheValid = false;
return true;
}
// ---- 是否已缓存 ----
bool DataCache::hasUF20Config(const QString& fileName)
{
if (!m_db.isOpen()) {
return false;
}
if (fileName != "uf2.json") {
return true;
}
QSqlQuery query(m_db);
if (query.exec("SELECT COUNT(*) FROM uf20_cname_table")) {
if (query.next()) {
return query.value(0).toInt() > 0;
}
}
return false;
Q_UNUSED(fileName);
return datasetExists("uf20", Constants::Cache::Uf20InterfaceTable);
}
bool DataCache::hasUFT3Config(const QString& fileName)
{
if (!m_db.isOpen()) {
return false;
Q_UNUSED(fileName);
return datasetExists("uft3", Constants::Cache::Uft3InterfaceTable);
}
// ---- 功能查询: 从 *interface 表读取 data(JSON) 提取 cname/eName/functionNo ----
namespace {
// 从单行 data(可能为数组或对象) 解析出 FunctionInfo
QList<FunctionInfo> parseInterfaceData(const QString& dataJson)
{
QList<FunctionInfo> list;
if (dataJson.isEmpty()) {
return list;
}
QJsonDocument doc = QJsonDocument::fromJson(dataJson.toUtf8());
QJsonArray arr;
if (doc.isArray()) {
arr = doc.array();
} else if (doc.isObject()) {
arr.append(doc.object());
}
for (const QJsonValue& v : arr) {
if (!v.isObject()) continue;
QJsonObject obj = v.toObject();
FunctionInfo info;
info.cname = obj.value("cname").toString();
info.eName = obj.value("eName").toString();
info.functionNo = obj.value("functionNo").toString();
if (!info.cname.isEmpty()) {
list.append(info);
}
}
return list;
}
}
// 懒加载并缓存指定库的解析后 FunctionInfo 列表。
// 首次调用或缓存失效时, 全表读取 *interface 表并解析 JSON 构建列表, 之后直接返回内存副本,
// 避免每次 search* / getAll*Cnames 都全表扫描 + 重解析所有 JSON 行。
const QList<FunctionInfo>& DataCache::cachedFunctions(const QString& db) const
{
bool isUf20 = (db == "uf20");
QList<FunctionInfo>& cache = isUf20 ? m_uf20FuncCache : m_uft3FuncCache;
bool& valid = isUf20 ? m_uf20FuncCacheValid : m_uft3FuncCacheValid;
if (valid) {
return cache;
}
if (fileName != "uft3.json") {
return true;
cache.clear();
QElapsedTimer timer;
timer.start();
if (isUf20) {
// UF20 功能号全部落在 uf2_interface 一张表
QMap<QString, QString> rows = getAllRows(db, Constants::Cache::Uf20InterfaceTable);
for (auto it = rows.begin(); it != rows.end(); ++it) {
cache.append(parseInterfaceData(it.value()));
}
} else {
// UFT3 功能号分散在 uft3_interface (普通接口) 与 uft3_trans_code (转码) 两张表,
// 必须合并才能覆盖全部 LS 转码功能号 (如 LS_证券周边_客户资金余额获取)。
QStringList tables;
tables << Constants::Cache::Uft3InterfaceTable
<< Constants::Cache::TblUft3TransCode;
for (const QString& t : tables) {
QMap<QString, QString> rows = getAllRows(db, t);
for (auto it = rows.begin(); it != rows.end(); ++it) {
cache.append(parseInterfaceData(it.value()));
}
}
}
valid = true;
LogManager::instance()->logInfo(QString("数据缓存 - 重建 %1 功能缓存 %2 条,耗时: %3ms")
.arg(db).arg(cache.size()).arg(timer.elapsed()));
return cache;
}
QSqlQuery query(m_db);
if (query.exec("SELECT COUNT(*) FROM uft3_cname_table")) {
if (query.next()) {
return query.value(0).toInt() > 0;
}
}
return false;
void DataCache::refreshFunctionCache()
{
m_uf20FuncCacheValid = false;
m_uft3FuncCacheValid = false;
// 立即预热, 使后续查询走内存缓存
cachedFunctions("uf20");
cachedFunctions("uft3");
LogManager::instance()->logInfo("数据缓存 - 功能查询缓存已重建");
}
bool DataCache::checkUF20CnameExists(const QString& cname)
{
if (!m_db.isOpen()) {
QSqlDatabase conn = database("uf20");
if (!conn.isOpen()) {
return false;
}
QSqlQuery query(m_db);
query.prepare("SELECT COUNT(*) FROM uf20_cname_table WHERE cname = :cname");
query.bindValue(":cname", cname);
if (query.exec() && query.next()) {
return query.value(0).toInt() > 0;
}
return false;
QSqlQuery query(conn);
query.prepare(QString("SELECT data FROM %1 WHERE name = :k").arg(Constants::Cache::Uf20InterfaceTable));
query.bindValue(":k", cname);
return query.exec() && query.next();
}
QSet<QString> DataCache::getAllUF20Cnames()
{
QSet<QString> result;
if (!m_db.isOpen()) {
return result;
}
QElapsedTimer timer;
timer.start();
QSqlQuery query(m_db);
if (query.exec("SELECT cname FROM uf20_cname_table")) {
while (query.next()) {
result.insert(query.value(0).toString());
const QList<FunctionInfo>& all = cachedFunctions("uf20");
for (const FunctionInfo& info : all) {
if (!info.cname.isEmpty()) {
result.insert(info.cname);
}
}
LogManager::instance()->logInfo(QString("数据缓存 - 获取UF20 Cname缓存,耗时: %1ms").arg(timer.elapsed()));
LogManager::instance()->logInfo(QString("数据缓存 - 获取UF20 Cname缓存 %1 条").arg(result.size()));
return result;
}
QList<FunctionInfo> DataCache::searchUF20Functions(const QString& keyword, const QString& field, bool exactMatch)
{
QList<FunctionInfo> result;
QString kw = keyword.trimmed();
Qt::CaseSensitivity cs = exactMatch ? Qt::CaseSensitive : Qt::CaseInsensitive;
if (!m_db.isOpen()) {
return result;
}
QSqlQuery query(m_db);
QString sql;
QString keywordValue = exactMatch ? keyword : "%" + keyword + "%";
QString op = exactMatch ? "= :keyword" : "LIKE :keyword";
const QList<FunctionInfo>& all = cachedFunctions("uf20");
for (const FunctionInfo& info : all) {
bool hit = false;
if (field == "cname") {
sql = QString(R"(
SELECT cname, e_name, function_no
FROM uf20_cname_table
WHERE cname %1
ORDER BY cname
)").arg(op);
hit = exactMatch ? (info.cname == kw) : info.cname.contains(kw, cs);
} else if (field == "function_no") {
sql = QString(R"(
SELECT cname, e_name, function_no
FROM uf20_cname_table
WHERE function_no %1
ORDER BY cname
)").arg(op);
hit = exactMatch ? (info.functionNo == kw) : info.functionNo.contains(kw, cs);
} else if (field == "eName") {
hit = exactMatch ? (info.eName == kw) : info.eName.contains(kw, cs);
} else {
sql = QString(R"(
SELECT cname, e_name, function_no
FROM uf20_cname_table
WHERE cname %1 OR e_name %1 OR function_no %1
ORDER BY cname
)").arg(op);
hit = (info.cname.contains(kw, cs) ||
info.eName.contains(kw, cs) ||
info.functionNo.contains(kw, cs));
}
query.prepare(sql);
query.bindValue(":keyword", keywordValue);
if (query.exec()) {
while (query.next()) {
FunctionInfo info;
info.cname = query.value(0).toString();
info.eName = query.value(1).toString();
info.functionNo = query.value(2).toString();
if (hit) {
result.append(info);
}
}
return result;
}
bool DataCache::checkUFT3CnameExists(const QString& cname)
{
if (!m_db.isOpen()) {
QSqlDatabase conn = database("uft3");
if (!conn.isOpen()) {
return false;
}
QSqlQuery query(m_db);
query.prepare("SELECT COUNT(*) FROM uft3_cname_table WHERE cname = :cname");
query.bindValue(":cname", cname);
if (query.exec() && query.next()) {
return query.value(0).toInt() > 0;
}
return false;
QSqlQuery query(conn);
query.prepare(QString("SELECT data FROM %1 WHERE name = :k").arg(Constants::Cache::Uft3InterfaceTable));
query.bindValue(":k", cname);
return query.exec() && query.next();
}
QSet<QString> DataCache::getAllUFT3Cnames()
{
QSet<QString> result;
if (!m_db.isOpen()) {
return result;
}
QElapsedTimer timer;
timer.start();
QSqlQuery query(m_db);
if (query.exec("SELECT cname FROM uft3_cname_table")) {
while (query.next()) {
result.insert(query.value(0).toString());
const QList<FunctionInfo>& all = cachedFunctions("uft3");
for (const FunctionInfo& info : all) {
if (!info.cname.isEmpty()) {
result.insert(info.cname);
}
}
LogManager::instance()->logInfo(QString("数据缓存 - 获取UFT3 Cname缓存,耗时: %1ms").arg(timer.elapsed()));
LogManager::instance()->logInfo(QString("数据缓存 - 获取UFT3 Cname缓存 %1 条").arg(result.size()));
return result;
}
QList<FunctionInfo> DataCache::searchUFT3Functions(const QString& keyword, const QString& field, bool exactMatch)
{
QList<FunctionInfo> result;
QString kw = keyword.trimmed();
Qt::CaseSensitivity cs = exactMatch ? Qt::CaseSensitive : Qt::CaseInsensitive;
if (!m_db.isOpen()) {
return result;
}
QSqlQuery query(m_db);
QString sql;
QString keywordValue = exactMatch ? keyword : "%" + keyword + "%";
QString op = exactMatch ? "= :keyword" : "LIKE :keyword";
const QList<FunctionInfo>& all = cachedFunctions("uft3");
for (const FunctionInfo& info : all) {
bool hit = false;
if (field == "cname") {
sql = QString(R"(
SELECT cname, e_name, function_no
FROM uft3_cname_table
WHERE cname %1
ORDER BY cname
)").arg(op);
hit = exactMatch ? (info.cname == kw) : info.cname.contains(kw, cs);
} else if (field == "function_no") {
sql = QString(R"(
SELECT cname, e_name, function_no
FROM uft3_cname_table
WHERE function_no %1
ORDER BY cname
)").arg(op);
hit = exactMatch ? (info.functionNo == kw) : info.functionNo.contains(kw, cs);
} else if (field == "eName") {
hit = exactMatch ? (info.eName == kw) : info.eName.contains(kw, cs);
} else {
sql = QString(R"(
SELECT cname, e_name, function_no
FROM uft3_cname_table
WHERE cname %1 OR e_name %1 OR function_no %1
ORDER BY cname
)").arg(op);
hit = (info.cname.contains(kw, cs) ||
info.eName.contains(kw, cs) ||
info.functionNo.contains(kw, cs));
}
query.prepare(sql);
query.bindValue(":keyword", keywordValue);
if (query.exec()) {
while (query.next()) {
FunctionInfo info;
info.cname = query.value(0).toString();
info.eName = query.value(1).toString();
info.functionNo = query.value(2).toString();
if (hit) {
result.append(info);
}
}
return result;
}
void DataCache::clearCache()
{
if (!m_db.isOpen()) {
return;
// 缓存库由 C++ 元数据解析直接落库维护。此处仅重置内存状态并关闭连接, 不删除库文件。
QMutexLocker locker(&m_connMutex);
for (auto it = m_uf20Db.begin(); it != m_uf20Db.end(); ++it) {
if (it.value().isOpen()) it.value().close();
QSqlDatabase::removeDatabase(it.value().connectionName());
}
QSqlQuery query(m_db);
query.exec("DELETE FROM uf20_cname_table");
query.exec("DELETE FROM uft3_cname_table");
for (auto it = m_uft3Db.begin(); it != m_uft3Db.end(); ++it) {
if (it.value().isOpen()) it.value().close();
QSqlDatabase::removeDatabase(it.value().connectionName());
}
m_uf20Db.clear();
m_uft3Db.clear();
m_dataLoaded = false;
// 失效功能查询内存缓存, 重新打开后首次查询自动重建
m_uf20FuncCacheValid = false;
m_uft3FuncCacheValid = false;
LogManager::instance()->logInfo("数据缓存 - 已重置缓存连接(缓存库文件保留)");
}
+55 -11
View File
@@ -4,7 +4,13 @@
#include <QObject>
#include <QSqlDatabase>
#include <QJsonObject>
#include <QJsonArray>
#include <QMap>
#include <QSet>
#include <QHash>
#include <QThread>
#include <QMutex>
#include <QMutexLocker>
struct FunctionInfo {
QString cname;
@@ -12,6 +18,10 @@ struct FunctionInfo {
QString functionNo;
};
// DataCache: 缓存库(uf20_cache.db / uft3_cache.db)由 C++ 元数据解析后直接落库维护,
// 存放于 <exe>/cache/ 目录。物理表结构统一为 (name TEXT PRIMARY KEY, data TEXT),
// data 为 JSON 序列化字符串, 对齐 Python 参考项目 src/store/schema.py。
// 库文件不存在时 database() 会自动创建 cache/ 目录与 DB 文件, 无需预先运行 Python 工具。
class DataCache : public QObject
{
Q_OBJECT
@@ -19,21 +29,37 @@ class DataCache : public QObject
public:
static DataCache* instance();
// 打开两个缓存库(uf20_cache.db / uft3_cache.db)。库不存在时自动创建, 成功打开任一库即返回 true。
bool initDatabase();
bool isDataLoaded() const;
void setDataLoaded(bool loaded);
bool saveUF20Config(const QJsonObject& data);
bool saveUFT3Config(const QJsonObject& data);
// ---- 通用只读 API (db 选 "uf20" / "uft3", table 为 Constants::Cache 中表名) ----
// 物理表结构统一为: (name TEXT PRIMARY KEY, data TEXT), 对齐 Python src/store/schema.py
// 接口表是否存在且非空(用于“是否已缓存”判定)
bool datasetExists(const QString& db, const QString& table) const;
// 读取整张键值表: name -> data(JSON 字符串)
QMap<QString, QString> getAllRows(const QString& db, const QString& table) const;
// 读取单行 data
QString getRowData(const QString& db, const QString& table, const QString& key) const;
// 行数
int rowCount(const QString& db, const QString& table) const;
bool hasUF20Config(const QString& fileName);
bool hasUFT3Config(const QString& fileName);
// ---- 通用写入 API (C++ 解析源文件后落库到双库对应表) ----
// rows: name -> data(JSON 字符串); 原子写(临时表 + RENAME)后更新 _meta
bool saveDataset(const QString& db, const QString& table,
const QMap<QString, QString>& rows, double srcMtime = 0.0);
// 工程路径指纹:记录上次成功写入 cname 表时所用工程路径,
// 用于检测“换机器/换路径后旧缓存 db 仍非空”导致功能查询读到旧数据的问题
bool saveProjectPaths(const QMap<QString, QString>& paths);
QMap<QString, QString> loadProjectPaths();
// ---- 兼容旧调用方的占位实现 (配置由元数据解析直接落双库, 此处为空操作) ----
bool saveUF20Config(const QJsonObject& data) { Q_UNUSED(data); return true; }
bool saveUFT3Config(const QJsonObject& data) { Q_UNUSED(data); return true; }
// 是否已缓存对应版本配置(= 接口表非空)
bool hasUF20Config(const QString& fileName = QString());
bool hasUFT3Config(const QString& fileName = QString());
// ---- 功能查询 (从 *interface 表读取) ----
bool checkUF20CnameExists(const QString& cname);
QSet<QString> getAllUF20Cnames();
QList<FunctionInfo> searchUF20Functions(const QString& keyword, const QString& field = "", bool exactMatch = false);
@@ -42,18 +68,36 @@ public:
QSet<QString> getAllUFT3Cnames();
QList<FunctionInfo> searchUFT3Functions(const QString& keyword, const QString& field = "", bool exactMatch = false);
// 重新构建功能查询内存缓存(元数据落库后调用, 使后续查询走缓存而非全表扫描)
void refreshFunctionCache();
void clearCache();
private:
explicit DataCache(QObject *parent = nullptr);
~DataCache();
bool createTables();
QString getDatabasePath();
// 取指定库连接。QSqlDatabase 连接只能在创建它的线程中使用,
// 因此按线程分别建立并缓存连接(元数据更新在 QtConcurrent 后台线程执行,
// 而查询在主线程执行, 共用同一连接会导致写入静默失败)。
QSqlDatabase database(const QString& db) const;
// 懒加载并缓存指定库的解析后 FunctionInfo 列表(避免每次查询全表扫描+重解析)
const QList<FunctionInfo>& cachedFunctions(const QString& db) const;
static DataCache* m_instance;
QSqlDatabase m_db;
// key = 线程指针, value = 该线程专用的数据库连接
// QHash 非线程安全, 由 m_connMutex 保护(主线程查询与后台线程落库会并发访问)
mutable QHash<QThread*, QSqlDatabase> m_uf20Db;
mutable QHash<QThread*, QSqlDatabase> m_uft3Db;
mutable QMutex m_connMutex;
bool m_dataLoaded;
// 功能查询内存缓存(UF20/UFT3 各一份, 落库或清缓存后失效重建)
mutable QList<FunctionInfo> m_uf20FuncCache;
mutable QList<FunctionInfo> m_uft3FuncCache;
mutable bool m_uf20FuncCacheValid;
mutable bool m_uft3FuncCacheValid;
};
#endif
+3
View File
@@ -16,5 +16,8 @@
},
"V1.5.0": {
"release_notes": "1.解决转码遇到公共已转函数导致失败部分接口转码失败问题;"
},
"V1.6.0": {
"release_notes": "1.转码逻辑优化;\n2.元数据更新逻辑优化;\n3.元数据更新存储修改为db存储模式;\n4.写日志异常修复"
}
}