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"切换精确/模糊匹配模式,点击放大镜图标或按回车执行查询 -- 功能名称 - 功能名称查询 +uf20功能查询 -- 功能编号 - 模糊查询uf20 - 精准查询uf20 +**操作步骤:** +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"切换精确/模糊匹配模式,点击放大镜图标或按回车执行查询 -- 功能名称 - 功能名称查询uft3 -- 功能编号 - 模糊查询UFT3 - 精准查询uft3 +uft3功能查询 + +- 界面与 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`` UF20项目环境路径 -## UFT3元数据 -在 元数据更新 页面,选择更新范围(数据类型、标准字段、表结构、接口数据)并点击"更新UFT30"按钮完成元数据更新。 -UFT3元数据 -**备注:接口数据更新比较久** +## 元数据更新 +元数据更新 -## 更新UF20元数据 -在 元数据更新 页面,选择更新范围(数据类型、标准字段、标准错误号、接口数据、常量、菜单功能、模块、表结构)并点击"更新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 @@ # 设置 +设置.png + ## 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.写日志异常修复" } }