diff --git a/config/table_mapping.json b/config/table_mapping.json
index 894830f..003dfa6 100644
--- a/config/table_mapping.json
+++ b/config/table_mapping.json
@@ -807,6 +807,9 @@
"votelist": "usps_vote_list",
"voteresult": "vote_result",
"warrantcode": "usps_warrant_code",
- "witcode": "usps_witcode"
+ "witcode": "usps_witcode",
+ "traderiskstkholderjour":"trade_risk_stkholder_jour",
+ "traderiskstkholder":"trade_risk_stkholder",
+ "hkstockdetail": "uqms_hkstock_detail"
}
}
\ No newline at end of file
diff --git a/help/Writerside/hi.tree b/help/Writerside/hi.tree
index ba3ab54..5eb1dca 100644
--- a/help/Writerside/hi.tree
+++ b/help/Writerside/hi.tree
@@ -10,11 +10,11 @@
-
+
@@ -25,7 +25,7 @@
-
+
diff --git a/help/Writerside/images/UF20元数据.png b/help/Writerside/images/UF20元数据.png
deleted file mode 100644
index 79de5cf..0000000
Binary files a/help/Writerside/images/UF20元数据.png and /dev/null differ
diff --git a/help/Writerside/images/UFT3元数据.png b/help/Writerside/images/UFT3元数据.png
deleted file mode 100644
index c16127f..0000000
Binary files a/help/Writerside/images/UFT3元数据.png and /dev/null differ
diff --git a/help/Writerside/images/uf20功能查询.png b/help/Writerside/images/uf20功能查询.png
new file mode 100644
index 0000000..7cd81c2
Binary files /dev/null and b/help/Writerside/images/uf20功能查询.png differ
diff --git a/help/Writerside/images/uft3功能查询.png b/help/Writerside/images/uft3功能查询.png
new file mode 100644
index 0000000..91e7791
Binary files /dev/null and b/help/Writerside/images/uft3功能查询.png differ
diff --git a/help/Writerside/images/不转码函数.png b/help/Writerside/images/不转码函数.png
new file mode 100644
index 0000000..5afa1c3
Binary files /dev/null and b/help/Writerside/images/不转码函数.png differ
diff --git a/help/Writerside/images/停止转码.png b/help/Writerside/images/停止转码.png
deleted file mode 100644
index ccdf309..0000000
Binary files a/help/Writerside/images/停止转码.png and /dev/null differ
diff --git a/help/Writerside/images/元数据更新.png b/help/Writerside/images/元数据更新.png
new file mode 100644
index 0000000..3bfbdfe
Binary files /dev/null and b/help/Writerside/images/元数据更新.png differ
diff --git a/help/Writerside/images/全量转码.png b/help/Writerside/images/全量转码.png
new file mode 100644
index 0000000..4a8ae73
Binary files /dev/null and b/help/Writerside/images/全量转码.png differ
diff --git a/help/Writerside/images/内存表映射关系.png b/help/Writerside/images/内存表映射关系.png
new file mode 100644
index 0000000..f86ad68
Binary files /dev/null and b/help/Writerside/images/内存表映射关系.png differ
diff --git a/help/Writerside/images/删除.png b/help/Writerside/images/删除.png
deleted file mode 100644
index b9c4720..0000000
Binary files a/help/Writerside/images/删除.png and /dev/null differ
diff --git a/help/Writerside/images/功能名称查询.png b/help/Writerside/images/功能名称查询.png
deleted file mode 100644
index c50ae0c..0000000
Binary files a/help/Writerside/images/功能名称查询.png and /dev/null differ
diff --git a/help/Writerside/images/功能名称查询uft3.png b/help/Writerside/images/功能名称查询uft3.png
deleted file mode 100644
index c1860c4..0000000
Binary files a/help/Writerside/images/功能名称查询uft3.png and /dev/null differ
diff --git a/help/Writerside/images/双击添加.png b/help/Writerside/images/双击添加.png
deleted file mode 100644
index cdc63de..0000000
Binary files a/help/Writerside/images/双击添加.png and /dev/null differ
diff --git a/help/Writerside/images/按需转码.png b/help/Writerside/images/按需转码.png
new file mode 100644
index 0000000..e30fd89
Binary files /dev/null and b/help/Writerside/images/按需转码.png differ
diff --git a/help/Writerside/images/模糊查询UFT3.png b/help/Writerside/images/模糊查询UFT3.png
deleted file mode 100644
index 8048c7e..0000000
Binary files a/help/Writerside/images/模糊查询UFT3.png and /dev/null differ
diff --git a/help/Writerside/images/模糊查询uf20.png b/help/Writerside/images/模糊查询uf20.png
deleted file mode 100644
index cab0d22..0000000
Binary files a/help/Writerside/images/模糊查询uf20.png and /dev/null differ
diff --git a/help/Writerside/images/添加函数.png b/help/Writerside/images/添加函数.png
deleted file mode 100644
index 7d04d98..0000000
Binary files a/help/Writerside/images/添加函数.png and /dev/null differ
diff --git a/help/Writerside/images/精准查询uf20.png b/help/Writerside/images/精准查询uf20.png
deleted file mode 100644
index 914be36..0000000
Binary files a/help/Writerside/images/精准查询uf20.png and /dev/null differ
diff --git a/help/Writerside/images/精准查询uft3.png b/help/Writerside/images/精准查询uft3.png
deleted file mode 100644
index d969122..0000000
Binary files a/help/Writerside/images/精准查询uft3.png and /dev/null differ
diff --git a/help/Writerside/images/编辑.png b/help/Writerside/images/编辑.png
deleted file mode 100644
index d3eeed6..0000000
Binary files a/help/Writerside/images/编辑.png and /dev/null differ
diff --git a/help/Writerside/images/设置.png b/help/Writerside/images/设置.png
new file mode 100644
index 0000000..1e14013
Binary files /dev/null and b/help/Writerside/images/设置.png differ
diff --git a/help/Writerside/images/转码.png b/help/Writerside/images/转码.png
deleted file mode 100644
index 487591e..0000000
Binary files a/help/Writerside/images/转码.png and /dev/null differ
diff --git a/help/Writerside/redirection-rules.xml b/help/Writerside/redirection-rules.xml
index 44f1704..a852af6 100644
--- a/help/Writerside/redirection-rules.xml
+++ b/help/Writerside/redirection-rules.xml
@@ -10,4 +10,8 @@
Created after removal of "Empty MD Topic" from Uft30ChangeCode
Empty-MD-Topic.html
+
+ Created after removal of "按需转码" from Uft30ChangeCode
+ 按需转码.html
+
\ No newline at end of file
diff --git a/help/Writerside/topics/UF20-内存表映射查询.md b/help/Writerside/topics/UF20-内存表映射查询.md
new file mode 100644
index 0000000..cc6fa85
--- /dev/null
+++ b/help/Writerside/topics/UF20-内存表映射查询.md
@@ -0,0 +1,19 @@
+
+# UF20 & 内存表映射查询
+
+检索 UF20(2.0)表与内存表之间的映射关系,结果四列: UF20表名 、 UF20表中文名 、 内存表名 、 内存表中文名 ;双击结果行可添加映射到「数据映射 → 表替换」
+
+
+
+**操作步骤:**
+1. 进入 功能查询 → UF20 & 内存表映射查询;
+
+2. 输入 UF20 表名或内存表名(二者均可检索),回车或点搜索图标;
+ - 批量查询:多个表名用逗号、分号或空格分隔(中英文均可),各关键词命中结果取并集展示;
+
+ - 搜索框内可切换精确匹配(默认模糊匹配,不区分大小写);
+
+3. 双击结果行将该映射关系添加到「数据映射 → 表替换」:
+ - 仅当该行 UF20 表名与内存表名均非空(有完整映射关系)才可添加;
+
+ - 表替换中已存在该 UF20 表名时自动跳过,并在提示栏注明当前映射值(不覆盖);
\ No newline at end of file
diff --git a/help/Writerside/topics/UF20功能查询.md b/help/Writerside/topics/UF20功能查询.md
index 43d6033..ee8e0df 100644
--- a/help/Writerside/topics/UF20功能查询.md
+++ b/help/Writerside/topics/UF20功能查询.md
@@ -5,20 +5,23 @@
## 查询条件
从下拉框选择查询(功能名称、功能编号),输入框输入查询关键词,点击"Aa"切换精确/模糊匹配模式,点击放大镜图标或按回车执行查询
-- 功能名称
-
+
-- 功能编号
-
-
+**操作步骤:**
+1. 进入 UF20功能查询 页;
+2. 左侧下拉选择检索字段:函数名 / 功能号,输入关键字回车或点搜索图标;
+ 批量查询:多个关键词用逗号、分号或空格分隔(中英文均可),如 LS_证券参数, LS_股票
+ 名单 ;各关键词命中结果取并集展示;
+ 单个关键词可命中同一功能的多个字段,结果按"函数名+功能号"自动去重;
+3. 搜索框内可切换精确匹配(默认模糊匹配);
+4. 查看结果列表,列: 函数名 、 英文名 、 功能号 ;
+5. 双击或右键选中行一键加入转码工作台:LS_ 开头功能 → 全量转码;其它功能 → 按需转码;
+ - 支持 Ctrl/Shift 多选后一次加入;右键行不在选区内时仅加入该行;
-## 结果反馈
-查询提示在页面底部 tip 栏显示,不会弹出对话框打断操作:
+ - 加入结果只弹一个提示框("全量转码"、"按需转码"字样加粗):同时包含两类功能时显
+ 示"已将 N 个功能加入全量转码,已将 M 个功能加入按需转码";重复添加的功能自动跳过并
+ 在提示中注明;全部已存在时提示"均已在转码列表中"。
-- 输入为空 → 提示"请输入查询内容"
-- 无匹配结果 → 提示"⚠️ 未找到匹配的功能"
-- 查询成功 → 表格展示结果,tip 栏恢复默认操作提示
-
-## 添加到转码工作台
-双击或右键选中行加入转码工作台:LS_ 开头的功能进入"全量转码",其它功能进入"按需转码"
-
+**查询提示:** 查询反馈显示在页面底部提示栏(颜色随日间/夜间主题自动适配),不弹对话框打断操作
+——输入为空提示"请输入查询内容";无匹配结果提示"⚠️ 未找到匹配的功能";查询成功后显示"已查
+询 X 个业务功能函数(M 个无匹配)"并接默认操作提示。
diff --git a/help/Writerside/topics/UFT3功能查询.md b/help/Writerside/topics/UFT3功能查询.md
index 71b0526..19004be 100644
--- a/help/Writerside/topics/UFT3功能查询.md
+++ b/help/Writerside/topics/UFT3功能查询.md
@@ -5,17 +5,13 @@
## 查询条件
从下拉框选择查询(功能名称、功能编号),输入框输入查询关键词,点击"Aa"切换精确/模糊匹配模式,点击放大镜图标或按回车执行查询
-- 功能名称
-
-- 功能编号
-
-
+
+
+- 界面与 UF20 功能查询一致(函数名 / 功能号检索、批量关键词、精确匹配),结果列为 函数名 、 英文名 、 功能号 ,查询提示同样在页面底部提示栏反馈;
+
+- 仅用于查询核对 UFT3 功能元数据,双击/右键不加入转码清单;
+
+
-## 结果反馈
-查询提示在页面底部 tip 栏显示,不会弹出对话框打断操作:
-- 输入为空 → 提示"请输入查询内容"
-- 无匹配结果 → 提示"⚠️ 未找到匹配的功能"
-- 查询成功 → 表格展示结果,tip 栏恢复默认提示
-> 本页仅提供查询参考,结果行不支持双击加入转码工作台。
diff --git a/help/Writerside/topics/update-1-6-0.md b/help/Writerside/topics/update-1-6-0.md
deleted file mode 100644
index 043ddc1..0000000
--- a/help/Writerside/topics/update-1-6-0.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# 1.6.0
-
-1.转码逻辑优化;
-
-2.元数据更新逻辑优化;
-
-3.元数据更新存储修改为db存储模式;
-
-4.写日志异常修复;
diff --git a/help/Writerside/topics/update-2-0-0.md b/help/Writerside/topics/update-2-0-0.md
new file mode 100644
index 0000000..35cb77a
--- /dev/null
+++ b/help/Writerside/topics/update-2-0-0.md
@@ -0,0 +1,12 @@
+# 2.0.0
+
+1.新增按需转码功能,支持核心功能转码;
+
+2.新增uf20和uft3表关系映射查找功能;
+
+3.支持批量查询和添加;
+
+4.元数据更新存储修改为db存储模式;
+
+5.写日志异常修复
+
diff --git a/help/Writerside/topics/下载.md b/help/Writerside/topics/下载.md
index 9f17763..3dc2293 100644
--- a/help/Writerside/topics/下载.md
+++ b/help/Writerside/topics/下载.md
@@ -5,6 +5,9 @@
> 1.0.0版本是基础版本,包含所需功能,如果是第一次下载,请选择全量包。
{style="note"}
+
+> 2.0.0版本是新的版本,不能增量替换需要下载全量包
+{style="note"}
## 增量更新
下载增量包,解压后替换原文件即可
> 1.0.0版本是基础版本,如若要从旧版升级到1.0.0及以上版本,可以选择增量版本下载,并将旧版根目录下的uf2touft3和当前目录替换覆盖到新版根目录。
@@ -12,13 +15,13 @@
## 下载
-| 版本 | 更新日期 | 全量包 | 增量包 |
-|-------|------------|----------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
-| 1.6.0 | 2026-09-18 | [Uft30ChangeCode-1.6.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.6.0.zip) | [Uft30ChangeCode-1.6.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.6.0-update.zip) |
+| 版本 | 更新日期 | 全量包 | 增量包 |
+|-------|------------|----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
+| 1.2.0 | 2026-10-19 | [Uft30ChangeCode-2.0.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-2.0.0.zip) | |
| 1.5.0 | 2026-08-07 | [Uft30ChangeCode-1.5.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.5.0.zip) | [Uft30ChangeCode-1.5.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.5.0-update.zip) |
| 1.4.0 | 2026-07-21 | [Uft30ChangeCode-1.4.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.4.0.zip) | [Uft30ChangeCode-1.4.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.4.0-update.zip) |
| 1.3.0 | 2026-07-07 | [Uft30ChangeCode-1.3.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.3.0.zip) | [Uft30ChangeCode-1.3.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.3.0-update.zip) |
| 1.2.0 | 2026-07-02 | [Uft30ChangeCode-1.2.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.2.0.zip) | [Uft30ChangeCode-1.2.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.2.0-update.zip) |
| 1.1.0 | 2026-07-01 | [Uft30ChangeCode-1.1.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.1.0.zip) | [Uft30ChangeCode-1.1.0-update.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.1.0-update.zip) |
-| 1.0.0 | 2026-06-01 | [Uft30ChangeCode-1.0.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.0.0.zip) | |
+| 1.0.0 | 2026-06-01 | [Uft30ChangeCode-1.0.0.zip](http://10.20.163.105:6045/download/uft3changecode/Uft30ChangeCode-1.0.0.zip) | |
diff --git a/help/Writerside/topics/不转码功能函数.md b/help/Writerside/topics/不转码功能函数.md
index 9a0e45d..66a43f6 100644
--- a/help/Writerside/topics/不转码功能函数.md
+++ b/help/Writerside/topics/不转码功能函数.md
@@ -2,29 +2,13 @@
维护一份功能全名清单:转码时清单中的功能全名**保持原名不做替换**。入口:左侧导航"数据映射" > "不转码功能函数"。
-## 添加条目
+
-点击"添加"按钮,在弹窗中输入功能全名(如 `AF_用户公用_用户可操作分支获取`)后确定。
-- 功能全名不能为空;
-- 不允许与已有条目重复,重复时提示后不会加入。
-
-## 编辑与删除
-
-双击表格单元格可直接修改条目。
-
-选中行后点击"删除选中",确认后移除所选条目。
-
-## 保存
-
-点击"保存"按钮将清单写回 `bin/uf2touft3/config/cust.json`。
-
-保存前会校验:
-
-- 存在空条目时报错并中止,需填写或删除该行;
-- 存在重复条目时报错并中止,需修改后再保存。
-
-## 配置文件同步
-
-- 清单持久化于 `cust.json` 的不转码功能函数数组中,与其他数据映射配置共用同一文件;
-- `cust.json` 被外部修改时,清单自动刷新(未保存的修改会被覆盖,请先保存再改外部文件)。
+**操作步骤:**
+1. 进入 数据映射 → 不转码功能函数;
+2. 顶部搜索框实时过滤条目(不区分大小写);
+3. 双击单元格直接修改;
+4. 点 添加 弹窗输入功能函数名(必填,重复时保存被拦截提示);
+5. 删除选中 需二次确认(支持多选);
+6. 修改后点 保存 生效;
\ No newline at end of file
diff --git a/help/Writerside/topics/业务转码.md b/help/Writerside/topics/业务转码.md
index 8cf7b9b..5a20f28 100644
--- a/help/Writerside/topics/业务转码.md
+++ b/help/Writerside/topics/业务转码.md
@@ -1,36 +1,53 @@
# 业务转码
-将UF20函数转换为UFT3函数格式的核心功能。
+业务转码页顶部通过 Tab 切换 全量转码 / 按需转码 两个工作台。
-## 功能列表管理
+## 全量转码
+
-点击"添加"按钮,输入函数名添加到转码列表
-
+**说明:** 全量转码仅支持 LS_ 开头的功能("内存交易"改造范围),按清单对功能进行整体转码。
-双击表格中函数名所在行,可在弹窗中修改函数名(页面顶部有操作提示),如下:
-
+**表格列:** UF20函数名 | 是否存在 | 核心 | 是否柜面接口 (双击函数名所在行可在弹窗中修改函数
+名)。
+- "是否存在"显示 否:说明元数据未更新或该功能在 UF20 元数据中检索不到;
-选中行后点击"删除选中"移除函数、点击"清空列表"清空所有函数,如下:
-
+- 核心:下拉选择该功能转码后归属的核心,可选 ucbp(默认)/ uses / uarg / usms;SQL 方言 由核心自动派生——ucbp/uses 对应 mdb,uarg/usms 对应 db;
-## 核心 与 是否柜面接口
+- 是否柜面接口:下拉选择 否(默认)/ 是,标识该功能是否为柜面接口;
-每行提供"核心"与"是否柜面接口"两个下拉框:
+- 每次显示页面时自动刷新"是否存在"列。
-- **核心**:选择该功能转码后归属的核心,可选 ucbp(默认)/ uses / uarg / usms;SQL 方言由核心自动派生——ucbp/uses 对应 mdb,uarg/usms 对应 db;
-- **是否柜面接口**:选择 否(默认)/ 是,标识该功能是否为柜面接口。
+**操作步骤:**
+1. 切到 全量转码 Tab;
+2. 点击 添加,弹窗内每行输入一个 LS_ 函数名(支持批量粘贴);输入非 LS 函数将被拒绝,并提示
+切换到"按需转码";
+3. 也可在 UF20功能查询 中双击或右键选中 LS_ 功能直接加入;
+4. 双击函数名所在行可在弹窗中修改函数名;删除选中 / 清空列表(二次确认)维护清单;
+5. 点击 开始转换,进度条与状态文字显示转换进度;转换中按钮变为 停止转换,可随时点击终止任务。
-切换后立即写入配置并生效。
+## 按需转码
+
-## 配置文件同步
+**说明:** 按需转码针对非 LS 功能转码,支持单文件转码与递归转码两种模式,转码范围不包含核心文件、module 模块文件。
-- 自动读取 bin/uf2touft3/config/cust.json 配置(convert.dir 为对象数组 {name, core, viaUmgr, dialect},兼容旧版字符串数组格式);
-- 配置文件外部修改时自动刷新列表
+- 非递归模式:仅对目标文件本身执行转码,不会处理该文件内调用的函数;
-## 转换操作
+- 递归模式:转码目标文件的同时,自动对文件中所调用的关联函数一并完成转码。
-点击"开始转换"启动转换任务,转换过程中可查看进度条和状态信息,如下:
-
-点击"停止转换"中止当前任务,如下:
-
\ No newline at end of file
+**添加功能(三种方式):**
+- 方式一(推荐):进入 UF20功能查询,双击或右键目标非 LS 函数(支持多选后右键批量加入)→ 弹窗提示"已将 N 个功能加入按需转码";
+
+- 方式二:按需转码 Tab 点击 添加,弹窗内每行输入一个函数短名(如 AS_xxx );误带 .service_design 等后缀会自动剥离;新增功能递归模式默认 否;若输入 LS_ 开头函数仅给出提示(建议放入全量转码),不会拦截;
+
+- 方式三:手工编辑配置文件 cust_nonls.json (见 §6.2),保存后程序自动检测并刷新界面。
+
+**表格列:** UF20函数名 | 是否存在 | 是否递归 | db模式 (双击函数名所在行可在弹窗中修改函数名)。
+- LS_ 开头的函数名以橙色字体显示提醒(不拦截)。
+
+**设置递归模式与 db 模式:**
+1. 在目标行的 是否递归 下拉框选择 否(单文件)或 是(递归);
+2. 在 db模式 下拉框选择 SQL 语法转换方言:否 = mdb(默认)、是 = db;
+3. 选择后立即写入 cust_nonls.json 并生效,无需额外点击。
+
+**执行转码:** 点击 开始转换,进度显示与停止方式同全量转码;任务互斥规则相同。
\ No newline at end of file
diff --git a/help/Writerside/topics/元数据更新.md b/help/Writerside/topics/元数据更新.md
index 1f6a20b..57f4690 100644
--- a/help/Writerside/topics/元数据更新.md
+++ b/help/Writerside/topics/元数据更新.md
@@ -13,12 +13,25 @@
``E:\05.code\04.SVN\secu\dep1\hstrade20\trunk\Sources\DevCodes\经纪业务运营平台V21``
-## UFT3元数据
-在 元数据更新 页面,选择更新范围(数据类型、标准字段、表结构、接口数据)并点击"更新UFT30"按钮完成元数据更新。
-
-**备注:接口数据更新比较久**
+## 元数据更新
+
-## 更新UF20元数据
-在 元数据更新 页面,选择更新范围(数据类型、标准字段、标准错误号、接口数据、常量、菜单功能、模块、表结构)并点击"更新UF20"按钮完成公共资源更新
-
-**备注:接口数据更新比较久**
+**操作步骤:**
+1. 左侧导航进入 元数据更新;
+2. 页面分为 UFT30更新范围 与 UF20更新范围 两个卡片,勾选需要更新的元数据项(默认全选):
+ - UFT30:数据类型、标准字段、表结构、接口数据;
+
+ - UF20:数据类型、标准字段、标准错误号、接口数据、常量、菜单功能、模块、表结构;
+3. 可用 全选 / 全不选 按钮快速调整勾选范围;
+4. 点击 更新UFT30 或 更新UF20,页面显示进度与当前处理项;
+5. 等待完成提示后,再使用其他功能页面。
+
+**选项说明:**
+ - 增量快速跳过:增量更新先对比源码目录整体修改时间,源码无任何变化时秒级完成,不再逐文件重复扫描;
+
+ - 强制全量更新(进度条右上角复选框):默认增量更新,仅重新处理有变化的文件;勾选后将重新
+ 扫描并解析全部接口文件、重建增量缓存,耗时较久,勾选时需二次确认;
+
+ - 源码变更后请重新执行更新,保证索引与实际代码一致;
+
+ - 更新过程在后台线程执行,界面不会卡死。
diff --git a/help/Writerside/topics/函数替换.md b/help/Writerside/topics/函数替换.md
index d8a9114..0760872 100644
--- a/help/Writerside/topics/函数替换.md
+++ b/help/Writerside/topics/函数替换.md
@@ -1,5 +1,5 @@
# 函数替换
-uf2函数和uft3函数之间的映射关系,支持增、删、改、查功能
+维护 UF20 函数名 → UFT3 函数名的映射,操作方式与表替换相同,支持增、删、改、查功能
\ No newline at end of file
diff --git a/help/Writerside/topics/按需转码.md b/help/Writerside/topics/按需转码.md
deleted file mode 100644
index 8f479d0..0000000
--- a/help/Writerside/topics/按需转码.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# 按需转码
-
-针对**非 LS 功能**的按需转码工作台。入口:左侧导航"业务转码"页,顶部标签切换到"按需转码"。
-
-转码范围不包含核心文件、module 模块文件。支持两种模式:
-
-- **非递归模式**:仅对目标文件本身执行转码,不会处理该文件内调用的函数;
-- **递归模式**:转码目标文件的同时,自动对文件中所调用的关联函数一并完成转码。
-
-## 功能列表管理
-
-点击"添加"按钮,在弹窗中每行输入一个函数名(短名,如 AS_xxx),支持批量粘贴。若误带设计器扩展名后缀(如 .service_design)会自动剥离。
-
-对于以 LS_ 开头的函数,界面会以黄色文字提示,并弹窗建议放入"全量转码"工作台转码(不拦截添加)。
-
-选中行后点击"删除选中"移除函数、点击"清空列表"清空所有函数。双击函数名所在行,可在弹窗中修改函数名(页面顶部有操作提示)。
-
-## 递归模式 与 db模式
-
-每行提供"是否递归"与"db模式"下拉框:
-
-- **是否递归**:
- - **否**:非递归模式(默认);
- - **是**:递归模式,自动转码关联函数。
-- **db模式**:选择 SQL 语法转换方言:
- - **否**:mdb 方言(默认);
- - **是**:db 方言。
-
-切换后立即生效并保存。
-
-## 配置文件同步
-
-- 自动读取 `bin/uf2touft3/config/cust_nonls.json` 配置:"是否递归"对应 convert.dir 条目的 `depth`(0=否、-1=是),"db模式"对应 `dialect`(mdb=否、db=是);
-- 兼容旧版清单格式(纯函数名数组或数值条目,均按"否/mdb"加载);
-- 配置文件外部修改时自动刷新列表。
-
-## 转换操作
-
-点击"开始转换"启动转换任务,转换过程中可查看进度条和状态信息;点击"停止转换"可中止当前任务。
-
-工具以按需转码模式调用引擎,函数清单、递归开关与 SQL 方言由引擎直接读取 `cust_nonls.json`,无需手动同步。
diff --git a/help/Writerside/topics/数据映射.md b/help/Writerside/topics/数据映射.md
index 4f56468..3b42e37 100644
--- a/help/Writerside/topics/数据映射.md
+++ b/help/Writerside/topics/数据映射.md
@@ -3,4 +3,6 @@
## 快速跳转
[表替换](http://10.20.163.105:6045/uft3changecode/表替换.html)
-[函数替换](http://10.20.163.105:6045/uft3changecode/函数替换.html)
\ No newline at end of file
+[函数替换](http://10.20.163.105:6045/uft3changecode/函数替换.html)
+
+[不转码功能函数](不转码功能函数.md)
\ No newline at end of file
diff --git a/help/Writerside/topics/更新记录.md b/help/Writerside/topics/更新记录.md
index df8bbaf..3f5cd9a 100644
--- a/help/Writerside/topics/更新记录.md
+++ b/help/Writerside/topics/更新记录.md
@@ -1,6 +1,6 @@
# 更新记录
-- [1.6.0](update-1-6-0.md):转码逻辑优化、元数据更新逻辑优化、元数据更新存储修改为db存储模式、写日志异常修复
+- [2.0.0](update-2-0-0.md):转码逻辑优化、元数据更新逻辑优化、元数据更新存储修改为db存储模式、写日志异常修复
- [1.5.0](update-1-5-0.md):解决转码遇到公共已转函数导致失败部分接口转码失败问题
- [1.4.0](update-1-4-0.md):业务转码性能提升、元数据更新支持增量/强制全量、新增代码检索与调用链分析、全局文字可选中复制
- [1.3.0](update-1-3-0.md):修复设置路径不生效、修复元数据更新后函数检查状态异常
diff --git a/help/Writerside/topics/表替换.md b/help/Writerside/topics/表替换.md
index a48b318..b2eaab4 100644
--- a/help/Writerside/topics/表替换.md
+++ b/help/Writerside/topics/表替换.md
@@ -1,6 +1,19 @@
# 表替换
-uf2表和uft3表之间的映射关系,支持增、删、改、查功能
+维护 UF20 表名 → UFT3 表名的映射,支持增、删、改、查功能
+**操作步骤:**
+1. 进入 数据映射 → 表替换;
+2. 顶部搜索框实时过滤两列内容(不区分大小写);
+3. 双击单元格直接修改;
+4. 点 添加 弹窗输入 UF20 / UFT3 表名(UF20 表名必填且不可重复);
+5. 删除选中 需二次确认(支持多选);
+6. 修改后点 保存 生效;保存成功后自动与映射文件立即可查:
+ - 新增的映射自动追加到该文件;
+
+ - 修改一对一映射的内存表名后,文件中同名条目的值同步更新;
+
+ - 该文件中配置为一对多数组的条目(一个 UF20 表对应多个内存表)保持不变,不会被表替换中的单值覆盖;
+
diff --git a/help/Writerside/topics/设置.md b/help/Writerside/topics/设置.md
index 3a86e5a..4ceedb4 100644
--- a/help/Writerside/topics/设置.md
+++ b/help/Writerside/topics/设置.md
@@ -1,5 +1,7 @@
# 设置
+
+
## UFT30项目环境路径
在设置页面,配置UFT30项目路径,转码使用,例如:
diff --git a/src/metadataupdate/metadataprocessor.cpp b/src/metadataupdate/metadataprocessor.cpp
index b78fad5..124ccc2 100644
--- a/src/metadataupdate/metadataprocessor.cpp
+++ b/src/metadataupdate/metadataprocessor.cpp
@@ -257,7 +257,7 @@ double MetadataProcessor::dirMtimeCached(const QString& path)
bool MetadataProcessor::saveRowsToDb(const QString& db, const QString& table,
const QJsonObject& resultJson, double srcMtime,
- const QJsonArray& srcMtimes)
+ const QJsonArray& srcMtimes, bool emptyOverwrite)
{
QMap rows;
for (auto it = resultJson.begin(); it != resultJson.end(); ++it) {
@@ -273,9 +273,18 @@ bool MetadataProcessor::saveRowsToDb(const QString& db, const QString& table,
}
}
if (rows.isEmpty()) {
- LogManager::instance()->logWarning(
- QString("元数据落库跳过 - %1.%2 为空(源路径未配置或目录不存在?)").arg(db, table));
- return true; // 空数据集跳过不算失败(解析可能确实无数据)
+ // 空数据集语义分两种:
+ // - 源目录不存在/未配置(src_mtime 总和为 0): 属配置问题而非源内容变化,
+ // 跳过落库保留旧数据(此时落空表会误删有效缓存);
+ // - 源存在但扫描结果为空(src_mtime>0 且调用方允许 emptyOverwrite):
+ // 源内容确实为空, 落空表覆盖上一轮残留行(旧行残留会持续出现在搜索结果中)
+ if (!(emptyOverwrite && srcMtime > 0.0)) {
+ LogManager::instance()->logWarning(
+ QString("元数据落库跳过 - %1.%2 为空(源路径未配置或目录不存在?)").arg(db, table));
+ return true; // 空数据集跳过不算失败(解析可能确实无数据)
+ }
+ LogManager::instance()->logInfo(
+ QString("元数据落库 - %1.%2 扫描结果为空且源已变更, 落空表覆盖残留行").arg(db, table));
}
// 注意: 此处不可调用 DataCache::initDatabase() 做前置可行性判断。
// initDatabase() 是"只读探测"(库文件不存在时返回 false), 若在此依赖它,
@@ -371,8 +380,10 @@ bool MetadataProcessor::processInterfaces(IInterfaceScanner* scanner,
srcMtime += v.toDouble();
}
- // 无变化快速跳过: 不勾选"强制全量更新"时, 对比源目录 mtime 与库中上次成功落库记录
- if (trySkipUnchanged(db, interfaceTable, srcMtimes, srcMtime, cacheKey)) {
+ // 无变化快速跳过: 不勾选"强制全量更新"时, 对比源目录 mtime 与库中上次成功落库记录;
+ // UFT3 时同时校验转码表(独立事务落库, 主表成功而副表失败的上一轮残局必须重扫补齐)
+ if (trySkipUnchanged(db, interfaceTable, srcMtimes, srcMtime, cacheKey,
+ isUft3 ? QString(Constants::Cache::TblUft3TransCode) : QString())) {
return true;
}
@@ -393,13 +404,35 @@ bool MetadataProcessor::processInterfaces(IInterfaceScanner* scanner,
// 无变化快速跳过判断: 逐目录 mtime 精确比较优先, 老记录(无逐目录列)回退按总和比较。
// 返回 true 表示源未变化可直接跳过本次扫描; 强制全量/已变更/从未落库返回 false。
+// secondTable 非空时(UFT3 转码表)额外要求副表 mtime 也与源一致才允许跳过
bool MetadataProcessor::trySkipUnchanged(const QString& db, const QString& table,
const QJsonArray& srcMtimes, double srcMtime,
- const QString& cacheKey)
+ const QString& cacheKey, const QString& secondTable)
{
if (m_forceFull) {
return false;
}
+ if (!isTableUnchanged(db, table, srcMtimes, srcMtime, cacheKey)) {
+ return false;
+ }
+ // 副表独立事务落库: 上一轮可能主表成功而副表失败/落库阶段被取消,
+ // 只看主表 mtime 会直接跳过重扫, 旧转码行永久残留并被 buildFunctionCache
+ // 并入功能缓存(过期功能号持续出现在搜索结果中)。副表无记录或 mtime
+ // 与源不一致时强制重扫补齐
+ if (!secondTable.isEmpty()
+ && !isTableUnchanged(db, secondTable, srcMtimes, srcMtime, cacheKey)) {
+ LogManager::instance()->log(
+ QString("%1 副表(%2) mtime 与源不一致或无记录, 重新扫描补齐").arg(cacheKey, secondTable));
+ return false;
+ }
+ return true;
+}
+
+// 单表 mtime 无变化比较: 库中记录与本次采样源逐目录一致(或老记录总和一致)返回 true
+bool MetadataProcessor::isTableUnchanged(const QString& db, const QString& table,
+ const QJsonArray& srcMtimes, double srcMtime,
+ const QString& cacheKey)
+{
const QJsonArray lastMtimes = DataCache::instance()->getMetaSrcMtimes(db, table);
if (!lastMtimes.isEmpty()) {
// 新记录(有逐目录 mtime): 目录个数不同(如 dbsett 开关切换)或任一目录
@@ -457,7 +490,9 @@ bool MetadataProcessor::saveInterfaceRows(bool isUft3, const QString& db, const
{
bool ok = true;
if (isUft3) {
- ok = saveRowsToDb(Constants::Cache::Uft3DbName, Constants::Cache::TblUft3Interface, resultJson, srcMtime, srcMtimes) && ok;
+ // 接口表 emptyOverwrite=true: 源存在(src_mtime>0)但扫描 0 接口(如目录被清空)
+ // 时落空表覆盖旧行; 源不存在(src_mtime=0)仍跳过, 保留有效缓存
+ ok = saveRowsToDb(Constants::Cache::Uft3DbName, Constants::Cache::TblUft3Interface, resultJson, srcMtime, srcMtimes, true) && ok;
if (!transCodeJson.isEmpty()) {
ok = saveRowsToDb(Constants::Cache::Uft3DbName, Constants::Cache::TblUft3TransCode, transCodeJson, srcMtime, srcMtimes) && ok;
} else {
@@ -468,7 +503,7 @@ bool MetadataProcessor::saveInterfaceRows(bool isUft3, const QString& db, const
QMap(), srcMtime, srcMtimes, &m_cancelRequested) && ok;
}
} else {
- ok = saveRowsToDb(Constants::Cache::Uf20DbName, Constants::Cache::TblUf2Interface, resultJson, srcMtime, srcMtimes) && ok;
+ ok = saveRowsToDb(Constants::Cache::Uf20DbName, Constants::Cache::TblUf2Interface, resultJson, srcMtime, srcMtimes, true) && ok;
}
if (ok) {
diff --git a/src/metadataupdate/metadataprocessor.h b/src/metadataupdate/metadataprocessor.h
index 8e12fed..e8cadb6 100644
--- a/src/metadataupdate/metadataprocessor.h
+++ b/src/metadataupdate/metadataprocessor.h
@@ -76,11 +76,21 @@ private:
// srcMtime 为源目录最新修改时间(多源时相加), 写入 _meta.src_mtime 供缓存失效判断;
// srcMtimes 非空时逐目录 mtime 数组一并写入 _meta.src_mtimes(多源数据集逐目录比较,
// 防 mtime 相加抵消导致"无变化"误判漏更新)。
+ // emptyOverwrite: 空数据集语义开关 — false(默认)空集跳过落库保留旧数据(配置问题场景);
+ // true 且 src_mtime>0(源存在但扫描结果为空)落空表覆盖上一轮残留行(接口表场景)
bool saveRowsToDb(const QString& db, const QString& table, const QJsonObject& resultJson,
- double srcMtime = 0.0, const QJsonArray& srcMtimes = QJsonArray());
+ double srcMtime = 0.0, const QJsonArray& srcMtimes = QJsonArray(),
+ bool emptyOverwrite = false);
- // 无变化快速跳过判断(详见 .cpp): 源未变化返回 true, 强制全量/已变更/从未落库返回 false
+ // 无变化快速跳过判断(详见 .cpp): 源未变化返回 true, 强制全量/已变更/从未落库返回 false。
+ // secondTable 非空时(UFT3 传转码表)额外校验副表 mtime: 副表独立事务落库,
+ // 上一轮可能主表成功而副表失败, 副表不一致时强制重扫补齐防旧行残留
bool trySkipUnchanged(const QString& db, const QString& table,
+ const QJsonArray& srcMtimes, double srcMtime, const QString& cacheKey,
+ const QString& secondTable = QString());
+
+ // 单表 mtime 无变化比较(逐目录优先, 老记录回退总和比较), trySkipUnchanged 复用
+ bool isTableUnchanged(const QString& db, const QString& table,
const QJsonArray& srcMtimes, double srcMtime, const QString& cacheKey);
// 全量扫描 + 行转换: 结果填入 resultJson 与 transCodeJson(仅 UFT3 非空)
diff --git a/src/metadataupdate/uft30/uft3interface.cpp b/src/metadataupdate/uft30/uft3interface.cpp
index f8c743b..343169f 100644
--- a/src/metadataupdate/uft30/uft3interface.cpp
+++ b/src/metadataupdate/uft30/uft3interface.cpp
@@ -132,11 +132,15 @@ std::optional Uft3Interface::loadInterface(const QString& filePat
}
}
- // XML 格式错误时循环静默截断, 接口字段/代码不完整, 必须落日志
+ // XML 格式错误时解析截断, 接口字段/代码不完整: 落日志并放弃该文件,
+ // 严禁残缺接口以文件名 cname 兜底入库污染 uft3_interface/uft3_trans_code 表
+ // (对齐 UF2 侧语义: 解析失败返回 nullopt, 由 scanDirRecursive 跳过并计入失败汇总)
if (xml.hasError()) {
- LogManager::instance()->logError(QString("接口 XML 解析失败: %1 (%2)").arg(sFilePath, xml.errorString()));
+ LogManager::instance()->logError(QString("接口 XML 解析失败, 该文件已跳过: %1 (%2)").arg(sFilePath, xml.errorString()));
+ file.close();
+ return std::nullopt;
}
-
+
file.close();
return result;
diff --git a/src/pages/batchconvert/batchconvertpage.cpp b/src/pages/batchconvert/batchconvertpage.cpp
index eefb6e5..e6622b6 100644
--- a/src/pages/batchconvert/batchconvertpage.cpp
+++ b/src/pages/batchconvert/batchconvertpage.cpp
@@ -1,5 +1,6 @@
#include "batchconvertpage.h"
#include "src/utils/Constants.h"
+#include "src/utils/configmanager.h"
#include "src/utils/logmanager.h"
#include "src/utils/custjsonio.h"
#include
@@ -12,6 +13,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -21,7 +23,7 @@
namespace {
// V2 默认值(契约 详细设计文档.md §7.5): core 默认 ucbp, 柜面接口默认否;
-// dialect 不落 UI, 保存时由 core 派生(ucbp/uses→mdb, uarg/usms→db)
+// dialect 不落 UI, 保存时由 core 派生(∈dbcore→db, 其余→mdb)
bool validateLsFunctionNames(QWidget *parent, const QStringList &functionNames)
{
@@ -51,18 +53,65 @@ bool validateLsFunctionNames(QWidget *parent, const QStringList &functionNames)
enum TableCol {
ColName = 0, // UF20函数名
ColExists = 1, // 是否存在
- ColCore = 2, // 核心(ucbp/uses/uarg/usms)
+ ColCore = 2, // 核心(取值来自 config.ini [coreConfig])
ColViaUmgr = 3 // 是否柜面接口
};
-// 核心取值表(顺序即下拉顺序, 首项为默认值)
-const QStringList kCoreList = {Constants::DirEntry::CoreUcbp, Constants::DirEntry::CoreUses,
- Constants::DirEntry::CoreUarg, Constants::DirEntry::CoreUsms};
+// config.ini [coreConfig] 缺键/空清单时的回退默认(对齐历史硬编码清单)
+const QStringList kFallbackMdbCores = {QStringLiteral("ucbp"), QStringLiteral("uses")};
+const QStringList kFallbackDbCores = {QStringLiteral("uarg"), QStringLiteral("usms")};
-int coreIndexOf(const QString &core)
+// 逗号分隔清单解析: 去首尾空白、丢弃空项
+QStringList splitCoreCsv(const QString &raw)
{
- const int index = kCoreList.indexOf(core);
- return index >= 0 ? index : 0; // 未知核心回退默认首项(ucbp)
+ QStringList cores;
+ const QStringList parts = raw.split(QLatin1Char(','), Qt::SkipEmptyParts);
+ for (const QString &part : parts) {
+ const QString trimmed = part.trimmed();
+ if (!trimmed.isEmpty()) {
+ cores.append(trimmed);
+ }
+ }
+ return cores;
+}
+
+// mdb 方言核心清单(config.ini [coreConfig] mdbcore, 缺键回退内置默认)
+QStringList mdbCoreList()
+{
+ return splitCoreCsv(ConfigManager::instance()->getValue(
+ Constants::ConfigKeys::SectionCoreConfig, Constants::ConfigKeys::KeyMdbCore,
+ kFallbackMdbCores.join(QLatin1Char(','))));
+}
+
+// db 方言核心清单(config.ini [coreConfig] dbcore, 缺键回退内置默认)
+QStringList dbCoreList()
+{
+ return splitCoreCsv(ConfigManager::instance()->getValue(
+ Constants::ConfigKeys::SectionCoreConfig, Constants::ConfigKeys::KeyDbCore,
+ kFallbackDbCores.join(QLatin1Char(','))));
+}
+
+// 核心取值表 = mdbcore 在前 + dbcore 在后(顺序即下拉顺序, 首项为默认值;
+// mdbcore 在前保证默认核心 ucbp 靠前展示)
+QStringList coreList()
+{
+ QStringList cores = mdbCoreList();
+ const QStringList dbCores = dbCoreList();
+ for (const QString &core : dbCores) {
+ if (!cores.contains(core)) {
+ cores.append(core);
+ }
+ }
+ if (cores.isEmpty()) {
+ cores = kFallbackMdbCores + kFallbackDbCores;
+ }
+ return cores;
+}
+
+int coreIndexOf(const QStringList &cores, const QString &core)
+{
+ const int index = cores.indexOf(core);
+ return index >= 0 ? index : 0; // 未知核心回退默认首项(mdbcore 首项)
}
} // namespace
@@ -146,16 +195,26 @@ void BatchConvertPage::attachRowItems(QList &rowItems, const QS
void BatchConvertPage::attachRowWidgets(int row, const QString &funcName)
{
- // "核心"下拉: ucbp/uses/uarg/usms, 切换即写回并持久化(dialect 由 core 派生, 保存时写入)
+ // "核心"下拉: 取值汇总自 config.ini [coreConfig](mdbcore+dbcore),
+ // 切换即写回并持久化(dialect 由 core 派生, 保存时写入)
ElaComboBox *coreCombo = new ElaComboBox;
- for (const QString &core : kCoreList) {
+ const QStringList cores = coreList();
+ for (const QString &core : cores) {
coreCombo->addItem(core);
}
const QString curCore = m_coreMap.value(funcName, Constants::DirEntry::CoreUcbp);
- coreCombo->setCurrentIndex(coreIndexOf(curCore));
+ coreCombo->setCurrentIndex(coreIndexOf(cores, curCore));
connect(coreCombo, QOverload::of(&QComboBox::currentIndexChanged), this,
- [this, funcName, coreCombo](int index) {
- const QString core = kCoreList.value(index, Constants::DirEntry::CoreUcbp);
+ [this, funcName, coreCombo, cores](int index) {
+ // 转换运行期锁定: 下拉回弹到当前配置值, 不改内存不落盘
+ if (m_editLocked) {
+ coreCombo->blockSignals(true);
+ coreCombo->setCurrentIndex(
+ coreIndexOf(cores, m_coreMap.value(funcName, Constants::DirEntry::CoreUcbp)));
+ coreCombo->blockSignals(false);
+ return;
+ }
+ const QString core = cores.value(index, Constants::DirEntry::CoreUcbp);
const QString prev = m_coreMap.value(funcName, Constants::DirEntry::CoreUcbp);
if (prev == core) {
return;
@@ -166,7 +225,7 @@ void BatchConvertPage::attachRowWidgets(int row, const QString &funcName)
if (!saveConfig(m_funcList)) {
m_coreMap.insert(funcName, prev);
coreCombo->blockSignals(true);
- coreCombo->setCurrentIndex(coreIndexOf(prev));
+ coreCombo->setCurrentIndex(coreIndexOf(cores, prev));
coreCombo->blockSignals(false);
}
});
@@ -179,6 +238,13 @@ void BatchConvertPage::attachRowWidgets(int row, const QString &funcName)
umgrCombo->setCurrentIndex(m_viaUmgrMap.value(funcName, false) ? 1 : 0);
connect(umgrCombo, QOverload::of(&QComboBox::currentIndexChanged), this,
[this, funcName, umgrCombo](int index) {
+ // 转换运行期锁定: 下拉回弹到当前配置值, 不改内存不落盘
+ if (m_editLocked) {
+ umgrCombo->blockSignals(true);
+ umgrCombo->setCurrentIndex(m_viaUmgrMap.value(funcName, false) ? 1 : 0);
+ umgrCombo->blockSignals(false);
+ return;
+ }
const bool viaUmgr = (index == 1);
const bool prev = m_viaUmgrMap.value(funcName, false);
if (prev == viaUmgr) {
@@ -255,11 +321,31 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList)
return false;
}
- // 2. 定位 convert.dir 的 [ ] 区间
- QString content = QString::fromUtf8(original);
+ // 2. 校验原文件为合法 UTF-8 后再解码: fromUtf8 强解码会把 GBK 等非 UTF-8
+ // 字节静默替换为 U+FFFD, 保存后原中文永久损坏; 转换引擎按 UTF-8 读取 JSON,
+ // 非 UTF-8 文件本身也无法被引擎正确消费, 中止保存保护原文件
+ QStringDecoder utf8Decoder(QStringDecoder::Utf8);
+ QString content = utf8Decoder.decode(original);
+ if (utf8Decoder.hasError()) {
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,原文件不是合法 UTF-8 编码(可能为 GBK),"
+ "请先转换为 UTF-8 后重试: %1").arg(configFilePath()));
+ return false;
+ }
+
+ // 2.1 先定位 convert 节点再匹配 dir 键: 全文首个 "dir" 键未必属于 convert,
+ // 直接全文首匹配会替换错位置; 约定 dir 必须位于 convert 节点之后
+ QRegularExpression convertRe(
+ QStringLiteral("\"%1\"\\s*:\\s*\\{").arg(Constants::CustKeys::ConvertDir));
+ const qsizetype convertStart = content.indexOf(convertRe);
+ if (convertStart < 0) {
+ LogManager::instance()->logError(
+ QString("全量转码 - 保存配置失败,未找到 convert 节点"));
+ return false;
+ }
QRegularExpression dirRe(
QStringLiteral("\"%1\"\\s*:\\s*\\[").arg(Constants::CustKeys::ConvertDirSub));
- QRegularExpressionMatch dirMatch = dirRe.match(content);
+ QRegularExpressionMatch dirMatch = dirRe.match(content, convertStart);
if (!dirMatch.hasMatch()) {
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,未找到 convert.%1 节点").arg(
@@ -267,12 +353,12 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList)
return false;
}
- const int bracketStart = dirMatch.capturedEnd() - 1; // '[' 位置
- int bracketEnd = -1;
+ const qsizetype bracketStart = dirMatch.capturedEnd() - 1; // '[' 位置
+ qsizetype bracketEnd = -1;
int depth = 0;
bool inString = false;
bool escaped = false;
- for (int i = bracketStart; i < content.size(); ++i) {
+ for (qsizetype i = bracketStart; i < content.size(); ++i) {
const QChar c = content[i];
if (inString) {
if (escaped) {
@@ -305,12 +391,14 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList)
// 3. 构建新数组(V2 对象格式, dialect 由 core 派生)
// 紧凑格式: 每个功能一行对象(与需求示例一致), 避免逐字段展开占用大量缩进
+ const QStringList dbCores = dbCoreList();
+ const QStringList mdbCores = mdbCoreList();
QString newArray = "[";
for (int i = 0; i < funcList.size(); ++i) {
const QString &name = funcList[i];
const QString core = m_coreMap.value(name, Constants::DirEntry::CoreUcbp);
const bool viaUmgr = m_viaUmgrMap.value(name, false);
- const QString dialect = Constants::DirEntry::dialectForCore(core);
+ const QString dialect = Constants::DirEntry::dialectForCore(core, mdbCores, dbCores);
if (i > 0) {
newArray += ",";
}
@@ -358,16 +446,18 @@ bool BatchConvertPage::saveConfig(const QStringList &funcList)
// 保存窗口: 写入前开启, 窗口期内 QSaveFile 替换目标文件引发的
// fileChanged/directoryChanged 监控事件直接忽略, 防止自身保存触发重载
// (重载会 refreshTable 强制 scrollToTop, 表现为下拉选择后滚动条回顶);
- // 失败路径(打开/提交失败)由组件 2s 兜底自动关窗
+ // 失败路径(打开/提交失败)显式关窗, 不依赖组件 2s 兜底
m_configMonitor->beginSaveWindow();
QSaveFile saveFile(configFilePath());
if (!saveFile.open(QIODevice::WriteOnly)) {
+ m_configMonitor->endSave();
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,无法写入: %1").arg(configFilePath()));
return false;
}
saveFile.write(newContent.toUtf8());
if (!saveFile.commit()) {
+ m_configMonitor->endSave();
LogManager::instance()->logError(
QString("全量转码 - 保存配置失败,写入提交失败: %1").arg(configFilePath()));
return false;
@@ -406,6 +496,7 @@ void BatchConvertPage::loadConfig()
QStringList items;
m_coreMap.clear();
m_viaUmgrMap.clear();
+ const QStringList cores = coreList();
for (const QJsonValue &val : dir) {
// V1 字符串元素 / V2 对象元素双格式兼容: 缺失字段按契约默认值兜底
QString item;
@@ -415,7 +506,7 @@ void BatchConvertPage::loadConfig()
const QJsonObject entry = val.toObject();
item = entry.value(Constants::DirEntry::Name).toString();
const QString c = entry.value(Constants::DirEntry::Core).toString();
- if (kCoreList.contains(c)) {
+ if (cores.contains(c)) {
core = c;
}
viaUmgr = entry.value(Constants::DirEntry::ViaUmgr).toBool(false);
diff --git a/src/pages/batchconvert/batchconvertpage.h b/src/pages/batchconvert/batchconvertpage.h
index bb31228..bcdff28 100644
--- a/src/pages/batchconvert/batchconvertpage.h
+++ b/src/pages/batchconvert/batchconvertpage.h
@@ -12,8 +12,8 @@ class QStandardItem;
// 函数清单(短名)持久化到 config/cust.json,convert.dir 为对象数组
// [{name, core, viaUmgr, dialect}, ...](V2 格式,兼容读旧字符串数组 V1),
// 保存时对原文件做文本级区间替换(保留 cust.json 其余全部格式不重序列化)。
-// core 决定目标核心(ucbp/uses/uarg/usms);dialect 由 core 派生、不提供独立 UI
-// (ucbp/uses→mdb,uarg/usms→db);viaUmgr 为是否柜面接口。
+// core 决定目标核心(取值汇总自 config.ini [coreConfig] 的 mdbcore/dbcore);
+// dialect 由 core 派生、不提供独立 UI(∈dbcore→db,其余→mdb);viaUmgr 为是否柜面接口。
// 格式契约见 详细设计文档.md §7.5
class BatchConvertPage : public ConvertWorkbenchBase
{
@@ -55,7 +55,7 @@ protected:
private:
QLabel *m_tipLabel = nullptr; // 页首灰字说明条(需主题联动)
// 每个清单功能的核心/柜面接口属性(与 cust.json convert.dir 条目字段一致)
- QHash m_coreMap; // 功能名 → "ucbp"/"uses"/"uarg"/"usms"
+ QHash m_coreMap; // 功能名 → 核心名(取值来自 config.ini [coreConfig])
QHash m_viaUmgrMap; // 功能名 → 是否柜面接口
};
diff --git a/src/pages/common/convertworkbenchbase.cpp b/src/pages/common/convertworkbenchbase.cpp
index e3f8760..24d10a6 100644
--- a/src/pages/common/convertworkbenchbase.cpp
+++ b/src/pages/common/convertworkbenchbase.cpp
@@ -72,6 +72,17 @@ void styleStatusItem(QStandardItem *item, bool exists)
}
}
+// 状态列"判定中"占位样式: cname 集合未就绪(后台预热中)时暂不可判定,
+// 用中性灰占位避免误导为"不存在"; 预热完成后由 functionCacheReady 触发
+// refreshExistColumn 批量补刷为最终结果
+void styleStatusPending(QStandardItem *item)
+{
+ item->setText("…");
+ item->setTextAlignment(Qt::AlignCenter);
+ item->setData(QBrush(QColor(210, 210, 210)), Qt::BackgroundRole);
+ item->setData(QBrush(QColor(105, 105, 105)), Qt::ForegroundRole);
+}
+
} // namespace
ConvertWorkbenchBase::ConvertWorkbenchBase(QWidget *parent)
@@ -148,16 +159,16 @@ void ConvertWorkbenchBase::initUI()
funcLayout->addWidget(m_funcTable);
QHBoxLayout *tableBtnLayout = new QHBoxLayout;
- ElaPushButton *addBtn = new ElaPushButton("添加");
- connect(addBtn, &QPushButton::clicked, this, &ConvertWorkbenchBase::onAddFunction);
- ElaPushButton *removeBtn = new ElaPushButton("删除选中");
- connect(removeBtn, &QPushButton::clicked, this, &ConvertWorkbenchBase::onRemoveFunction);
- ElaPushButton *clearBtn = new ElaPushButton("清空列表");
- connect(clearBtn, &QPushButton::clicked, this, &ConvertWorkbenchBase::onClearTable);
- tableBtnLayout->addWidget(addBtn);
+ m_addBtn = new ElaPushButton("添加");
+ connect(m_addBtn, &QPushButton::clicked, this, &ConvertWorkbenchBase::onAddFunction);
+ m_removeBtn = new ElaPushButton("删除选中");
+ connect(m_removeBtn, &QPushButton::clicked, this, &ConvertWorkbenchBase::onRemoveFunction);
+ m_clearBtn = new ElaPushButton("清空列表");
+ connect(m_clearBtn, &QPushButton::clicked, this, &ConvertWorkbenchBase::onClearTable);
+ tableBtnLayout->addWidget(m_addBtn);
tableBtnLayout->addStretch();
- tableBtnLayout->addWidget(removeBtn);
- tableBtnLayout->addWidget(clearBtn);
+ tableBtnLayout->addWidget(m_removeBtn);
+ tableBtnLayout->addWidget(m_clearBtn);
funcLayout->addLayout(tableBtnLayout);
layout->addWidget(funcBox);
@@ -210,9 +221,10 @@ void ConvertWorkbenchBase::refreshTable()
m_funcTable->setUpdatesEnabled(false);
m_tableModel->removeRows(0, m_tableModel->rowCount());
- // 批量存在性判定: 一次取全量 cname 集合替代逐行单条查库(cname 缓存未就绪窗口期
- // 每行都会触发一次 warmup 探测 + 单条 SQL)。集合为空(数据未落库/缓存未就绪)时
- // 回退逐行 checkFunctionExists——其内部含单条查库兜底, 不会把"未就绪"误判为"不存在"
+ // 批量存在性判定: 一次取全量 cname 集合替代逐行单条查库。集合为空(cname 缓存
+ // 未就绪: 后台预热中/数据未落库)时不再逐行 checkFunctionExists 回退——N 行即
+ // N 次串行"预热探测+单条 SQL", 统一显示"加载中"占位; 预热完成后
+ // functionCacheReady -> refreshExistColumn 批量补刷为最终结果
const QSet allCnames = UF2ConfigReader::getAllCnamesStatic();
const bool batchExistsReady = !allCnames.isEmpty();
@@ -224,8 +236,11 @@ void ConvertWorkbenchBase::refreshTable()
QStandardItem *statusItem = new QStandardItem;
statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable);
- styleStatusItem(statusItem, batchExistsReady ? allCnames.contains(funcName)
- : checkFunctionExists(funcName));
+ if (batchExistsReady) {
+ styleStatusItem(statusItem, allCnames.contains(funcName));
+ } else {
+ styleStatusPending(statusItem);
+ }
QList rowItems;
rowItems.append(nameItem);
@@ -267,10 +282,10 @@ void ConvertWorkbenchBase::refreshExistColumn()
return;
}
const int rows = qMin(m_funcList.size(), m_tableModel->rowCount());
- // 一次取全量 cname 集合批量判定。与 refreshTable 的"空集合逐行单条查库兜底"不同:
- // 本函数由 showEvent 切页路径高频触发, 缓存未就绪(预热中/数据未落库)时逐行查库
- // 会产生 N 次单条 SQL, 故未就绪时本次直接跳过(状态列保留上次结果),
- // 预热完成后下次切页自动切换为内存集合快路径
+ // 一次取全量 cname 集合批量判定。本函数由 showEvent 切页路径高频触发,
+ // 缓存未就绪(预热中/数据未落库)时逐行查库会产生 N 次单条 SQL, 故未就绪时
+ // 本次直接跳过(refreshTable 的 pending 占位与上次结果均原样保留),
+ // 预热完成后 functionCacheReady 再次触发本函数, 批量补刷为最终结果
const QSet allCnames = UF2ConfigReader::getAllCnamesStatic();
if (allCnames.isEmpty()) {
return;
@@ -284,11 +299,6 @@ void ConvertWorkbenchBase::refreshExistColumn()
}
}
-bool ConvertWorkbenchBase::checkFunctionExists(const QString &funcName)
-{
- return UF2ConfigReader::checkFuncExistsStatic(funcName);
-}
-
void ConvertWorkbenchBase::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
@@ -336,6 +346,10 @@ void ConvertWorkbenchBase::onProgressUpdate(int progress, const QString &status)
// 排队请求中的旧行号会失效指向错误行, 函数名是唯一稳定键
void ConvertWorkbenchBase::onEditRowByName(const QString &funcName)
{
+ // 转换运行期锁定: 双击编辑静默拒绝(引擎正在读取的配置不允许被变更落盘)
+ if (m_editLocked) {
+ return;
+ }
const int row = m_funcList.indexOf(funcName);
if (row >= 0) {
onEditRow(row);
@@ -559,6 +573,7 @@ void ConvertWorkbenchBase::onStartConvert()
void ConvertWorkbenchBase::onConvertStarted()
{
m_converting = true;
+ setEditingLocked(true);
m_startBtn->setText("停止转换");
m_startBtn->setStyleSheet(kStopBtnStyle);
}
@@ -566,6 +581,7 @@ void ConvertWorkbenchBase::onConvertStarted()
void ConvertWorkbenchBase::onConvertFinished()
{
m_converting = false;
+ setEditingLocked(false);
m_progressBar->setValue(100);
m_progressLabel->setText("转换完成");
m_startBtn->setText("开始转换");
@@ -575,6 +591,7 @@ void ConvertWorkbenchBase::onConvertFinished()
void ConvertWorkbenchBase::onConvertFailed(const QString &detail)
{
m_converting = false;
+ setEditingLocked(false);
// 失败收尾: 进度条保持当前进度(不复位 100%, 与成功完成区分), 文字提示转换失败
m_progressLabel->setText(detail.isEmpty() ? "转换失败" : detail);
m_startBtn->setText("开始转换");
@@ -587,6 +604,9 @@ void ConvertWorkbenchBase::setTaskBusy(bool busy)
return;
}
m_startBtn->setEnabled(!busy);
+ // 其他工作台任务运行期间同样锁定本页编辑; 解锁时若本页自身仍在转换中
+ // (另一任务先于本任务结束), 保持锁定由 onConvertFinished/Failed 收口
+ setEditingLocked(busy || m_converting);
if (busy) {
m_progressBar->setValue(0);
m_progressLabel->setText("有其他转换任务进行中,请等待完成...");
@@ -595,6 +615,20 @@ void ConvertWorkbenchBase::setTaskBusy(bool busy)
}
}
+void ConvertWorkbenchBase::setEditingLocked(bool locked)
+{
+ m_editLocked = locked;
+ if (m_addBtn) {
+ m_addBtn->setEnabled(!locked);
+ }
+ if (m_removeBtn) {
+ m_removeBtn->setEnabled(!locked);
+ }
+ if (m_clearBtn) {
+ m_clearBtn->setEnabled(!locked);
+ }
+}
+
// 添加流程公共核心: 名称清洗 + QSet 判重(含同批互判) + onFunctionAdded 附加动作。
// 返回 {新增名列表, 跳过数}; 不落盘/不刷新/不弹窗, 差异化交互由调用方处理
QPair ConvertWorkbenchBase::addNames(const QStringList &names)
@@ -626,6 +660,12 @@ void ConvertWorkbenchBase::addFunction(const QString &funcName)
return;
}
+ // 转换运行期锁定: 跨页添加拒绝(提示后直接返回, 不改清单不落盘)
+ if (m_editLocked) {
+ QMessageBox::information(nullptr, "提示", "转换进行中,请等待完成后再添加");
+ return;
+ }
+
const QString name = normalizeFunctionName(funcName);
if (name.isEmpty()) {
return;
@@ -652,6 +692,13 @@ QPair ConvertWorkbenchBase::addFunctionsBatch(const QStringList &funcN
{
// 批量添加(功能查询页多选双击): 不逐条弹窗;
// notify=true 时结束后一次汇总提示, notify=false 静默(由调用方统一汇总), 返回 {新增数, 跳过数}
+ // 转换运行期锁定: 拒绝添加, 返回 {-1, -1}(语义同保存失败: 本次未成功)
+ if (m_editLocked) {
+ if (notify) {
+ QMessageBox::information(nullptr, "提示", "转换进行中,请等待完成后再添加");
+ }
+ return qMakePair(-1, -1);
+ }
const QPair addResult = addNames(funcNames);
const QStringList &added = addResult.first;
const int skipped = addResult.second;
diff --git a/src/pages/common/convertworkbenchbase.h b/src/pages/common/convertworkbenchbase.h
index 8feb699..b4351db 100644
--- a/src/pages/common/convertworkbenchbase.h
+++ b/src/pages/common/convertworkbenchbase.h
@@ -14,6 +14,7 @@ class QVBoxLayout;
class QResizeEvent;
class QShowEvent;
class QStandardItem;
+class ElaPushButton;
// 转码工作台公共基类(全量转码 BatchConvertPage / 按需转码 NonLsConvertPage)
// 统一收敛:
@@ -53,6 +54,9 @@ public slots:
int getFunctionCount() const { return m_funcList.size(); }
// 本次开始转换前已拼装好的引擎参数(由 prepareStartArguments 钩子生成,默认空)
QStringList preparedArguments() const { return m_preparedArgs; }
+ // 转换运行期编辑锁定: 锁定期间双击编辑/行内下拉/跨页添加均被拒绝,
+ // 防止运行中清单变更落盘后与引擎实际读取的配置不一致
+ bool isEditingLocked() const { return m_editLocked; }
protected:
// ---- 界面骨架虚钩子 ----
@@ -107,10 +111,8 @@ protected:
// ---- 单一刷新入口 ----
// 表格全量重建的唯一入口: 所有数据变更路径统一经此刷新
void refreshTable();
- // 轻量刷新: 仅重算"是否存在"列(showEvent 用), 不重建表格与行控件
+ // 轻量刷新: 仅重算"是否存在"列(showEvent 与 functionCacheReady 用), 不重建表格与行控件
void refreshExistColumn();
- // 函数存在性检查(读 DataCache)
- bool checkFunctionExists(const QString &funcName);
// ---- 两阶段初始化: 子类构造函数体内显式调用 ----
// 装配公共界面骨架 -> loadConfig 加载清单 -> 挂外部修改监控与主题联动
@@ -122,6 +124,9 @@ protected:
QStringList m_funcList;
JsonFileMonitor *m_configMonitor = nullptr; // 外部修改监控(双监控+防抖+mtime 回环过滤)
QStringList m_preparedArgs;
+ // 转换运行期编辑锁定标志(随 m_converting/setTaskBusy 自动联动);
+ // 子类行内下拉 lambda 守卫直接读取
+ bool m_editLocked = false;
private slots:
void onStartConvert();
@@ -134,6 +139,8 @@ private slots:
private:
void initUI();
+ // 编辑锁定联动: 禁用/启用 添加/删除/清空 三按钮(行内下拉与双击编辑由各守卫点判断)
+ void setEditingLocked(bool locked);
void resizeEvent(QResizeEvent *event) override;
void showEvent(QShowEvent *event) override;
@@ -143,6 +150,10 @@ private:
ElaProgressBar *m_progressBar = nullptr;
ElaText *m_progressLabel = nullptr;
QPushButton *m_startBtn = nullptr;
+ // 转码功能区三按钮(成员化: 转换运行期 setEditingLocked 需要禁用/启用)
+ ElaPushButton *m_addBtn = nullptr;
+ ElaPushButton *m_removeBtn = nullptr;
+ ElaPushButton *m_clearBtn = nullptr;
// 转换运行态标志: 判运行态用状态位而非按钮文本(文本判据在文案/样式调整时静默失效)
bool m_converting = false;
};
diff --git a/src/pages/metadataupdate/metadataupdatepage.cpp b/src/pages/metadataupdate/metadataupdatepage.cpp
index 2f82cb5..a627c88 100644
--- a/src/pages/metadataupdate/metadataupdatepage.cpp
+++ b/src/pages/metadataupdate/metadataupdatepage.cpp
@@ -9,6 +9,8 @@
#include
#include
#include
+#include
+#include
#include
const int UPDATE_ITEM_SHOW_COLUMN_COUNT = 3;
@@ -479,6 +481,10 @@ void MetadataUpdatePage::onWorkerFinished()
// 数据缓存重载必须在主线程执行(DataCache 单例与静态 cnameCache 非线程安全)
if (r.needReload) {
_processLabel->setText("正在更新数据缓存...");
+ // 重载为全表扫描+JSON 解析, 大库可达数秒: 先让提示落地绘制再进入同步重载,
+ // 否则界面冻结期间标签停留在旧文案, 看似无响应
+ // (排除用户输入, 期间按钮均已禁用, 仅处理绘制/定时器事件)
+ QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
// 按当前任务选择对应版本的配置重载
if (m_currentTaskName.compare(kTaskUf20, Qt::CaseInsensitive) == 0) {
UF2ConfigReader::reloadUF20Config();
diff --git a/src/pages/nonlsconvert/nonlsconvertpage.cpp b/src/pages/nonlsconvert/nonlsconvertpage.cpp
index 81123cf..2fc0b23 100644
--- a/src/pages/nonlsconvert/nonlsconvertpage.cpp
+++ b/src/pages/nonlsconvert/nonlsconvertpage.cpp
@@ -159,6 +159,14 @@ void NonLsConvertPage::attachRowWidgets(int row, const QString &funcName)
(m_recursionMap.value(funcName, RecursionNo) == RecursionYes) ? 1 : 0);
connect(recCombo, QOverload::of(&QComboBox::currentIndexChanged),
this, [this, funcName, recCombo](int index) {
+ // 转换运行期锁定: 下拉回弹到当前配置值, 不改内存不落盘
+ if (m_editLocked) {
+ recCombo->blockSignals(true);
+ recCombo->setCurrentIndex(
+ (m_recursionMap.value(funcName, RecursionNo) == RecursionYes) ? 1 : 0);
+ recCombo->blockSignals(false);
+ return;
+ }
const int recursion = (index == 1) ? RecursionYes : RecursionNo;
const int prev = m_recursionMap.value(funcName, RecursionNo);
if (prev == recursion) {
@@ -186,6 +194,15 @@ void NonLsConvertPage::attachRowWidgets(int row, const QString &funcName)
== Constants::DirEntry::DialectDb) ? 1 : 0);
connect(dbCombo, QOverload::of(&QComboBox::currentIndexChanged),
this, [this, funcName, dbCombo](int index) {
+ // 转换运行期锁定: 下拉回弹到当前配置值, 不改内存不落盘
+ if (m_editLocked) {
+ dbCombo->blockSignals(true);
+ dbCombo->setCurrentIndex(
+ (m_dialectMap.value(funcName, Constants::DirEntry::DialectMdb)
+ == Constants::DirEntry::DialectDb) ? 1 : 0);
+ dbCombo->blockSignals(false);
+ return;
+ }
const QString dialect = (index == 1) ? Constants::DirEntry::DialectDb
: Constants::DirEntry::DialectMdb;
const QString prev = m_dialectMap.value(funcName, Constants::DirEntry::DialectMdb);
@@ -310,10 +327,11 @@ bool NonLsConvertPage::saveConfig(const QStringList &funcList)
// 避免写入中途崩溃/断电损坏配置。
// 保存窗口: 写入前开启, 窗口期内 QSaveFile 替换目标文件引发的
// fileChanged/directoryChanged 监控事件直接忽略, 防止自身保存触发重载;
- // 失败路径(打开/提交失败)由组件 2s 兜底自动关窗
+ // 失败路径(打开/提交失败)显式关窗, 不依赖组件 2s 兜底
m_configMonitor->beginSaveWindow();
QSaveFile file(jsonPath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ m_configMonitor->endSave();
LogManager::instance()->logWarning(
QString("按需转码 - 保存配置失败,无法写入: %1").arg(jsonPath));
return false;
@@ -321,6 +339,7 @@ bool NonLsConvertPage::saveConfig(const QStringList &funcList)
QJsonDocument doc(root);
file.write(doc.toJson(QJsonDocument::Indented));
if (!file.commit()) {
+ m_configMonitor->endSave();
LogManager::instance()->logWarning(
QString("按需转码 - 保存配置失败,写入提交异常: %1").arg(jsonPath));
return false;
diff --git a/src/pythonrunner/PythonRunner.cpp b/src/pythonrunner/PythonRunner.cpp
index 4bbf485..b0f03a2 100644
--- a/src/pythonrunner/PythonRunner.cpp
+++ b/src/pythonrunner/PythonRunner.cpp
@@ -8,11 +8,13 @@ PythonRunner::PythonRunner(QObject *parent)
, m_killTimer(nullptr)
{
connect(m_process, &QProcess::started, this, &PythonRunner::started);
- connect(m_process, QOverload::of(&QProcess::finished),
- this, &PythonRunner::finished);
- // 进程结束后冲刷残留的不完整序列, 避免最后一块输出丢失
+ // 进程结束后先冲刷残留的不完整序列, 再向调用方转发 finished: 两个同序直连
+ // 按注册顺序执行, 调用方必须先看到最后一块输出再收到结束通知, 反序会让
+ // 结束后的日志汇总/结果判定遗漏尾部输出
connect(m_process, QOverload::of(&QProcess::finished),
this, &PythonRunner::flushPendingOutput);
+ connect(m_process, QOverload::of(&QProcess::finished),
+ this, &PythonRunner::finished);
connect(m_process, &QProcess::readyReadStandardOutput,
this, &PythonRunner::onReadyReadStandardOutput);
connect(m_process, &QProcess::readyReadStandardError,
@@ -36,10 +38,11 @@ PythonRunner::~PythonRunner()
if (m_runTimer) {
m_runTimer->stop();
}
- // 析构时不能留孤儿进程: 直接 kill 并有界等待(不再走 stop() 的异步升级,
- // 对象即将销毁, 定时器回调无处可投递)
+ // 析构时不能留孤儿进程: 进程树级终止(引擎为打包 exe, 可能已派生孙进程,
+ // 仅 kill 直接子进程会留下继续占用文件/端口的孤儿树), 并有界等待(不再走
+ // stop() 的异步升级, 对象即将销毁, 定时器回调无处可投递)
if (m_process->state() != QProcess::NotRunning) {
- m_process->kill();
+ killProcessTree();
if (!m_process->waitForFinished(Constants::Timings::ProcessTerminateWaitMs)) {
// 极少数情况(进程 hung 在不可中断的系统调用/Starting 窗口内 kill 无效)
// 下等不到退出: 析构不能无限阻塞, 落错误日志供用户手动清理残留进程
@@ -103,9 +106,12 @@ bool PythonRunner::start()
// 强制子进程 stdio 输出 UTF-8: Windows 管道下 Python 默认按 locale(GBK)输出,
// UTF-8/GBK 混流无法可靠判定编码, 统一后解码只需处理 UTF-8
- // (仅影响 stdio, 不改变脚本读写文件的默认编码)
+ // (仅影响 stdio, 不改变脚本读写文件的默认编码)。
+ // 同时禁用 stdio 块缓冲(PYTHONUNBUFFERED=1): 管道输出默认全缓冲, 进度类输出
+ // 会积压数 KB 才一次性到达, 表现为界面日志滞后成块蹦出
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("PYTHONIOENCODING", "utf-8");
+ env.insert("PYTHONUNBUFFERED", "1");
program = m_packagedExePath;
arguments = m_arguments;
@@ -127,6 +133,14 @@ bool PythonRunner::start()
// error() 判据兜底: QProcessPrivate::start 每次入口都重置 processError, 无残留误判。
if (m_process->state() == QProcess::NotRunning
|| m_process->error() == QProcess::FailedToStart) {
+ // 同步启动失败收尾: finished 不会发出(Qt 6.6 先 cleanup 再 setErrorAndEmit),
+ // 遗留的升级与超时定时器须显式停止, 避免其到期误杀后续新任务
+ if (m_killTimer) {
+ m_killTimer->stop();
+ }
+ if (m_runTimer) {
+ m_runTimer->stop();
+ }
return false;
}
// 启动流程已受理: 结果由 started / finished 信号异步上报
@@ -155,6 +169,33 @@ void PythonRunner::stop()
void PythonRunner::onKillTimeout()
{
+ // 升级终止: terminate 等待期内未退出, 转进程树级强杀(terminate 对打包 exe
+ // 的无窗口控制台进程通常无效, 实际终止以本升级为主)
+ if (m_process->state() != QProcess::NotRunning) {
+ killProcessTree();
+ }
+}
+
+void PythonRunner::killProcessTree()
+{
+ const qint64 pid = m_process->processId();
+ if (pid > 0) {
+ QProcess taskkill;
+ // CREATE_NO_WINDOW(0x08000000): 避免主程序 GUI 下启动 taskkill 闪现控制台黑框
+ taskkill.setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments *args) {
+ args->flags |= 0x08000000;
+ });
+ // taskkill /T 自顶向下终止整棵进程树(含引擎派生的孙进程), /F 强制;
+ // taskkill 不可用(极端环境)或超时则回退直接 kill 直接子进程
+ taskkill.start(QStringLiteral("taskkill"),
+ {QStringLiteral("/PID"), QString::number(pid),
+ QStringLiteral("/T"), QStringLiteral("/F")});
+ if (taskkill.waitForFinished(Constants::Timings::ProcessTerminateWaitMs)) {
+ return;
+ }
+ LogManager::instance()->logWarning(
+ QString("PythonRunner - taskkill 终止进程树(pid=%1)未确认成功, 回退直接 kill").arg(pid));
+ }
if (m_process->state() != QProcess::NotRunning) {
m_process->kill();
}
@@ -183,10 +224,14 @@ void PythonRunner::decodeAndEmit(const QByteArray& chunk, QByteArray& pending, b
// 累积到 pending, 只解码完整 UTF-8 序列, 尾部不完整序列留待下一块
pending += chunk;
int end = pending.size();
+ // 容量防护: 引擎若持续输出以不完整序列结尾的数据(损坏输出/二进制垃圾),
+ // 尾部残留会无限累积直至内存耗尽。超过上限时本轮强制消费到空——
+ // 尾部不完整序列按 U+FFFD 解码输出, 内存恢复有界
+ const bool forceFlush = (pending.size() > Constants::Io::MaxPendingDecodeBytes);
int i = end - 1;
int cont = 0;
while (i >= 0 && (pending.at(i) & 0xC0) == 0x80) { ++cont; --i; }
- if (i >= 0 && (pending.at(i) & 0x80) != 0) {
+ if (!forceFlush && i >= 0 && (pending.at(i) & 0x80) != 0) {
const uchar lead = static_cast(pending.at(i));
const int need = (lead & 0xE0) == 0xC0 ? 2
: (lead & 0xF0) == 0xE0 ? 3
diff --git a/src/pythonrunner/PythonRunner.h b/src/pythonrunner/PythonRunner.h
index a30461c..8b4be0e 100644
--- a/src/pythonrunner/PythonRunner.h
+++ b/src/pythonrunner/PythonRunner.h
@@ -67,6 +67,9 @@ private:
void armRunTimeout(); // 启动受理成功后武装运行超时定时器
void decodeAndEmit(const QByteArray& chunk, QByteArray& pending, bool isStdout);
void flushPendingOutput();
+ // Windows 进程树终止: taskkill /T /F 强杀整棵树(含引擎派生的孙进程),
+ // 失败回退直接 kill 直接子进程
+ void killProcessTree();
};
#endif // PYTHONRUNNER_H
diff --git a/src/utils/Constants.h b/src/utils/Constants.h
index 11c1a0d..64b534c 100644
--- a/src/utils/Constants.h
+++ b/src/utils/Constants.h
@@ -15,7 +15,7 @@ namespace Constants {
// 应用版本号(发版时只需修改此处)
// ---------------------------------------------------------------------------
namespace App {
- constexpr char Version[] = "1.6.0";
+ constexpr char Version[] = "2.0.0";
}
// ---------------------------------------------------------------------------
@@ -97,6 +97,9 @@ namespace ConfigKeys {
inline const QString SectionTestConfig = "testConfig"; // 测试配置节(supportUftdbSett/moduleGenerationCheck 共用)
inline const QString SupportUftdbSett = "supportUftdbSett"; // UF20 是否支持清算参数目录(0/1)
inline const QString ModuleGenerationCheck = "moduleGenerationCheck"; // 模块生成检查开关(0/1)
+ inline const QString SectionCoreConfig = "coreConfig"; // 核心清单配置节
+ inline const QString KeyMdbCore = "mdbcore"; // mdb 方言核心清单(逗号分隔)
+ inline const QString KeyDbCore = "dbcore"; // db 方言核心清单(逗号分隔)
}
// ---------------------------------------------------------------------------
@@ -191,16 +194,17 @@ namespace DirEntry {
inline const QString Depth = "depth"; // 递归深度标志(按需转码, 语义同 V1 数值)
inline const QString CoreUcbp = "ucbp"; // 默认核心
- inline const QString CoreUses = "uses";
- inline const QString CoreUarg = "uarg";
- inline const QString CoreUsms = "usms";
- inline const QString DialectMdb = "mdb"; // ucbp/uses 对应方言(默认)
- inline const QString DialectDb = "db"; // uarg/usms 对应方言
+ inline const QString DialectMdb = "mdb"; // mdb 方言核心对应方言(默认)
+ inline const QString DialectDb = "db"; // db 方言核心对应方言
- // dialect 由 core 派生: ucbp/uses→mdb, uarg/usms→db; UI 不提供独立 dialect 入口
- inline QString dialectForCore(const QString &core)
+ // dialect 由 core 派生: ∈dbcore→db, 其余(含未知核心)→mdb; UI 不提供独立 dialect 入口。
+ // 核心清单来自 config.ini [coreConfig](mdbcore/dbcore), 由调用方读取后传入,
+ // 本头文件保持纯常量、不依赖 ConfigManager
+ inline QString dialectForCore(const QString &core,
+ const QStringList &mdbCores, const QStringList &dbCores)
{
- return (core == CoreUarg || core == CoreUsms) ? DialectDb : DialectMdb;
+ Q_UNUSED(mdbCores)
+ return dbCores.contains(core) ? DialectDb : DialectMdb;
}
}
@@ -241,10 +245,18 @@ namespace Timings {
// 漂移视为保存余波, 不判为外部修改重载
// 转换引擎整体运行超时: 超时自动走 stop() 升级链终止疑似卡死的引擎,
// 防止任务无限挂起(全量大工程正常耗时可观的场景下取宽裕值)
- inline const int CacheWarmupWaitMs = 5000; // 功能缓存后台预热的主线程有界等待上限(超限转同步重建)
inline const qint64 ConvertRunTimeoutMs = 2 * 60 * 60 * 1000; // 2 小时
}
+// ---------------------------------------------------------------------------
+// IO 相关常量
+// ---------------------------------------------------------------------------
+namespace Io {
+ // PythonRunner 输出解码 pending 缓冲的容量上限: 引擎若持续输出以不完整 UTF-8
+ // 序列结尾的数据(损坏输出/二进制垃圾), pending 会无限累积直至内存耗尽
+ inline const int MaxPendingDecodeBytes = 1 * 1024 * 1024; // 1 MB
+}
+
// ---------------------------------------------------------------------------
// 调用链树节点状态串(callchainanalyzer 生产 + codesearchpage 消费,
// 生产/消费两侧必须同源, 拼错会静默落入未知状态分支)
diff --git a/src/utils/configreaderbase.cpp b/src/utils/configreaderbase.cpp
index cbfa18e..1c23692 100644
--- a/src/utils/configreaderbase.cpp
+++ b/src/utils/configreaderbase.cpp
@@ -76,11 +76,18 @@ bool ConfigReaderBase::doLoadCnameCache(const ConfigReaderTraits& traits,
return false;
}
+ // 轻量直查优先: cname 即接口表主键列(name), 单条 SELECT 毫秒级同步完成,
+ // 打开转码页立即可判定"是否存在", 无需等全量 JSON 预热(数秒)后由补刷刷新
+ if (traits.loadCnames && traits.loadCnames(cache)) {
+ cacheLoaded = true;
+ return true;
+ }
+
// 全量重建需全表扫描+解析数万行 JSON(可达数秒), 同步执行会阻塞主线程
// (启动期页面构造先于 singleShot 预热提交, 这里是批量路径的预热保护,
// 与 doCheckFuncExists 的单条路径对齐)。触发后台预热后仍未就绪时返回空集:
- // 存在性判定方(getAllCnamesStatic 的调用方)拿到空集会逐行回退单条查库兜底,
- // 预热完成后自动切换为内存集合快路径。
+ // 存在性判定方(refreshTable 等)拿到空集显示"加载中"占位, 预热完成后经
+ // functionCacheReady 信号补刷, 自动切换为内存集合快路径。
if (traits.warmupCache && !traits.warmupCache()) {
LogManager::instance()->logInfo(
QString("%1 Cname缓存预热中, 本次返回空集, 存在性判定走单条查库兜底").arg(traits.logTag));
diff --git a/src/utils/configreaderbase.h b/src/utils/configreaderbase.h
index 5b69651..4817dbb 100644
--- a/src/utils/configreaderbase.h
+++ b/src/utils/configreaderbase.h
@@ -18,6 +18,9 @@ struct ConfigReaderTraits
// 触发功能缓存后台预热(不阻塞调用线程)。返回 true 表示内存缓存已就绪可立即派生 cname 集合;
// 返回 false 表示预热已在后台进行, 查询方应走单条查库兜底
std::function warmupCache;
+ // cname 轻量直查(主键列单条 SELECT, 毫秒级): 成功时同步填 out 并返回 true,
+ // 存在性判定无需等全量 JSON 预热; 失败返回 false 走预热/兜底路径
+ std::function&)> loadCnames;
// 单行直查(绕过 cname 内存缓存): 用于数据尚未就绪、内存缓存为空时兜底,
// 避免把“缓存没准备好”误判为“功能不存在”
std::function checkExists;
diff --git a/src/utils/datacache.cpp b/src/utils/datacache.cpp
index 2dcf527..870ab1c 100644
--- a/src/utils/datacache.cpp
+++ b/src/utils/datacache.cpp
@@ -6,14 +6,11 @@
#include
#include
#include
-#include
-#include
#include
#include
#include
#include
#include
-#include
#include
DataCache::DataCache(QObject *parent) : QObject(parent), m_dataLoaded(false)
@@ -102,6 +99,13 @@ QSqlDatabase DataCache::database(const QString& db, bool createIfMissing) const
LogManager::instance()->logWarning(QString("数据缓存 - 设置 %1 busy_timeout 失败: %2")
.arg(db, pragmaQuery.lastError().text()));
}
+ // WAL 模式: 读不阻塞写、写不阻塞读, 后台线程全量落库(秒级持锁)期间主线程读库
+ // 不再被整体阻塞(回滚日志模式下写锁会挡住全部读, busy_timeout 只能缓解不能根治)。
+ // WAL 是库级持久属性, 但逐连接重复设置幂等且无副作用; 失败仅降级为默认日志模式
+ if (!pragmaQuery.exec("PRAGMA journal_mode = WAL")) {
+ LogManager::instance()->logWarning(QString("数据缓存 - 设置 %1 WAL 模式失败: %2")
+ .arg(db, pragmaQuery.lastError().text()));
+ }
// TLS 只存连接的 shared_ptr 不存值对象: QThreadStorage 值语义 setLocalData 会把传入对象
// 拷贝一份存入 TLS, 传入的临时对象随后析构——若直接存 ScopedConnection, 临时析构即触发
// removeDatabase, 连接在返回调用方之前就被移除并 disable(driver 被删, isOpen 变 false,
@@ -122,18 +126,29 @@ bool DataCache::initDatabase()
{
// 纯只读探测: 仅当缓存库文件已存在时才打开, 不创建空库文件、不建 _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(Constants::Cache::Uf20DbName);
- if (ok3) database(Constants::Cache::Uft3DbName);
+ // 按实际打开结果判定: 原实现仅凭文件存在即返回 true, 库文件损坏/被占用/驱动异常时
+ // 上层误判缓存就绪, 后续所有查询静默失败且无根因日志
+ const bool exists20 = QFile::exists(Constants::uf20CacheDb());
+ const bool exists3 = QFile::exists(Constants::uft3CacheDb());
+ // 仅打开已存在的库文件, 避免为尚未生成的库创建空 DB 文件
+ const bool opened20 = exists20 ? database(Constants::Cache::Uf20DbName).isOpen() : false;
+ const bool opened3 = exists3 ? database(Constants::Cache::Uft3DbName).isOpen() : false;
+ if (opened20 || opened3) {
LogManager::instance()->log(QString("数据缓存 - 已加载缓存库: uf20=%1, uft3=%2")
- .arg(ok20 ? "是" : "否").arg(ok3 ? "是" : "否"));
+ .arg(opened20 ? "是" : "否").arg(opened3 ? "是" : "否"));
+ if (exists20 != opened20 || exists3 != opened3) {
+ LogManager::instance()->logError(
+ "数据缓存 - 部分缓存库存在但打开失败, 对应库的查询不可用, 建议重新执行元数据更新");
+ }
return true;
}
- LogManager::instance()->logWarning("数据缓存 - 缓存库不存在(uf20_cache.db / uft3_cache.db), 请先执行元数据更新");
+ if (exists20 || exists3) {
+ LogManager::instance()->logError(
+ QString("数据缓存 - 缓存库打开失败(uf20 文件=%1, uft3 文件=%2), 请检查文件权限或重新执行元数据更新")
+ .arg(exists20 ? "存在" : "不存在", exists3 ? "存在" : "不存在"));
+ } else {
+ LogManager::instance()->logWarning("数据缓存 - 缓存库不存在(uf20_cache.db / uft3_cache.db), 请先执行元数据更新");
+ }
return false;
}
@@ -263,6 +278,14 @@ bool DataCache::saveDataset(const QString& db, const QString& table,
.arg(table, q.lastError().text()));
return false;
}
+ // 防御历史残留: 上次落库进程被强杀等极端场景可能留下同名临时表(新表尚未 RENAME),
+ // 直接 CREATE 会报 "table already exists" 使本轮落库失败, 先 DROP 兜底
+ if (!q.exec(QString("DROP TABLE IF EXISTS %1").arg(tmpTable))) {
+ conn.rollback();
+ LogManager::instance()->logError(QString("数据缓存 - 清理残留临时表 %1 失败: %2")
+ .arg(tmpTable, q.lastError().text()));
+ return false;
+ }
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()));
@@ -522,6 +545,38 @@ QSet DataCache::getAllCnames(const QString& db, const QString& logTag)
return result;
}
+QSet DataCache::loadCnameSet(const QString& db, const QStringList& tables, bool* ok)
+{
+ if (ok) *ok = false;
+ QSet result;
+ // 只读探测: 缓存库不存在时不创建(与 checkCnameExists/datasetExists 口径一致)
+ QSqlDatabase conn = database(db, false);
+ if (!conn.isOpen()) {
+ return result;
+ }
+ for (const QString& table : tables) {
+ // 缺表是合法状态(如无交易码数据时 trans_code 表不落库), 跳过
+ if (!datasetExists(db, table)) {
+ continue;
+ }
+ QSqlQuery query(conn);
+ if (!query.exec(QString("SELECT name FROM %1").arg(table))) {
+ LogManager::instance()->logError(
+ QString("数据缓存 - 轻量加载 %1.%2 失败: %3").arg(db, table, query.lastError().text()));
+ return result; // SQL 失败不置 ok, 调用方回退原预热路径
+ }
+ while (query.next()) {
+ const QString name = query.value(0).toString();
+ if (!name.isEmpty()) {
+ result.insert(name);
+ }
+ }
+ }
+ // 到达此处: 库已打开且存在的表均查询成功, 空集也是合法结果(数据未落库)
+ if (ok) *ok = true;
+ return result;
+}
+
QList DataCache::searchFunctions(const QString& db, const QStringList& keywords,
const QString& field, bool exactMatch, int* keywordsWithoutMatch)
{
@@ -587,28 +642,15 @@ const QList& DataCache::cachedFunctions(const QString& db) const
return cache;
}
- // 后台预热在途或已完成未消费: 等待/直接接管其结果, 避免主线程重复全表扫描。
- // 注意: 预热任务完成后 isRunning() 变 false, 若只检查 isRunning 会漏掉已完成的
- // 预热结果导致主线程再次全量重建, 因此以"是否还有未消费结果"为接管条件。
+ // 后台预热在途: 不同步忙等。原实现 msleep+processEvents 轮询最多 5s——既阻塞本调用,
+ // processEvents 又会重入无关信号槽(重入路径再次进入本函数时状态机错乱), 超限后还要
+ // 在主线程重复全表扫描与后台预热赛跑。现直接返回当前未就绪缓存(空列表), 预热完成后
+ // QFutureWatcher 广播 functionCacheReady, 依赖方(转码工作台存在列/功能查询页)补刷,
+ // 补刷触发的再次查询由下方接管逻辑拿到结果
// (预热任务只构建局部列表不写共享成员, 读写仍全部在主线程, 无并发风险)
QFuture>& warmup = isUf20 ? m_uf20WarmupFuture : m_uft3WarmupFuture;
if (warmup.isRunning()) {
- // 有界等待: 预热为全表扫描+JSON 解析, 大库可达数秒, 无界 waitForFinished 会冻结 UI。
- // Qt 6.6 的 QFuture 无带超时参数的 waitForFinished, 只能轮询; 轮询间隙必须
- // msleep 让出 CPU——processEvents 的第二参是"最多处理事件的时间", 事件队列
- // 空闲时立即返回, 不加 sleep 会让主线程在等待期 100% 自旋空转。
- // sleep 间穿插处理绘制类事件保持界面响应; 超限后转主线程同步重建。
- QElapsedTimer waitTimer;
- waitTimer.start();
- while (warmup.isRunning() && waitTimer.elapsed() < Constants::Timings::CacheWarmupWaitMs) {
- QThread::msleep(10);
- QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
- }
- if (warmup.isRunning()) {
- LogManager::instance()->logWarning(
- QString("数据缓存 - %1 功能预热未在限期(%2ms)内完成, 转为主线程同步重建")
- .arg(db).arg(Constants::Timings::CacheWarmupWaitMs));
- }
+ return cache;
}
// 接管条件必须判"结果非空"而非 resultCount()>0: QtConcurrent::run 的 future
// 完成后 resultCount 恒为 1(结果是一个 QList 容器本身, 与元素个数无关),
@@ -616,7 +658,7 @@ const QList& DataCache::cachedFunctions(const QString& db) const
// 空缓存被长期固化后存在列/功能搜索永远拿不到数据(表现为缺表场景状态永不刷新)。
// 空结果不接管则保持"未就绪", 后续查询自动重试; 快速失败路径(缺表/无库)
// 重试开销近乎为零, 不构成反复扫描
- if (!warmup.isRunning() && warmup.isValid() && !warmup.result().isEmpty()) {
+ if (warmup.isValid() && !warmup.result().isEmpty()) {
cache = warmup.result();
valid = true;
LogManager::instance()->logInfo(QString("数据缓存 - 接管 %1 后台预热功能缓存 %2 条")
diff --git a/src/utils/datacache.h b/src/utils/datacache.h
index 8e46067..e92beff 100644
--- a/src/utils/datacache.h
+++ b/src/utils/datacache.h
@@ -104,6 +104,12 @@ public:
// 查询方应走单条查库兜底(用于启动期存在性检查不阻塞主线程)。
bool warmupFunctionCacheAsync(const QString& db);
+ // cname 轻量直查: 接口表的 name 列即主键(cname), 单条 SELECT 毫秒级完成,
+ // 免全量 JSON 解析——供存在性判定方在 FunctionInfo 预热完成前同步加载 cname 全集。
+ // 缺表(如无交易码数据时 trans_code 表不落库)跳过; 库已打开且存在的表均查询成功
+ // (含合法的空结果)为 ok=true, 库未打开/SQL 出错为 false(返回空集)
+ QSet loadCnameSet(const QString& db, const QStringList& tables, bool* ok = nullptr);
+
signals:
// 指定库功能缓存后台预热完成(结果已可接管)。QFutureWatcher 保证仅在结果就绪后
// 于主线程发出。依赖方(如转码工作台"是否存在"列)借此在预热完成后立即补刷,
diff --git a/src/utils/uf2configreader.cpp b/src/utils/uf2configreader.cpp
index 78e2847..9163745 100644
--- a/src/utils/uf2configreader.cpp
+++ b/src/utils/uf2configreader.cpp
@@ -24,6 +24,12 @@ const ConfigReaderTraits& UF2ConfigReader::traits()
traits.warmupCache = [] {
return DataCache::instance()->warmupFunctionCacheAsync(Constants::Cache::Uf20DbName);
};
+ traits.loadCnames = [](QSet& out) {
+ bool ok = false;
+ out = DataCache::instance()->loadCnameSet(
+ Constants::Cache::Uf20DbName, {Constants::Cache::Uf20InterfaceTable}, &ok);
+ return ok;
+ };
traits.checkExists = [](const QString& cname) {
return DataCache::instance()->checkUF20CnameExists(cname);
};
diff --git a/src/utils/uft3configreader.cpp b/src/utils/uft3configreader.cpp
index 8187322..0363a84 100644
--- a/src/utils/uft3configreader.cpp
+++ b/src/utils/uft3configreader.cpp
@@ -24,6 +24,14 @@ const ConfigReaderTraits& UFT3ConfigReader::traits()
traits.warmupCache = [] {
return DataCache::instance()->warmupFunctionCacheAsync(Constants::Cache::Uft3DbName);
};
+ traits.loadCnames = [](QSet& out) {
+ bool ok = false;
+ // 与 getAllUFT3Cnames 覆盖范围一致: interface + trans_code 两张表
+ out = DataCache::instance()->loadCnameSet(
+ Constants::Cache::Uft3DbName,
+ {Constants::Cache::Uft3InterfaceTable, Constants::Cache::TblUft3TransCode}, &ok);
+ return ok;
+ };
traits.checkExists = [](const QString& cname) {
DataCache *cache = DataCache::instance();
// 与 getAllUFT3Cnames 覆盖范围保持一致: interface + trans_code 两张表
diff --git a/update_info.json b/update_info.json
index b7aa62f..2239831 100644
--- a/update_info.json
+++ b/update_info.json
@@ -17,7 +17,7 @@
"V1.5.0": {
"release_notes": "1.解决转码遇到公共已转函数导致失败部分接口转码失败问题;"
},
- "V1.6.0": {
- "release_notes": "1.转码逻辑优化;\n2.元数据更新逻辑优化;\n3.元数据更新存储修改为db存储模式;\n4.写日志异常修复"
+ "V2.0.0": {
+ "release_notes": "1.新增按需转码功能,支持核心功能转码;\n2.新增uf20和uft3表关系映射查找功能;\n3.支持批量查询和添加;\n4.元数据更新存储修改为db存储模式;\n5.写日志异常修复"
}
}