diff --git a/Uft30ChangeCode.pro b/Uft30ChangeCode.pro index 23f0461..8d1acb1 100644 --- a/Uft30ChangeCode.pro +++ b/Uft30ChangeCode.pro @@ -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 \ diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index cfc4ec5..d10d317 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -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; } diff --git a/src/metadataupdate/metadataprocessor.cpp b/src/metadataupdate/metadataprocessor.cpp index 1b57044..a764165 100644 --- a/src/metadataupdate/metadataprocessor.cpp +++ b/src/metadataupdate/metadataprocessor.cpp @@ -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 #include #include +#include #include 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(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(it.fileInfo().lastModified().toSecsSinceEpoch()); + if (m > latest) { + latest = m; + } + } + if (count == 0) { + // 未扫到任何文件(空目录/无权限/遍历异常): 回退为目录自身 mtime, + // 避免 src_mtime 记为 0 导致缓存失效判断始终认为"源已变更"。 + latest = static_cast(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 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 entries = scanner->collectFiles(dirPath, baseCtx); + (void)entries; // 全量模式下扫描在 scanDir 内完成, entries 仅供增量模式遍历使用 QMap 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 mtimeByPath; - for (const FileEntry& e : entries) mtimeByPath[e.path] = e.mtime; - ScanResult scanResult = scanner->scanDir(dirPath, baseCtx); QMap resultMap = scanResult.resultMap; QMap 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 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); - } - } - - 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())); + saveRowsFromJson("uf20", Constants::Cache::TblUf2Interface, resultJson, srcMtime); } - 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; } @@ -488,16 +422,13 @@ bool MetadataProcessor::processUf2DataType() } resultJson[key] = typeJson; } - - 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 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); - + + // 解析结果直接落库 uf20.uf2_datatype, 不再生成中间 JSON(uf2dataType.json) + saveRowsFromJson("uf20", Constants::Cache::TblUf2DataType, resultJson, dirMtime(m_uf20Path)); + + LogManager::instance()->log(QString("UF2.0 数据类型处理完成,耗时: %1ms,已落库: uf20.%2") + .arg(timer.elapsed()).arg(Constants::Cache::TblUf2DataType)); + return true; } @@ -509,18 +440,21 @@ bool MetadataProcessor::processUf2StdFields() QString dirPath = m_uf20Path; + // 数据类型依赖从缓存库 uf20.uf2_datatype 读取, 不再读中间 JSON(uf2dataType.json) QMap> uf2datatypeMap; - QString datatypeJsonPath = QDir(m_uf2touft3Path).filePath("uf2dataType.json"); - QJsonObject datatypeJson = readJson(datatypeJsonPath); - for (const QString& key : datatypeJson.keys()) { - QJsonObject typeObj = datatypeJson[key].toObject(); - QMap typeInfo; - for (const QString& attrKey : typeObj.keys()) { - typeInfo[attrKey] = typeObj[attrKey].toString(); + if (DataCache::instance()->initDatabase()) { + QMap 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 typeInfo; + for (const QString& attrKey : typeObj.keys()) { + typeInfo[attrKey] = typeObj[attrKey].toString(); + } + uf2datatypeMap[it.key()] = typeInfo; } - uf2datatypeMap[key] = typeInfo; } - + Uf2StdFields uf2StdFields; auto resultMap = uf2StdFields.loadData(dirPath, uf2datatypeMap); @@ -533,16 +467,13 @@ bool MetadataProcessor::processUf2StdFields() } resultJson[key] = fieldJson; } - - 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 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); - + + // 解析结果直接落库 uf20.uf2_stdfield, 不再生成中间 JSON(uf2stdfield.json) + saveRowsFromJson("uf20", Constants::Cache::TblUf2StdField, resultJson, dirMtime(m_uf20Path)); + + LogManager::instance()->log(QString("UF2.0 标准字段处理完成,耗时: %1ms,已落库: uf20.%2") + .arg(timer.elapsed()).arg(Constants::Cache::TblUf2StdField)); + return true; } @@ -566,16 +497,122 @@ bool MetadataProcessor::processUf2ErrNumber() } resultJson[key] = errorJson; } - - QString outputPath = QDir(m_uf2touft3Path).filePath("uf2Error.json"); - - if (!writeJson(outputPath, resultJson)) { - LogManager::instance()->log(QString("UF2.0 标准错误号处理失败,耗时: %1ms").arg(timer.elapsed())); - return false; + + // 解析结果直接落库 uf20.uf2_error, 不再生成中间 JSON(uf2Error.json) + saveRowsFromJson("uf20", Constants::Cache::TblUf2Error, resultJson, dirMtime(m_uf20Path)); + + 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& 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& 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& 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; } @@ -599,16 +636,13 @@ bool MetadataProcessor::processUft3DataType() } resultJson[key] = typeJson; } - - 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 数据类型处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); - + + // 解析结果直接落库 uft3.uft3_datatype, 不再生成中间 JSON(uft3datatype.json) + saveRowsFromJson("uft3", Constants::Cache::TblUft3DataType, resultJson, dirMtime(m_uft30PubPath)); + + LogManager::instance()->log(QString("UFT3.0 数据类型处理完成,耗时: %1ms,已落库: uft3.%2") + .arg(timer.elapsed()).arg(Constants::Cache::TblUft3DataType)); + return true; } @@ -620,18 +654,21 @@ bool MetadataProcessor::processUft3StdFields() QString dirPath = m_uft30PubPath; + // 数据类型依赖从缓存库 uft3.uft3_datatype 读取, 不再读中间 JSON(uft3datatype.json) QMap> uf3datatypeMap; - QString datatypeJsonPath = QDir(m_uf2touft3Path).filePath("uft3datatype.json"); - QJsonObject datatypeJson = readJson(datatypeJsonPath); - for (const QString& key : datatypeJson.keys()) { - QJsonObject typeObj = datatypeJson[key].toObject(); - QMap typeInfo; - for (const QString& attrKey : typeObj.keys()) { - typeInfo[attrKey] = typeObj[attrKey].toString(); + if (DataCache::instance()->initDatabase()) { + QMap 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 typeInfo; + for (const QString& attrKey : typeObj.keys()) { + typeInfo[attrKey] = typeObj[attrKey].toString(); + } + uf3datatypeMap[it.key()] = typeInfo; } - uf3datatypeMap[key] = typeInfo; } - + Uft3StdFields uft3StdFields; auto resultMap = uft3StdFields.loadData(dirPath, uf3datatypeMap); @@ -644,16 +681,13 @@ bool MetadataProcessor::processUft3StdFields() } resultJson[key] = fieldJson; } - - 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 标准字段处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); - + + // 解析结果直接落库 uft3.uft3_stdfield, 不再生成中间 JSON(uft3stdfields.json) + saveRowsFromJson("uft3", Constants::Cache::TblUft3StdField, resultJson, dirMtime(m_uft30PubPath)); + + LogManager::instance()->log(QString("UFT3.0 标准字段处理完成,耗时: %1ms,已落库: uft3.%2") + .arg(timer.elapsed()).arg(Constants::Cache::TblUft3StdField)); + return true; } @@ -680,15 +714,12 @@ bool MetadataProcessor::processUft3Table() } resultJson[key] = tableJson; } - - 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 表结构处理完成,耗时: %1ms,结果已保存到: %2").arg(timer.elapsed()).arg(outputPath)); - + + // 解析结果直接落库 uft3.uft3_table, 不再生成中间 JSON(uft3Table.json) + saveRowsFromJson("uft3", Constants::Cache::TblUft3Table, resultJson, dirMtime(m_uft30PubPath)); + + LogManager::instance()->log(QString("UFT3.0 表结构处理完成,耗时: %1ms,已落库: uft3.%2") + .arg(timer.elapsed()).arg(Constants::Cache::TblUft3Table)); + return true; } \ No newline at end of file diff --git a/src/metadataupdate/metadataprocessor.h b/src/metadataupdate/metadataprocessor.h index 2d85350..cdc60ee 100644 --- a/src/metadataupdate/metadataprocessor.h +++ b/src/metadataupdate/metadataprocessor.h @@ -23,6 +23,10 @@ public: bool processUf2StdFields(); bool processUf2DataType(); bool processUf2ErrNumber(); + bool processUf2Constants(); + bool processUf2MenuFunc(); + bool processUf2Module(); + bool processUf2Table(); bool processUft3StdFields(); bool processUft3DataType(); @@ -37,28 +41,28 @@ 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 序列化字符串), + // 落库路径 /cache/uf20_cache.db 与 /cache/uft3_cache.db(见 DataCache)。 + // 各 processUf2Xxx/processUft3Xxx 解析后调用 saveRowsFromJson 落库, 不再生成中间 JSON 文件。 + QString m_basePath; QString m_uf2touft3Path; diff --git a/src/metadataupdate/uf2constants.cpp b/src/metadataupdate/uf2constants.cpp new file mode 100644 index 0000000..2c0df57 --- /dev/null +++ b/src/metadataupdate/uf2constants.cpp @@ -0,0 +1,41 @@ +#include "uf2constants.h" +#include "utils/Constants.h" +#include +#include +#include + +Uf2Constants::Uf2Constants(QObject *parent) : QObject(parent) +{ +} + +QMap> Uf2Constants::loadData(const QString& dirPath) +{ + QMap> 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 entry; + entry["cname"] = name; + entry["value"] = value; + resultMap[name] = entry; // key = constant_name + } + } + file.close(); + return resultMap; +} diff --git a/src/metadataupdate/uf2constants.h b/src/metadataupdate/uf2constants.h new file mode 100644 index 0000000..b0f2b48 --- /dev/null +++ b/src/metadataupdate/uf2constants.h @@ -0,0 +1,21 @@ +#ifndef UF2CONSTANTS_H +#define UF2CONSTANTS_H + +#include +#include +#include + +// 解析 UF2.0 常量文件: 公共资源/constants.xml +// key = constant_name, data = {cname, value} +class Uf2Constants : public QObject +{ + Q_OBJECT +public: + explicit Uf2Constants(QObject *parent = nullptr); + + QMap> loadData(const QString& dirPath); + +private: +}; + +#endif diff --git a/src/metadataupdate/uf2menuandfunction.cpp b/src/metadataupdate/uf2menuandfunction.cpp new file mode 100644 index 0000000..afc0cf9 --- /dev/null +++ b/src/metadataupdate/uf2menuandfunction.cpp @@ -0,0 +1,119 @@ +#include "uf2menuandfunction.h" +#include "utils/Constants.h" +#include +#include +#include + +Uf2MenuAndFunction::Uf2MenuAndFunction(QObject *parent) : QObject(parent) +{ +} + +QMap> Uf2MenuAndFunction::loadData(const QString& dirPath) +{ + QMap> 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 entry;// 直接子元素名 -> 文本 + int depth = 0; // item 内部相对深度, item 自身为 0 + QString tag; // 当前直接子元素名 + QString text; // 当前直接子元素文本缓冲 + }; + + ItemState cur; + bool inItem = false; + QList 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 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; +} diff --git a/src/metadataupdate/uf2menuandfunction.h b/src/metadataupdate/uf2menuandfunction.h new file mode 100644 index 0000000..b28dcef --- /dev/null +++ b/src/metadataupdate/uf2menuandfunction.h @@ -0,0 +1,23 @@ +#ifndef UF2MENUANDFUNCTION_H +#define UF2MENUANDFUNCTION_H + +#include +#include +#include + +// 解析 UF2.0 菜单与功能: 公共资源/menuandfunction.xml +// 含 与 , 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> loadData(const QString& dirPath); + +private: +}; + +#endif diff --git a/src/metadataupdate/uf2module.cpp b/src/metadataupdate/uf2module.cpp new file mode 100644 index 0000000..ad76adc --- /dev/null +++ b/src/metadataupdate/uf2module.cpp @@ -0,0 +1,59 @@ +#include "uf2module.h" +#include "utils/logmanager.h" +#include +#include +#include +#include + +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 一致), + // 而非 的 cname 属性 (该属性通常为空)。 + QString cname = QDir(info.path()).dirName(); + QString ename = attrs.value("ename").toString().trimmed(); + if (ename.isEmpty()) { + continue; + } + QMap entry; + entry["cname"] = cname; + entry["path"] = info.path(); // 所在目录 + m_resultMap[ename] = entry; // key = ename + } + } + file.close(); + } + } +} + +QMap> Uf2Module::loadData(const QString& dirPath) +{ + m_resultMap.clear(); + scanDir(dirPath); + return m_resultMap; +} diff --git a/src/metadataupdate/uf2module.h b/src/metadataupdate/uf2module.h new file mode 100644 index 0000000..aa63c2a --- /dev/null +++ b/src/metadataupdate/uf2module.h @@ -0,0 +1,23 @@ +#ifndef UF2MODULE_H +#define UF2MODULE_H + +#include +#include +#include + +// 解析 UF2.0 模块: 工程各目录下 module.xml +// key = ename, data = {cname, path} +class Uf2Module : public QObject +{ + Q_OBJECT +public: + explicit Uf2Module(QObject *parent = nullptr); + + QMap> loadData(const QString& dirPath); + +private: + void scanDir(const QString& dirPath); + QMap> m_resultMap; +}; + +#endif diff --git a/src/metadataupdate/uf2table.cpp b/src/metadataupdate/uf2table.cpp new file mode 100644 index 0000000..a04f699 --- /dev/null +++ b/src/metadataupdate/uf2table.cpp @@ -0,0 +1,128 @@ +#include "uf2table.h" +#include "utils/logmanager.h" +#include +#include +#include +#include +#include +#include + +Uf2Table::Uf2Table(QObject *parent) : QObject(parent) +{ +} + +QString schemaToUser(const QString& schema) +{ + static const QMap 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 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; +} diff --git a/src/metadataupdate/uf2table.h b/src/metadataupdate/uf2table.h new file mode 100644 index 0000000..f6f8be4 --- /dev/null +++ b/src/metadataupdate/uf2table.h @@ -0,0 +1,25 @@ +#ifndef UF2TABLE_H +#define UF2TABLE_H + +#include +#include +#include +#include + +// 解析 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 loadData(const QString& dirPath, const QString& settDirPath = QString()); + +private: + void scanDir(const QString& dirPath); + QJsonObject loadTable(const QString& filePath, const QString& tableCName); + QMap m_resultMap; +}; + +#endif diff --git a/src/pages/metadatupdate/metadatupdatepage.cpp b/src/pages/metadatupdate/metadatupdatepage.cpp index e1c04c8..423b8be 100644 --- a/src/pages/metadatupdate/metadatupdatepage.cpp +++ b/src/pages/metadatupdate/metadatupdatepage.cpp @@ -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 #include @@ -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)); diff --git a/src/pages/metadatupdate/metadatupdatepage.h b/src/pages/metadatupdate/metadatupdatepage.h index b9d7dfe..832877f 100644 --- a/src/pages/metadatupdate/metadatupdatepage.h +++ b/src/pages/metadatupdate/metadatupdatepage.h @@ -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(); diff --git a/src/utils/Constants.h b/src/utils/Constants.h index b2ed281..f1b87fa 100644 --- a/src/utils/Constants.h +++ b/src/utils/Constants.h @@ -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 diff --git a/src/utils/configmanager.cpp b/src/utils/configmanager.cpp index 8a5a9ec..c0090b2 100644 --- a/src/utils/configmanager.cpp +++ b/src/utils/configmanager.cpp @@ -1,4 +1,5 @@ #include "configmanager.h" +#include "utils/Constants.h" #include "logmanager.h" #include #include @@ -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)); diff --git a/src/utils/configreaderbase.cpp b/src/utils/configreaderbase.cpp index 140e73b..8643e7d 100644 --- a/src/utils/configreaderbase.cpp +++ b/src/utils/configreaderbase.cpp @@ -6,19 +6,6 @@ #include #include -// 计算当前工程路径指纹(含盘符/路径变化即视为不同工程),用于识别 -// “换机器/换路径后旧缓存 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 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; - } + 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)); - 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; + // 接口表为空: 缓存库尚无数据, 需先执行元数据更新。 + LogManager::instance()->logError( + QString("%1配置加载失败 - 接口表为空, 请先执行元数据更新").arg(traits.logTag)); + m_failedFiles.append(traits.mainFile); + return false; } 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) { - LogManager::instance()->logError( - QString("%1配置重新加载失败 - JSON解析失败: %2").arg(traits.logTag).arg(filePath)); - 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 (!DataCache::instance()->initDatabase()) { LogManager::instance()->logError( - QString("%1配置重新加载失败 - 配置为空").arg(traits.logTag)); + QString("%1配置重新加载失败 - 数据库初始化失败").arg(traits.logTag)); return false; } - if (!traits.saveConfig(mergedConfig)) { + if (!traits.hasConfig(traits.mainFile)) { 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; } diff --git a/src/utils/datacache.cpp b/src/utils/datacache.cpp index 982e7cb..23ba59f 100644 --- a/src/utils/datacache.cpp +++ b/src/utils/datacache.cpp @@ -4,8 +4,11 @@ #include #include #include +#include +#include #include #include +#include #include #include @@ -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& 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; + 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(); } - - if (!query.exec(createUFT3Table)) { - LogManager::instance()->logError(QString("数据缓存 - 创建uft3_cname_table失败: %1").arg(query.lastError().text())); - return false; + // 连接名需全局唯一: 库名 + 线程地址 + QString connName = QString("%1_cache_connection_%2") + .arg(isUft3 ? "uft3" : "uf20") + .arg(reinterpret_cast(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(); } - - // 工程路径指纹表:记录上次写入 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; + { + QMutexLocker locker(&m_connMutex); + conns.insert(self, conn); } - - LogManager::instance()->log("数据缓存 - 表创建成功"); - return true; -} - -bool DataCache::saveProjectPaths(const QMap& paths) -{ - if (!m_db.isOpen()) { - return false; - } - 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 DataCache::loadProjectPaths() -{ - QMap 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(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()) { - return true; - } + // 纯只读探测: 仅当缓存库文件已存在时才打开, 不创建空库文件、不建 _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)); - return false; - } - - QSqlQuery query(m_db); - query.exec("PRAGMA synchronous = OFF"); - query.exec("PRAGMA journal_mode = MEMORY"); - - LogManager::instance()->log("数据缓存 - 数据库打开成功"); - return createTables(); + LogManager::instance()->logWarning("数据缓存 - 缓存库不存在(uf20_cache.db / uft3_cache.db), 请先执行元数据更新"); + return false; } 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; } - - QElapsedTimer timer; - timer.start(); - - 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()); - } + 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 DataCache::getAllRows(const QString& db, const QString& table) const +{ + QMap 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(); } } - - if (cnames.isEmpty()) { - LogManager::instance()->logWarning("数据缓存 - UF20没有找到cname记录"); - return true; + return result; +} + +QString DataCache::getRowData(const QString& db, const QString& table, const QString& key) const +{ + QSqlDatabase conn = database(db); + if (!conn.isOpen()) { + return QString(); } - - 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(); + 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(); } - - m_db.commit(); - - LogManager::instance()->logInfo(QString("数据缓存 - UF20成功保存 %1 个cname记录,耗时: %2ms").arg(cnames.size()).arg(timer.elapsed())); + 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__new -> 写入 -> DROP 旧表 -> RENAME 临时表 -> 更新 _meta +bool DataCache::saveDataset(const QString& db, const QString& table, + const QMap& rows, double srcMtime) +{ + QSqlDatabase conn = database(db); + if (!conn.isOpen()) { + LogManager::instance()->logError(QString("数据缓存 - 写入 %1.%2 失败: 库未打开").arg(db, table)); + return false; + } + + QString tmpTable = "t_" + table + "_new"; + + if (!conn.transaction()) { + LogManager::instance()->logError(QString("数据缓存 - 写入 %1.%2 开启事务失败").arg(db, table)); + return false; + } + + 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 (!conn.commit()) { + conn.rollback(); + LogManager::instance()->logError(QString("数据缓存 - 提交 %1.%2 失败").arg(db, table)); + return false; + } + 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::saveUFT3Config(const QJsonObject& data) -{ - if (!m_db.isOpen()) { - 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()); - } - } - } - - if (cnames.isEmpty()) { - LogManager::instance()->logWarning("数据缓存 - UFT3没有找到cname记录"); - return true; - } - - 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())); - 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 parseInterfaceData(const QString& dataJson) + { + QList 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; } - - if (fileName != "uft3.json") { - return true; +} + +// 懒加载并缓存指定库的解析后 FunctionInfo 列表。 +// 首次调用或缓存失效时, 全表读取 *interface 表并解析 JSON 构建列表, 之后直接返回内存副本, +// 避免每次 search* / getAll*Cnames 都全表扫描 + 重解析所有 JSON 行。 +const QList& DataCache::cachedFunctions(const QString& db) const +{ + bool isUf20 = (db == "uf20"); + QList& cache = isUf20 ? m_uf20FuncCache : m_uft3FuncCache; + bool& valid = isUf20 ? m_uf20FuncCacheValid : m_uft3FuncCacheValid; + if (valid) { + return cache; } - - QSqlQuery query(m_db); - if (query.exec("SELECT COUNT(*) FROM uft3_cname_table")) { - if (query.next()) { - return query.value(0).toInt() > 0; + + cache.clear(); + QElapsedTimer timer; + timer.start(); + if (isUf20) { + // UF20 功能号全部落在 uf2_interface 一张表 + QMap 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 rows = getAllRows(db, t); + for (auto it = rows.begin(); it != rows.end(); ++it) { + cache.append(parseInterfaceData(it.value())); + } } } - - return false; + valid = true; + LogManager::instance()->logInfo(QString("数据缓存 - 重建 %1 功能缓存 %2 条,耗时: %3ms") + .arg(db).arg(cache.size()).arg(timer.elapsed())); + return cache; +} + +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 DataCache::getAllUF20Cnames() { QSet 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& 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 DataCache::searchUF20Functions(const QString& keyword, const QString& field, bool exactMatch) { QList result; - - if (!m_db.isOpen()) { - return result; - } - - QSqlQuery query(m_db); - QString sql; - QString keywordValue = exactMatch ? keyword : "%" + keyword + "%"; - QString op = exactMatch ? "= :keyword" : "LIKE :keyword"; - - if (field == "cname") { - sql = QString(R"( - SELECT cname, e_name, function_no - FROM uf20_cname_table - WHERE cname %1 - ORDER BY cname - )").arg(op); - } 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); - } 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); - } - - 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(); + QString kw = keyword.trimmed(); + Qt::CaseSensitivity cs = exactMatch ? Qt::CaseSensitive : Qt::CaseInsensitive; + + const QList& all = cachedFunctions("uf20"); + for (const FunctionInfo& info : all) { + bool hit = false; + if (field == "cname") { + hit = exactMatch ? (info.cname == kw) : info.cname.contains(kw, cs); + } else if (field == "function_no") { + 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 { + hit = (info.cname.contains(kw, cs) || + info.eName.contains(kw, cs) || + info.functionNo.contains(kw, cs)); + } + 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 DataCache::getAllUFT3Cnames() { QSet 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& 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 DataCache::searchUFT3Functions(const QString& keyword, const QString& field, bool exactMatch) { QList result; - - if (!m_db.isOpen()) { - return result; - } - - QSqlQuery query(m_db); - QString sql; - QString keywordValue = exactMatch ? keyword : "%" + keyword + "%"; - QString op = exactMatch ? "= :keyword" : "LIKE :keyword"; - - if (field == "cname") { - sql = QString(R"( - SELECT cname, e_name, function_no - FROM uft3_cname_table - WHERE cname %1 - ORDER BY cname - )").arg(op); - } 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); - } 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); - } - - 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(); + QString kw = keyword.trimmed(); + Qt::CaseSensitivity cs = exactMatch ? Qt::CaseSensitive : Qt::CaseInsensitive; + + const QList& all = cachedFunctions("uft3"); + for (const FunctionInfo& info : all) { + bool hit = false; + if (field == "cname") { + hit = exactMatch ? (info.cname == kw) : info.cname.contains(kw, cs); + } else if (field == "function_no") { + 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 { + hit = (info.cname.contains(kw, cs) || + info.eName.contains(kw, cs) || + info.functionNo.contains(kw, cs)); + } + 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("数据缓存 - 已重置缓存连接(缓存库文件保留)"); } diff --git a/src/utils/datacache.h b/src/utils/datacache.h index 66296da..01f46ed 100644 --- a/src/utils/datacache.h +++ b/src/utils/datacache.h @@ -4,7 +4,13 @@ #include #include #include +#include +#include #include +#include +#include +#include +#include struct FunctionInfo { QString cname; @@ -12,48 +18,86 @@ struct FunctionInfo { QString functionNo; }; +// DataCache: 缓存库(uf20_cache.db / uft3_cache.db)由 C++ 元数据解析后直接落库维护, +// 存放于 /cache/ 目录。物理表结构统一为 (name TEXT PRIMARY KEY, data TEXT), +// data 为 JSON 序列化字符串, 对齐 Python 参考项目 src/store/schema.py。 +// 库文件不存在时 database() 会自动创建 cache/ 目录与 DB 文件, 无需预先运行 Python 工具。 class DataCache : public QObject { Q_OBJECT 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); - - bool hasUF20Config(const QString& fileName); - bool hasUFT3Config(const QString& fileName); - // 工程路径指纹:记录上次成功写入 cname 表时所用工程路径, - // 用于检测“换机器/换路径后旧缓存 db 仍非空”导致功能查询读到旧数据的问题 - bool saveProjectPaths(const QMap& paths); - QMap loadProjectPaths(); - + // ---- 通用只读 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 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; + + // ---- 通用写入 API (C++ 解析源文件后落库到双库对应表) ---- + // rows: name -> data(JSON 字符串); 原子写(临时表 + RENAME)后更新 _meta + bool saveDataset(const QString& db, const QString& table, + const QMap& rows, double srcMtime = 0.0); + + // ---- 兼容旧调用方的占位实现 (配置由元数据解析直接落双库, 此处为空操作) ---- + 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 getAllUF20Cnames(); QList searchUF20Functions(const QString& keyword, const QString& field = "", bool exactMatch = false); - + bool checkUFT3CnameExists(const QString& cname); QSet getAllUFT3Cnames(); QList 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& cachedFunctions(const QString& db) const; + static DataCache* m_instance; - QSqlDatabase m_db; + // key = 线程指针, value = 该线程专用的数据库连接 + // QHash 非线程安全, 由 m_connMutex 保护(主线程查询与后台线程落库会并发访问) + mutable QHash m_uf20Db; + mutable QHash m_uft3Db; + mutable QMutex m_connMutex; bool m_dataLoaded; + + // 功能查询内存缓存(UF20/UFT3 各一份, 落库或清缓存后失效重建) + mutable QList m_uf20FuncCache; + mutable QList m_uft3FuncCache; + mutable bool m_uf20FuncCacheValid; + mutable bool m_uft3FuncCacheValid; }; #endif diff --git a/update_info.json b/update_info.json index b43831f..b7aa62f 100644 --- a/update_info.json +++ b/update_info.json @@ -16,5 +16,8 @@ }, "V1.5.0": { "release_notes": "1.解决转码遇到公共已转函数导致失败部分接口转码失败问题;" + }, + "V1.6.0": { + "release_notes": "1.转码逻辑优化;\n2.元数据更新逻辑优化;\n3.元数据更新存储修改为db存储模式;\n4.写日志异常修复" } }