修改帮助文档;修复若干问题

This commit is contained in:
taocong
2026-10-22 13:46:33 +08:00
committed by taocong45644
parent 84009be804
commit 4c183f510d
59 changed files with 636 additions and 260 deletions
+4 -1
View File
@@ -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"
}
}
+2 -2
View File
@@ -10,11 +10,11 @@
</toc-element>
<toc-element topic="功能转码.md">
<toc-element topic="业务转码.md">
<toc-element topic="按需转码.md"/>
</toc-element>
<toc-element topic="代码检索.md"/>
</toc-element>
<toc-element topic="功能查询.md">
<toc-element topic="UF20-内存表映射查询.md"/>
<toc-element topic="UFT3功能查询.md"/>
<toc-element topic="UF20功能查询.md"/>
</toc-element>
@@ -25,7 +25,7 @@
</toc-element>
<toc-element topic="元数据更新.md"/>
<toc-element topic="更新记录.md">
<toc-element topic="update-1-6-0.md"/>
<toc-element topic="update-2-0-0.md"/>
<toc-element topic="update-1-5-0.md"/>
<toc-element topic="update-1-4-0.md"/>
<toc-element topic="update-1-3-0.md"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

+4
View File
@@ -10,4 +10,8 @@
<description>Created after removal of "Empty MD Topic" from Uft30ChangeCode</description>
<accepts>Empty-MD-Topic.html</accepts>
</rule>
<rule id="41fa593">
<description>Created after removal of "按需转码" from Uft30ChangeCode</description>
<accepts>按需转码.html</accepts>
</rule>
</rules>
@@ -0,0 +1,19 @@
# UF20 &amp; 内存表映射查询
检索 UF20(2.0)表与内存表之间的映射关系,结果四列: UF20表名 、 UF20表中文名 、 内存表名 、 内存表中文名 ;双击结果行可添加映射到「数据映射 → 表替换」
<img src="内存表映射关系.png" alt="内存表映射关系">
**操作步骤:**
1. 进入 功能查询 → UF20 & 内存表映射查询;
2. 输入 UF20 表名或内存表名(二者均可检索),回车或点搜索图标;
- 批量查询:多个表名用逗号、分号或空格分隔(中英文均可),各关键词命中结果取并集展示;
- 搜索框内可切换精确匹配(默认模糊匹配,不区分大小写);
3. 双击结果行将该映射关系添加到「数据映射 → 表替换」:
- 仅当该行 UF20 表名与内存表名均非空(有完整映射关系)才可添加;
- 表替换中已存在该 UF20 表名时自动跳过,并在提示栏注明当前映射值(不覆盖);
+17 -14
View File
@@ -5,20 +5,23 @@
## 查询条件
从下拉框选择查询(功能名称、功能编号),输入框输入查询关键词,点击"Aa"切换精确/模糊匹配模式,点击放大镜图标或按回车执行查询
- 功能名称
<img src="功能名称查询.png" alt="功能名称查询"/>
<img src="uf20功能查询.png" alt="uf20功能查询">
- 功能编号
<img src="模糊查询uf20.png" alt="模糊查询uf20"/>
<img src="精准查询uf20.png" alt="精准查询uf20"/>
**操作步骤:**
1. 进入 UF20功能查询 页;
2. 左侧下拉选择检索字段:函数名 / 功能号,输入关键字回车或点搜索图标;
批量查询:多个关键词用逗号、分号或空格分隔(中英文均可),如 LS_证券参数, LS_股票
名单 ;各关键词命中结果取并集展示;
单个关键词可命中同一功能的多个字段,结果按"函数名+功能号"自动去重;
3. 搜索框内可切换精确匹配(默认模糊匹配);
4. 查看结果列表,列: 函数名 、 英文名 、 功能号 ;
5. 双击或右键选中行一键加入转码工作台:LS_ 开头功能 → 全量转码;其它功能 → 按需转码;
- 支持 Ctrl/Shift 多选后一次加入;右键行不在选区内时仅加入该行;
## 结果反馈
查询提示在页面底部 tip 栏显示,不会弹出对话框打断操作:
- 加入结果只弹一个提示框("全量转码"、"按需转码"字样加粗):同时包含两类功能时显
示"已将 N 个功能加入全量转码,已将 M 个功能加入按需转码";重复添加的功能自动跳过并
在提示中注明;全部已存在时提示"均已在转码列表中"。
- 输入为空 → 提示"请输入查询内容"
- 无匹配结果 → 提示"⚠️ 未找到匹配的功能"
- 查询成功 → 表格展示结果,tip 栏恢复默认操作提示
## 添加到转码工作台
双击或右键选中行加入转码工作台:LS_ 开头的功能进入"全量转码",其它功能进入"按需转码"
<img src="双击添加.png" alt="双击添加"/>
**查询提示:** 查询反馈显示在页面底部提示栏(颜色随日间/夜间主题自动适配),不弹对话框打断操作
——输入为空提示"请输入查询内容";无匹配结果提示"⚠️ 未找到匹配的功能";查询成功后显示"已查
询 X 个业务功能函数(M 个无匹配)"并接默认操作提示。
+7 -11
View File
@@ -5,17 +5,13 @@
## 查询条件
从下拉框选择查询(功能名称、功能编号),输入框输入查询关键词,点击"Aa"切换精确/模糊匹配模式,点击放大镜图标或按回车执行查询
- 功能名称
<img src="功能名称查询uft3.png" alt="功能名称查询uft3"/>
- 功能编号
<img src="模糊查询UFT3.png" alt="模糊查询UFT3"/>
<img src="精准查询uft3.png" alt="精准查询uft3"/>
<img src="uft3功能查询.png" alt="uft3功能查询"/>
- 界面与 UF20 功能查询一致(函数名 / 功能号检索、批量关键词、精确匹配),结果列为 函数名 、 英文名 、 功能号 ,查询提示同样在页面底部提示栏反馈;
- 仅用于查询核对 UFT3 功能元数据,双击/右键不加入转码清单;
## 结果反馈
查询提示在页面底部 tip 栏显示,不会弹出对话框打断操作:
- 输入为空 → 提示"请输入查询内容"
- 无匹配结果 → 提示"⚠️ 未找到匹配的功能"
- 查询成功 → 表格展示结果,tip 栏恢复默认提示
> 本页仅提供查询参考,结果行不支持双击加入转码工作台。
-9
View File
@@ -1,9 +0,0 @@
# 1.6.0
1.转码逻辑优化;
2.元数据更新逻辑优化;
3.元数据更新存储修改为db存储模式;
4.写日志异常修复;
+12
View File
@@ -0,0 +1,12 @@
# 2.0.0
1.新增按需转码功能,支持核心功能转码;
2.新增uf20和uft3表关系映射查找功能;
3.支持批量查询和添加;
4.元数据更新存储修改为db存储模式;
5.写日志异常修复
+7 -4
View File
@@ -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) | |
@@ -2,29 +2,13 @@
维护一份功能全名清单:转码时清单中的功能全名**保持原名不做替换**。入口:左侧导航"数据映射" > "不转码功能函数"。
## 添加条目
<img src="不转码函数.png" alt="不转码函数">
点击"添加"按钮,在弹窗中输入功能全名(如 `AF_用户公用_用户可操作分支获取`)后确定。
- 功能全名不能为空;
- 不允许与已有条目重复,重复时提示后不会加入。
## 编辑与删除
双击表格单元格可直接修改条目。
选中行后点击"删除选中",确认后移除所选条目。
## 保存
点击"保存"按钮将清单写回 `bin/uf2touft3/config/cust.json`。
保存前会校验:
- 存在空条目时报错并中止,需填写或删除该行;
- 存在重复条目时报错并中止,需修改后再保存。
## 配置文件同步
- 清单持久化于 `cust.json` 的不转码功能函数数组中,与其他数据映射配置共用同一文件;
- `cust.json` 被外部修改时,清单自动刷新(未保存的修改会被覆盖,请先保存再改外部文件)。
**操作步骤:**
1. 进入 数据映射 → 不转码功能函数;
2. 顶部搜索框实时过滤条目(不区分大小写);
3. 双击单元格直接修改;
4. 点 添加 弹窗输入功能函数名(必填,重复时保存被拦截提示);
5. 删除选中 需二次确认(支持多选);
6. 修改后点 保存 生效;
+38 -21
View File
@@ -1,36 +1,53 @@
# 业务转码
将UF20函数转换为UFT3函数格式的核心功能。
业务转码页顶部通过 Tab 切换 全量转码 / 按需转码 两个工作台。
## 功能列表管理
## 全量转码
<img src="全量转码.png" alt="全量转码"/>
点击"添加"按钮,输入函数名添加到转码列表
<img src="添加函数.png" alt="添加函数"/>
**说明:** 全量转码仅支持 LS_ 开头的功能("内存交易"改造范围),按清单对功能进行整体转码。
双击表格中函数名所在行,可在弹窗中修改函数名(页面顶部有操作提示),如下:
<img src="编辑.png" alt="编辑"/>
**表格列:** UF20函数名 | 是否存在 | 核心 | 是否柜面接口 (双击函数名所在行可在弹窗中修改函数
名)。
- "是否存在"显示 否:说明元数据未更新或该功能在 UF20 元数据中检索不到;
选中行后点击"删除选中"移除函数、点击"清空列表"清空所有函数,如下:
<img src="删除.png" alt="删除"/>
- 核心:下拉选择该功能转码后归属的核心,可选 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. 点击 开始转换,进度条与状态文字显示转换进度;转换中按钮变为 停止转换,可随时点击终止任务。
切换后立即写入配置并生效。
## 按需转码
<img src="按需转码.png" alt="按需转码">
## 配置文件同步
**说明:** 按需转码针对非 LS 功能转码,支持单文件转码与递归转码两种模式,转码范围不包含核心文件、module 模块文件。
- 自动读取 bin/uf2touft3/config/cust.json 配置(convert.dir 为对象数组 {name, core, viaUmgr, dialect},兼容旧版字符串数组格式);
- 配置文件外部修改时自动刷新列表
- 非递归模式:仅对目标文件本身执行转码,不会处理该文件内调用的函数;
## 转换操作
- 递归模式:转码目标文件的同时,自动对文件中所调用的关联函数一并完成转码。
点击"开始转换"启动转换任务,转换过程中可查看进度条和状态信息,如下:
<img src="转码.png" alt="转码"/>
点击"停止转换"中止当前任务,如下:
<img src="停止转码.png" alt="停止转码"/>
**添加功能(三种方式):**
- 方式一(推荐):进入 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 并生效,无需额外点击。
**执行转码:** 点击 开始转换,进度显示与停止方式同全量转码;任务互斥规则相同。
+21 -8
View File
@@ -13,12 +13,25 @@
``E:\05.code\04.SVN\secu\dep1\hstrade20\trunk\Sources\DevCodes\经纪业务运营平台V21``
<img src="UF20项目环境路径.png" alt="UF20项目环境路径"/>
## UFT3元数据
在 元数据更新 页面,选择更新范围(数据类型、标准字段、表结构、接口数据)并点击"更新UFT30"按钮完成元数据更新。
<img src="UFT3元数据.png" alt="UFT3元数据"/>
**备注:接口数据更新比较久**
## 元数据更新
<img src="元数据更新.png" alt="元数据更新">
## 更新UF20元数据
在 元数据更新 页面,选择更新范围(数据类型、标准字段、标准错误号、接口数据、常量、菜单功能、模块、表结构)并点击"更新UF20"按钮完成公共资源更新
<img src="UF20元数据.png" alt="UF20元数据"/>
**备注:接口数据更新比较久**
**操作步骤:**
1. 左侧导航进入 元数据更新;
2. 页面分为 UFT30更新范围 与 UF20更新范围 两个卡片,勾选需要更新的元数据项(默认全选):
- UFT30:数据类型、标准字段、表结构、接口数据;
- UF20:数据类型、标准字段、标准错误号、接口数据、常量、菜单功能、模块、表结构;
3. 可用 全选 / 全不选 按钮快速调整勾选范围;
4. 点击 更新UFT30 或 更新UF20,页面显示进度与当前处理项;
5. 等待完成提示后,再使用其他功能页面。
**选项说明:**
- 增量快速跳过:增量更新先对比源码目录整体修改时间,源码无任何变化时秒级完成,不再逐文件重复扫描;
- 强制全量更新(进度条右上角复选框):默认增量更新,仅重新处理有变化的文件;勾选后将重新
扫描并解析全部接口文件、重建增量缓存,耗时较久,勾选时需二次确认;
- 源码变更后请重新执行更新,保证索引与实际代码一致;
- 更新过程在后台线程执行,界面不会卡死。
+1 -1
View File
@@ -1,5 +1,5 @@
# 函数替换
uf2函数和uft3函数之间的映射关系,支持增、删、改、查功能
维护 UF20 函数名 → UFT3 函数名的映射,操作方式与表替换相同,支持增、删、改、查功能
<img src="函数替换.png" alt="函数替换"/>
-41
View File
@@ -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`,无需手动同步。
+3 -1
View File
@@ -3,4 +3,6 @@
## 快速跳转
[表替换](http://10.20.163.105:6045/uft3changecode/表替换.html)
[函数替换](http://10.20.163.105:6045/uft3changecode/函数替换.html)
[函数替换](http://10.20.163.105:6045/uft3changecode/函数替换.html)
[不转码功能函数](不转码功能函数.md)
+1 -1
View File
@@ -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):修复设置路径不生效、修复元数据更新后函数检查状态异常
+14 -1
View File
@@ -1,6 +1,19 @@
# 表替换
uf2表和uft3表之间的映射关系,支持增、删、改、查功能
维护 UF20 表名 → UFT3 表名的映射,支持增、删、改、查功能
<img src="表替换.png" alt="表替换"/>
**操作步骤:**
1. 进入 数据映射 → 表替换;
2. 顶部搜索框实时过滤两列内容(不区分大小写);
3. 双击单元格直接修改;
4. 点 添加 弹窗输入 UF20 / UFT3 表名(UF20 表名必填且不可重复);
5. 删除选中 需二次确认(支持多选);
6. 修改后点 保存 生效;保存成功后自动与映射文件立即可查:
- 新增的映射自动追加到该文件;
- 修改一对一映射的内存表名后,文件中同名条目的值同步更新;
- 该文件中配置为一对多数组的条目(一个 UF20 表对应多个内存表)保持不变,不会被表替换中的单值覆盖;
+2
View File
@@ -1,5 +1,7 @@
# 设置
<img src="设置.png" alt="设置.png">
## UFT30项目环境路径
在设置页面,配置UFT30项目路径,转码使用,例如:
+44 -9
View File
@@ -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<QString, QString> 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<QString, QString>(), 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) {
+12 -2
View File
@@ -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 非空)
+7 -3
View File
@@ -132,11 +132,15 @@ std::optional<InterfaceInfo> 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;
+114 -23
View File
@@ -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 <QVBoxLayout>
@@ -12,6 +13,7 @@
#include <QJsonParseError>
#include <QMessageBox>
#include <QRegularExpression>
#include <QStringDecoder>
#include <QLabel>
#include <QStandardItem>
#include <cstring>
@@ -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<QStandardItem *> &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<int>::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<int>::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);
+3 -3
View File
@@ -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<QString, QString> m_coreMap; // 功能名 → "ucbp"/"uses"/"uarg"/"usms"
QHash<QString, QString> m_coreMap; // 功能名 → 核心名(取值来自 config.ini [coreConfig])
QHash<QString, bool> m_viaUmgrMap; // 功能名 → 是否柜面接口
};
+70 -23
View File
@@ -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<QString> 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<QStandardItem*> 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<QString> 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<QStringList, int> 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<int, int> 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<QStringList, int> addResult = addNames(funcNames);
const QStringList &added = addResult.first;
const int skipped = addResult.second;
+14 -3
View File
@@ -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;
};
@@ -9,6 +9,8 @@
#include <QMessageBox>
#include <QSet>
#include <QSpacerItem>
#include <QApplication>
#include <QEventLoop>
#include <QtConcurrent>
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();
+20 -1
View File
@@ -159,6 +159,14 @@ void NonLsConvertPage::attachRowWidgets(int row, const QString &funcName)
(m_recursionMap.value(funcName, RecursionNo) == RecursionYes) ? 1 : 0);
connect(recCombo, QOverload<int>::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<int>::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;
+53 -8
View File
@@ -8,11 +8,13 @@ PythonRunner::PythonRunner(QObject *parent)
, m_killTimer(nullptr)
{
connect(m_process, &QProcess::started, this, &PythonRunner::started);
connect(m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &PythonRunner::finished);
// 进程结束后冲刷残留的不完整序列, 避免最后一块输出丢失
// 进程结束后先冲刷残留的不完整序列, 再向调用方转发 finished: 两个同序直连
// 按注册顺序执行, 调用方必须先看到最后一块输出再收到结束通知, 反序会让
// 结束后的日志汇总/结果判定遗漏尾部输出
connect(m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &PythonRunner::flushPendingOutput);
connect(m_process, QOverload<int, QProcess::ExitStatus>::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<uchar>(pending.at(i));
const int need = (lead & 0xE0) == 0xC0 ? 2
: (lead & 0xF0) == 0xE0 ? 3
+3
View File
@@ -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
+22 -10
View File
@@ -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 消费,
// 生产/消费两侧必须同源, 拼错会静默落入未知状态分支)
+9 -2
View File
@@ -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));
+3
View File
@@ -18,6 +18,9 @@ struct ConfigReaderTraits
// 触发功能缓存后台预热(不阻塞调用线程)。返回 true 表示内存缓存已就绪可立即派生 cname 集合;
// 返回 false 表示预热已在后台进行, 查询方应走单条查库兜底
std::function<bool()> warmupCache;
// cname 轻量直查(主键列单条 SELECT, 毫秒级): 成功时同步填 out 并返回 true,
// 存在性判定无需等全量 JSON 预热; 失败返回 false 走预热/兜底路径
std::function<bool(QSet<QString>&)> loadCnames;
// 单行直查(绕过 cname 内存缓存): 用于数据尚未就绪、内存缓存为空时兜底,
// 避免把“缓存没准备好”误判为“功能不存在”
std::function<bool(const QString&)> checkExists;
+74 -32
View File
@@ -6,14 +6,11 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QCoreApplication>
#include <QEventLoop>
#include <QDir>
#include <QFileInfo>
#include <QDateTime>
#include <QElapsedTimer>
#include <QtConcurrent>
#include <QThread>
#include <atomic>
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<QString> DataCache::getAllCnames(const QString& db, const QString& logTag)
return result;
}
QSet<QString> DataCache::loadCnameSet(const QString& db, const QStringList& tables, bool* ok)
{
if (ok) *ok = false;
QSet<QString> 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<FunctionInfo> DataCache::searchFunctions(const QString& db, const QStringList& keywords,
const QString& field, bool exactMatch, int* keywordsWithoutMatch)
{
@@ -587,28 +642,15 @@ const QList<FunctionInfo>& DataCache::cachedFunctions(const QString& db) const
return cache;
}
// 后台预热在途或已完成未消费: 等待/直接接管其结果, 避免主线程重复全表扫描。
// 注意: 预热任务完成后 isRunning() 变 false, 若只检查 isRunning 会漏掉已完成的
// 预热结果导致主线程再次全量重建, 因此以"是否还有未消费结果"为接管条件。
// 后台预热在途: 不同步忙等。原实现 msleep+processEvents 轮询最多 5s——既阻塞本调用,
// processEvents 又会重入无关信号槽(重入路径再次进入本函数时状态机错乱), 超限后还要
// 在主线程重复全表扫描与后台预热赛跑。现直接返回当前未就绪缓存(空列表), 预热完成后
// QFutureWatcher 广播 functionCacheReady, 依赖方(转码工作台存在列/功能查询页)补刷,
// 补刷触发的再次查询由下方接管逻辑拿到结果
// (预热任务只构建局部列表不写共享成员, 读写仍全部在主线程, 无并发风险)
QFuture<QList<FunctionInfo>>& 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<FunctionInfo>& 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 条")
+6
View File
@@ -104,6 +104,12 @@ public:
// 查询方应走单条查库兜底(用于启动期存在性检查不阻塞主线程)。
bool warmupFunctionCacheAsync(const QString& db);
// cname 轻量直查: 接口表的 name 列即主键(cname), 单条 SELECT 毫秒级完成,
// 免全量 JSON 解析——供存在性判定方在 FunctionInfo 预热完成前同步加载 cname 全集。
// 缺表(如无交易码数据时 trans_code 表不落库)跳过; 库已打开且存在的表均查询成功
// (含合法的空结果)为 ok=true, 库未打开/SQL 出错为 false(返回空集)
QSet<QString> loadCnameSet(const QString& db, const QStringList& tables, bool* ok = nullptr);
signals:
// 指定库功能缓存后台预热完成(结果已可接管)。QFutureWatcher 保证仅在结果就绪后
// 于主线程发出。依赖方(如转码工作台"是否存在"列)借此在预热完成后立即补刷,
+6
View File
@@ -24,6 +24,12 @@ const ConfigReaderTraits& UF2ConfigReader::traits()
traits.warmupCache = [] {
return DataCache::instance()->warmupFunctionCacheAsync(Constants::Cache::Uf20DbName);
};
traits.loadCnames = [](QSet<QString>& 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);
};
+8
View File
@@ -24,6 +24,14 @@ const ConfigReaderTraits& UFT3ConfigReader::traits()
traits.warmupCache = [] {
return DataCache::instance()->warmupFunctionCacheAsync(Constants::Cache::Uft3DbName);
};
traits.loadCnames = [](QSet<QString>& 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 两张表
+2 -2
View File
@@ -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.写日志异常修复"
}
}