Files
change_code/docs/详细设计文档.md

1025 lines
68 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Uft30ChangeCode 详细设计文档
| 文档信息 | 内容 |
| --- | --- |
| 项目名称 | Uft30ChangeCode(UF20 → UFT3 转码工具) |
| 文档版本 | V1.6.0 |
| 更新日期 | 2026-09-14 |
| 技术栈 | Qt 6.6.2(MinGW 64-bit)/ C++ / ElaWidgetTools / SQLite |
---
## 1. 引言
### 1.1 编写目的
本文档面向开发与维护人员,描述 Uft30ChangeCode 转码工具的总体架构、模块划分、核心流程与关键设计决策,作为后续开发、测试与维护的依据。
### 1.2 系统定位
Uft30ChangeCode 是一款桌面端 **UF20 → UFT3 代码转码工具**,提供:
- **元数据更新**:后台解析 UF20/UFT3 源码,构建功能元数据索引并落库(SQLite 双缓存库);
- **业务转码**:**全量转码**(LS_ 功能清单式整体转码,清单存 `cust.json`)与**按需转码**(非 LS 功能,单文件/递归两种模式,清单存 `cust_nonls.json`,转码范围不含核心文件、module 模块文件),转码动作由外部引擎 `bin/uf2touft3/uf2touft3.exe` 执行;
- **代码检索**:功能**调用链路查询**(向上调用方、向下被调),双模式(转码业务/非转码业务),支持导出调用文件与链路文本;
- **功能查询**:检索 UF20 / UFT3 功能元数据,支持**多关键词批量查询**(逗号/分号/空格分隔,OR 并集);UF20 查询结果**双击/右键一键分流**——可多选,LS_ 功能加入全量转码、其它功能加入按需转码,加入结果合并为单次提示;UFT3 仅用于查询核对;
- **数据映射**:维护 UF20 → UFT3 的表替换、函数替换对照表与**不转码功能函数清单**(存 `cust.json`,转码引擎按映射替换代码中的表名/函数名,清单内功能函数名跳过替换);
- **系统设置**:项目环境路径(含 UF20清算项目环境路径)与主题配置(元数据更新与转码引擎依赖这些路径)。
### 1.3 术语约定
| 术语 | 说明 |
| --- | --- |
| UF20 / UFT3 | 新旧两代系统版本,转码即将 UF20 风格代码转换为 UFT3 风格 |
| LS 功能 | "内存交易"改造范围的功能类别;**全量转码仅支持 LS_ 开头功能**,按需转码仅针对非 LS 功能 |
| 递归标志 | `cust_nonls.json` 中 `convert.dir` 条目 `depth` 的取值:`0`=否(不递归),`-1`=是(递归) |
| SQL 方言(dialect) | 转码引擎 SQL 语法转换目标:`mdb` / `db`。全量转码由核心派生(ucbp/uses→mdb,uarg/usms→db);按需转码由"db模式"列选择(否=mdb,是=db) |
| 元数据 | 从源码解析出的功能/函数索引信息,存于 SQLite 缓存库 |
| 调用链路 | 某功能的向上调用方(谁调用了它)与向下被调(它调用了谁)构成的关系树 |
---
## 2. 总体设计
### 2.1 技术选型
| 类别 | 选型 | 说明 |
| --- | --- | --- |
| GUI 框架 | Qt 6.6.2(MinGW 64-bit) | 跨平台 C++ 桌面框架 |
| UI 组件库 | ElaWidgetTools | Fluent 风格控件(ElaWindow / ElaPivot / ElaTableView / ElaComboBox 等) |
| 本地存储 | Qt SQL(QSqlDatabase) | SQLite 双缓存库 `uf20_cache.db` / `uft3_cache.db`,由 `DataCache` 单例统一管理 |
| 并发 | QtConcurrent + 信号槽 | 元数据后台解析、代码索引后台构建,避免阻塞 UI |
| 进程调用 | QProcess(`PythonRunner` 封装) | 调用外部转码引擎(打包 exe,兼容 Python 脚本模式) |
| 文件监听 | QFileSystemWatcher(+ QTimer 防抖) | 监听外部修改 `cust.json`(数据映射/全量转码)与 `cust_nonls.json`(按需转码) |
| 日志 | LogManager(单例) | 统一 info/warning/error 日志输出到 exe 目录 `logs\` |
### 2.2 系统分层架构
```mermaid
flowchart TB
subgraph SHELL["主窗口(src/mainwindow)"]
M1["MainWindow(ElaWindow)<br/>页面注册 / 任务调度互斥 / 引擎调用"]
end
subgraph UI["UI 层(src/pages)"]
A2[元数据更新 MetadataUpdatePage]
A3a["业务转码容器 BusinessConvertPage<br/>(ElaPivot:全量/按需)"]
A3[全量转码 BatchConvertPage]
A4[按需转码 NonLsConvertPage]
A5["功能查询 FunctionSearchPage<br/>(UF20 / UFT3 两个子页)"]
A6[代码检索 CodeSearchPage]
A7["数据映射 TableMappingPage<br/>FunctionMappingPage"]
A8[设置 SettingsPage]
A9[关于 AboutDialog]
end
subgraph CORE["业务核心层"]
B1["业务策略(src/utils)<br/>BusinessStrategy + Factory<br/>UF20 / UFT3 实现"]
B2["配置读取器(src/utils)<br/>ConfigReaderBase + Traits<br/>UF2ConfigReader / UFT3ConfigReader"]
B3["元数据解析器组(src/metadataupdate)<br/>MetaDataProcessor / FileDB<br/>+ uf2* / uft3* 解析器"]
end
subgraph UTIL["公共支撑层(src/utils)"]
C1[DataCache 缓存读写]
C2[CallChainAnalyzer 调用链分析]
C3[ConfigManager 配置管理]
C4[LogManager 日志]
C5[Constants 常量]
C6[VersionChecker 更新检查]
end
subgraph EXT["引擎与外部资源"]
D1[PythonRunner<br/>QProcess 封装]
D2[("bin/uf2touft3/uf2touft3.exe<br/>转码引擎")]
D3[("bin/uf2touft3/cache/<br/>uf20_cache.db / uft3_cache.db")]
D4[("bin/uf2touft3/config/<br/>cust.json / cust_nonls.json")]
D5[("bin/uf2touft3/change_code/<br/>默认代码检索根目录")]
D6[UF20/UFT3 源码目录]
end
M1 --> UI
M1 --> D1
D1 --> D2
A3 -.读写.-> D4
A4 -.监听/读写.-> D4
A7 -.监听/读写.-> D4
A6 -.扫描.-> D5
B3 -.解析.-> D6
B3 --> C1
C1 --> D3
C2 --> C1
UI --> CORE
UI --> UTIL
CORE --> UTIL
```
**分层职责:**
- **主窗口**:ElaWindow 容器与导航注册;**全局唯一的 `PythonRunner` 实例**,接收全量/按需转码页的 `startConvert` 信号,负责**任务互斥**、引擎进程启停与输出回收;
- **UI 层**:页面构建、用户交互、表格/树渲染,不直接访问数据库/源码,统一经由支撑层与核心层完成;
- **业务核心层**:以**策略模式**隔离 UF20/UFT3 差异(`BusinessStrategy`),以**模板方法模式**统一配置读取流程(`ConfigReaderBase` + Traits);元数据解析器组负责将源码解析为结构化键值数据;
- **公共支撑层**:缓存读写、调用链分析、配置、日志、版本检查等横切能力。
### 2.3 目录结构
```
Uft30ChangeCode/
├── src/
│ ├── mainwindow/ # MainWindow(ElaWindow 主窗口 + 导航注册 + 转码任务调度)
│ ├── metadataupdate/ # 元数据解析核心(MetaDataProcessor / FileDB
│ │ # + uf2datatype / uf2stdfields / uf2table / uf2interface
│ │ # / uf2errnumber / uf2menuandfunction / uf2module 等)
│ ├── pythonrunner/ # PythonRunner(外部转码引擎进程封装,QProcess)
│ ├── components/ # 通用组件(ElaSearchEdit 搜索框、InterDialog 弹窗)
│ ├── pages/
│ │ ├── businessconvert/ # 业务转码容器页(ElaPivot:全量/按需 Tab)
│ │ ├── batchconvert/ # 全量转码页
│ │ ├── nonlsconvert/ # 按需转码页
│ │ ├── codesearch/ # 代码检索页(调用链路查询)
│ │ ├── functionsearch/ # 功能查询(FunctionSearchPage 基类 + UF20/UFT3 子页)
│ │ ├── datamapping/ # 数据映射(表替换 / 函数替换 / 不转码功能函数)
│ │ ├── metadataupdate/ # 元数据更新页
│ │ ├── settings/ # 设置页
│ │ └── about/ # 关于(AboutDialog 对话框)
│ └── utils/ # Constants.h / business_strategy / configreaderbase
│ # / uf2configreader / uft3configreader / callchainanalyzer
│ # / datacache / configmanager / logmanager / custjsonio
│ # / version / versionchecker
├── 3rd/ElaWidgetTools/ # 第三方 UI 库
├── bin/
│ ├── Uft30ChangeCode.exe # 构建产物
│ └── uf2touft3/
│ ├── uf2touft3.exe # 转码引擎
│ ├── config/ # cust.json / cust_nonls.json / config.ini / change_code.ini
│ ├── cache/ # uf20_cache.db / uft3_cache.db(元数据缓存库)
│ └── change_code/ # 默认代码检索根目录
└── docs/ # 本文档与使用手册
```
### 2.4 模块清单
| 模块 | 主要类 | 设计模式 | 职责 |
| --- | --- | --- | --- |
| 主窗口导航 | `MainWindow` | — | ElaWindow 容器、页面注册、**转码任务调度与互斥**、引擎进程管理 |
| 业务转码容器 | `BusinessConvertPage` | — | ElaPivot 承载全量/按需两个工作台 |
| 全量转码 | `BatchConvertPage` | — | `cust.json` 的 `convert.dir` 清单管理(仅 LS_ 功能,含核心/是否柜面接口配置)、触发转码 |
| 按需转码 | `NonLsConvertPage` | — | 非 LS 功能清单管理、递归模式与 db 模式(SQL 方言)选择、外部修改同步 |
| 功能查询 | `FunctionSearchPage`(基类)、`UF20/UFT3FunctionSearchPage` | 模板方法(基类复用) | 元数据检索(多关键词批量查询);UF20 双击/右键一键分流加入转码清单(合并单次提示),UFT3 纯查询 |
| 代码检索 | `CodeSearchPage`、`CallChainAnalyzer` | — | 调用链路查询、索引构建与缓存、导出 |
| 数据映射 | `TableMappingPage`、`FunctionMappingPage`、`NoChangeFullNamePage` | — | `cust.json` 表替换/函数替换映射维护、不转码功能函数清单维护(增删改查) |
| 元数据更新 | `MetadataUpdatePage` + `src/metadataupdate/` 解析器组 | 观察者(信号槽) | 后台解析源码、进度上报、原子落库 |
| 设置 | `SettingsPage` | — | 项目环境路径(含 UF20清算项目环境路径 `UftdbSett`)、主题配置 |
| 缓存 | `DataCache` | 单例 | QSqlDatabase 双库读写、功能查询内存缓存 |
| 引擎调用 | `PythonRunner` | — | 外部进程封装(打包 exe / Python 脚本双模式) |
| 日志 | `LogManager` | 单例 | 统一日志 |
| 版本检查 | `VersionChecker` | — | 启动时在线检查新版本(`update_info.json`) |
---
## 3. 全局流程
### 3.1 应用启动流程
```mermaid
flowchart TD
S([main 入口]) --> A[初始化 QApplication / 高分屏属性]
A --> B[初始化 LogManager 日志单例]
B --> C[初始化 ConfigManager 读取全局配置]
C --> D[构造 MainWindow ElaWindow]
D --> E["注册导航页面<br/>功能转码:业务转码 / 代码检索<br/>功能查询:UF20 / UFT3<br/>数据映射:表替换 / 函数替换 / 不转码功能函数<br/>元数据更新;底部:设置 / 帮助 / 关于"]
E --> F[DataCache.initDatabase<br/>打开 uf20_cache.db / uft3_cache.db]
F --> G{缓存库是否有效?}
G -- 是 --> H[正常进入主界面]
G -- 否/为空 --> I[各页面提示: 请先执行元数据更新]
H --> J[VersionChecker 后台检查在线版本]
I --> J
J --> K["singleShot(0) 首帧渲染后:<br/>异步预热 uf20 / uft3 功能缓存"]
K --> L([等待用户操作])
```
> 说明:页面在启动时统一注册到 `MainWindow`(ElaWindow 侧边栏分组导航 + 底部按钮)。`DataCache` 在启动时打开两个缓存库(不存在则自动创建),功能查询使用**内存缓存**(`refreshFunctionCache`),首次查询前需元数据落库。启动流程末尾经 `QTimer::singleShot(0)` 在首帧渲染后提交线程池**异步预热**两库功能缓存(`warmupFunctionCacheAsync`,幂等:已就绪或在途则跳过),用户进入功能查询页时无需再等待主线程重建(详见 §8.6)。
### 3.2 页面导航机制
- `MainWindow` 基于 `ElaWindow` 实现,侧边栏按分组注册页面:**功能转码**(业务转码、代码检索)、**功能查询**(UF20、UFT3)、**数据映射**(表替换、函数替换、不转码功能函数)、**元数据更新**;底部为设置、帮助、关于;
- 业务转码页内部通过 ElaPivot 切换全量/按需两个工作台;
- 页面切换由 ElaWindow 内部路由完成,各页面通过 `showEvent` 感知可见性变化(如按需转码页每次显示时刷新表格,代码检索页每次显示时重读 `cust.json` 清单并按需构建索引)。
---
## 4. 核心数据流
### 4.1 元数据更新流程
元数据更新是全工具的数据基石:解析 UF20/UFT3 源码目录,构建功能/函数索引并写入 SQLite 缓存库。解析在**工作线程**中执行,通过信号槽向页面汇报进度,UI 不卡顿。支持**增量更新**(按源文件 mtime 比对跳过未变更文件)与**强制全量更新**(二次确认后全量重扫)。
```mermaid
sequenceDiagram
participant U as 用户(元数据更新页)
participant P as MetadataUpdatePage
participant R as 元数据解析器组<br/>(src/metadataupdate)
participant T as 工作线程
participant C as DataCache
participant DB as (SQLite 缓存库)
U->>P: 勾选更新范围 / 点击"更新UFT30 / 更新UF20"
P->>R: 启动解析任务(增量 或 强制全量)
R->>T: 提交后台解析
loop 遍历源码文件
T->>T: 解析元数据(uf2* / uft3* 解析器)
T->>P: 信号: 进度 / 当前处理项
T->>C: saveDataset(键值行, srcMtime)
C->>DB: 原子写(临时表 + RENAME)+ 更新 _meta
end
T-->>R: 解析完成
R->>P: 信号: 更新完成(统计结果)
P->>C: refreshFunctionCache()
P->>U: 展示结果
```
**设计要点:**
1. **后台化**:解析与写库全部在工作线程,UI 线程仅收发信号;
2. **原子落库**:`saveDataset` 以"临时表 + RENAME"方式整体替换表数据,并更新 `_meta` 元信息表(记录源文件 mtime,供增量判断);
3. **增量/全量**:默认增量(mtime 比对),且先做**源码目录整体 mtime 快速比对**(0.5s 容差),源码无变化时整个更新秒级跳过;"强制全量更新"复选框需二次确认;
4. **缓存生效时机**:更新完成后调用 `refreshFunctionCache` 重建功能查询内存缓存,各页面"是否存在"判断随之生效。
### 4.2 数据缓存与存在性判断流程
`DataCache` 是所有"是否存在 / 元数据查询"的统一入口,底层经 `QSqlDatabase` 访问 SQLite,功能查询走内存索引缓存。
```mermaid
flowchart TD
A([页面发起查询<br/>如 checkUF20CnameExists / searchUF20Functions]) --> B{内存功能缓存<br/>是否已构建?}
B -- 否 --> C[读取 *interface 表<br/>refreshFunctionCache 建内存索引]
C --> D{加载成功?}
D -- 是 --> E[在内存索引中匹配]
D -- 否 --> F[返回 未找到<br/>并记录 logWarning]
B -- 是 --> E
E --> G{命中?}
G -- 是 --> H[返回 存在 + 元数据<br/>cname / eName / functionNo]
G -- 否 --> F
```
### 4.3 配置读取流程(模板方法 + Traits)
`ConfigReaderBase` 定义统一的读取骨架(打开文件 → 逐段解析 → 校验 → 输出结构化配置),UF20 与 UFT3 的差异通过子类 + `ConfigReaderTraits` 萃取,避免大量 if-else 分支。
```mermaid
flowchart TD
A([调用方指定版本]) --> B{版本?}
B -- UF20 --> C[UF2ConfigReader]
B -- UFT3 --> D[UFT3ConfigReader]
C --> T["Traits&lt;UF2&gt;<br/>关键字段/节名/正则差异"]
D --> T2["Traits&lt;UFT3&gt;<br/>关键字段/节名/正则差异"]
T --> E[ConfigReaderBase::read<br/>模板方法统一骨架]
T2 --> E
E --> F[打开配置文件]
F --> G{打开成功?}
G -- 否 --> X[logWarning + 返回失败]
G -- 是 --> H[逐节解析 parseSection 子类实现]
H --> I{校验通过?}
I -- 否 --> X
I -- 是 --> J[输出结构化配置对象]
X --> K([结束])
J --> K
```
### 4.4 转码执行流程(引擎调用)
转码动作由外部引擎 `bin/uf2touft3/uf2touft3.exe` 执行。`MainWindow` 持有全局唯一 `PythonRunner`(`UsePackagedExe` 模式,兼容 Python 脚本模式),并负责**全量/按需任务互斥**。
```mermaid
sequenceDiagram
participant U as 用户
participant P as BatchConvertPage /<br/>NonLsConvertPage
participant M as MainWindow
participant R as PythonRunner
participant E as uf2touft3.exe(引擎)
U->>P: 点击"开始转换"
P->>M: startConvert 信号
M->>M: beginConvert 任务互斥<br/>(禁用另一 Tab 的开始按钮)
M->>R: 组装参数并启动(UsePackagedExe)
R->>E: QProcess 启动引擎进程
loop 引擎运行
E-->>R: stdout / stderr
R->>M: standardOutput / standardError 信号
M->>M: 前置失败检测(FAIL 行计数)<br/>再解析翻译进度
M->>P: 进度与状态展示(进度条 / 停止转换按钮)
end
E-->>R: finished(exitCode)
R->>M: finished 信号
M->>M: 成败路由: exitCode==0 且 m_failCount==0<br/>→ onConvertFinished / 否则 onConvertFailed
M->>M: 解除任务互斥,展示结果 + 日志
M->>M: 成功时: 代码检索页 refreshIndexSilent()<br/>(双模式静默刷新索引)
```
**要点:**
- **全量转码**:引擎读取 `cust.json` 的 `convert.dir` 清单(V2 对象数组 `{name, core, viaUmgr, dialect}`),对清单内 LS_ 功能整体转码,核心/柜面接口/SQL 方言随清单下发(契约见 §7.5);
- **按需转码**:`NonLsConvertPage::buildArguments()` 组装引擎参数(含 `-m 2` 模式标识),功能清单、递归标志(depth)与 SQL 方言(dialect)由 `cust_nonls.json` 提供,引擎按映射关系(`cust.json`)完成表名/函数名替换;
- **成败判定**:引擎在个别文件转码失败时退出码仍可能为 0,因此 `MainWindow` 在输出解析链最前端检测 `FAIL` 行(`[N/M] FAIL 函数名`)与汇总行(`批量转码完成: X/Y 通过, Z 失败`)累计 `m_failCount`;收尾按 `正常退出 && 退出码==0 && m_failCount==0` 判定成功,失败时页面调用 `onConvertFailed`——进度条**保持当前进度**(不置 100%)、状态文字"转换失败(N 个文件转换失败)"(详见 §8.7);
- **转码后联动**:任务成功后自动静默刷新代码检索索引,保证调用链路反映最新代码。
---
## 5. 业务模块详细设计
### 5.1 业务转码容器(BusinessConvertPage)
**职责**:单页承载全量转码与按需转码两个工作台,顶部 ElaPivot Tab 切换。
- 两个工作台(`BatchConvertPage` / `NonLsConvertPage`)作为子页内嵌;
- **任务互斥**在 `MainWindow` 层统一控制:任一工作台转换期间,另一工作台的"开始转换"按钮禁用并提示等待,避免并发冲突。
### 5.2 全量转码模块(BatchConvertPage)
**职责**:维护 LS_ 功能转码清单(含核心、是否柜面接口配置)并对功能执行整体转码,表格展示"UF20函数名 / 是否存在 / 核心 / 是否柜面接口"(无操作列,双击函数名所在行触发编辑)。
```mermaid
flowchart TD
A([进入全量转码页]) --> B["加载清单<br/>cust.json → convert.dir 对象数组<br/>(V2: {name, core, viaUmgr, dialect})"]
B --> C[updateTable 渲染 ElaTableView]
C --> D{逐行判断}
D --> E["checkUF20CnameExists(函数名)<br/>查询 DataCache"]
E --> F["存在 → 显示 是<br/>不存在 → 显示 否"]
F --> G{用户点击开始转换}
G --> H["startConvert 信号 → MainWindow<br/>任务互斥 + PythonRunner 启动引擎"]
H --> I{转码结果}
I -- 成功 --> K[提示成功 + 日志]
I -- 失败 --> L[提示失败原因 + logWarning]
```
**要点:**
- 清单**仅允许 LS_ 开头**的功能(`validateLsFunctionNames` 校验),非 LS 输入被拒绝并提示切换到按需转码;
- 清单持久化于 `cust.json` 的 `convert.dir` 对象数组(V2 元素 `{name, core, viaUmgr, dialect}`,dialect 由 core 派生:ucbp/uses→mdb、uarg/usms→db);保存时用**正则定位数组做局部替换**,保留文件其余内容与格式;加载兼容 V1 字符串数组(按 core=ucbp、viaUmgr=false、dialect=mdb 兜底);
- `QFileSystemWatcher + QTimer`(300ms 防抖)监听 `cust.json` 外部修改,与手工编辑双向同步;
- "是否存在"列依赖元数据缓存;**首次运行若未执行元数据更新,可能整列显示"否"**(详见 §8.2)。
### 5.3 按需转码模块(NonLsConvertPage)
**职责**:管理"非 LS 功能"的按需转码清单,支持**单文件转码**与**递归转码**两种模式(下拉选择),并为每项功能选择 **db 模式**(SQL 方言:否=mdb,是=db),转码范围不包含核心文件、module 模块文件。清单持久化于 `bin/uf2touft3/config/cust_nonls.json`,并监听外部修改。
#### 5.3.1 数据结构
| 成员 | 类型 | 说明 |
| --- | --- | --- |
| `m_funcList` | `QStringList` | 功能名有序清单(与表格行序一致) |
| `m_recursionMap` | `QMap<QString,int>` | 功能名 → 递归标志(`0`=否,`-1`=是),对应 json 的 `convert.dir[名].depth` |
| `m_dialectMap` | `QMap<QString,QString>` | 功能名 → SQL 方言(`mdb`=否/默认,`db`=是),对应 json 的 `convert.dir[名].dialect` |
| `m_tableModel` | `QStandardItemModel` | 表格模型,列:`UF20函数名 / 是否存在 / 是否递归 / db模式`(双击函数名所在行触发编辑) |
| `m_configWatcher` | `QFileSystemWatcher` | 监听 `cust_nonls.json` 文件与其所在目录 |
| `m_reloadTimer` | `QTimer` | 防抖定时器,外部修改后延迟重载 |
| `m_lastFileMtime` | `QDateTime` | 上次已处理的文件修改时间,用于过滤自身写入触发的变更 |
#### 5.3.2 添加功能流程(来自 UF20 功能查询页双击)
```mermaid
sequenceDiagram
participant U as 用户
participant S as UF20FunctionSearchPage
participant N as NonLsConvertPage
participant J as cust_nonls.json
U->>S: 双击非 LS 函数
S->>S: 校验非 LS / 去重
S->>N: addFunction(函数名)
N->>N: m_funcList.append(name)
N->>N: m_recursionMap.insert(name, 0) // 默认"否"
N->>N: updateTable() // 立即渲染新行
N->>J: saveToCustNonLsJson(m_funcList)
Note over N,J: 写盘后刷新 m_lastFileMtime,<br/>避免 watcher 误判为外部修改
N-->>U: 弹窗"已将功能 [x] 添加到按需转码"
```
#### 5.3.3 表格刷新流程(updateTable)
```mermaid
flowchart TD
A([updateTable]) --> B["removeRows 清空模型<br/>(不使用 blockSignals)"]
B --> C{遍历 m_funcList}
C --> D[构建行: 函数名 / 是否存在<br/>存在性查询 DataCache]
D --> E["是否递归列: setIndexWidget<br/>放置 ElaComboBox(否/是)"]
E --> F["双击函数名所在行触发编辑<br/>(基类统一接线, 以函数名为稳定键)"]
F --> G{还有下一行?}
G -- 是 --> D
G -- 否 --> H["viewport()->update()<br/>resizeRowsToContents()<br/>scrollToTop() / clearSelection()"]
H --> I([刷新完成])
```
**关键设计决策——禁用 `blockSignals`:**
早期版本在重建模型时使用 `m_tableModel->blockSignals(true)` 防止勾选误触发写盘。但 `QTableView` 依赖 `rowsInserted/rowsRemoved` 信号更新内部布局缓存,屏蔽后**新增行不可见**(数据层行数正确、视图层不渲染)。现方案:
- 重建期间不屏蔽信号;
- 误触发防护改由数据侧保证:`m_recursionMap` 先行预填充,下拉切换回调中"**值相等则短路**",重建时 `setCurrentIndex` 不会产生多余写盘。
#### 5.3.4 递归模式切换流程
```mermaid
flowchart TD
A([用户切换下拉 否/是]) --> B["currentIndexChanged(index)"]
B --> C["recursion = index==1 ? -1 : 0"]
C --> D{m_recursionMap 中<br/>旧值 == 新值?}
D -- 是 --> E([短路返回, 不写盘])
D -- 否 --> F[m_recursionMap 更新]
F --> G[saveToCustNonLsJson 持久化]
G --> H[刷新 m_lastFileMtime]
H --> I([完成])
```
#### 5.3.5 外部修改同步流程(文件监听 + 防抖)
```mermaid
flowchart TD
A["QFileSystemWatcher<br/>fileChanged / directoryChanged"] --> B[启动/重启 m_reloadTimer 防抖]
B --> C{"onReloadTimeout:<br/>当前 mtime != m_lastFileMtime ?"}
C -- 否 --> D([忽略: 自身写入或无效变更])
C -- 是 --> E[loadFromCustNonLsJson 重新解析]
E --> F{解析成功?}
F -- 是 --> G[m_funcList / m_recursionMap 更新<br/>updateTable 重绘]
F -- 否 --> H[logWarning + 保留内存现状]
G --> I([界面与磁盘一致])
H --> I
```
**要点:**
- `fileChanged` 在部分编辑器保存时会**移除监听**,回调中检测到即对路径重新 `addPath`,保证持续监听;
- 目录监听用于捕获"文件被删除重建"的场景;
- mtime 比较过滤掉程序自身写盘引发的回环刷新;
- **兼容旧版格式**:旧版 `convert.dir` 为数组(无递归标志与方言)或值为数值(`0`/`-1`,无方言)均可自动兼容,均按"否/mdb"兜底加载;新版为对象 `{功能名: {depth, dialect}}`,引擎兜底键 `"*"`(数值)由程序保存时原样保留。
#### 5.3.6 按需转码执行模式
| 模式 | 递归标志 | 行为 |
| --- | --- | --- |
| 非递归(单文件) | `0` | 仅对目标文件本身执行转码,不处理该文件内调用的函数 |
| 递归 | `-1` | 转码目标文件的同时,自动对文件中所调用的关联函数一并完成转码 |
两种模式均**不包含核心文件、module 模块文件**。执行时 `buildArguments()` 组装引擎参数(含 `-m 2`),由 `MainWindow` 统一经 `PythonRunner` 启动引擎。
### 5.4 功能查询模块(FunctionSearchPage 基类 + UF20/UFT3 子页 + UF20 & 内存表映射查询页)
**职责**:检索 UF20 / UFT3 功能元数据(依赖 `DataCache`),列:`函数名 / 英文名 / 功能号`;支持函数名/功能号字段切换、精确匹配开关与**多关键词批量查询**。UF20 页结果表支持多选,双击/右键一键加入转码工作台;UFT3 页为纯查询(不接入转码链路)。
#### 5.4.1 多关键词批量查询
- **拆词**:查询框输入按**中英文逗号、分号与空白**统一拆分为关键词列表(`splitKeywords`,全角字符以 `QStringLiteral` 字符串字面量处理,规避多字节字符常量问题);
- **匹配语义**:多关键词之间为 **OR 并集**——任一关键词在所选字段(函数名/功能号/任一)命中即收录结果;
- **去重**:结果按 `cname + '|' + functionNo` 组合键去重(同一功能可能被多个关键词或多个字段重复命中);
- **零命中统计**:`DataCache` 多词检索接口带 `keywordsWithoutMatch` 出参,返回零命中关键词个数;
- **接口形态**:单词版 `searchUF20Functions(keyword, ...)` 内部委托多词版(`QStringList{keyword}`),两库多词重载共用匿名命名空间 `filterFunctionInfos()` 的单次遍历实现。
**查询反馈(tip 栏)**:页面底部提示栏随查询状态变化——空关键字提示"请输入查询内容"、无结果提示"⚠️ 未找到匹配的功能"、成功后显示"已查询 X 个业务功能函数(M 个无匹配)"并拼接默认操作提示(UF20 含双击/右键操作说明与批量查询示例),全程不弹模态对话框;提示文字颜色经 `ElaTheme` 主题取色并连接 `themeModeChanged` 信号联动刷新,日间/夜间模式均可读。
#### 5.4.2 UF20 功能查询一键加入转码工作台
UF20 页结果表为**扩展多选**(ExtendedSelection)+ 自定义右键菜单;**双击与右键共用**收集逻辑 `addSelectedRowsToConvert()`:收集选区(或右键行)按 `LS_` 前缀分流为两个列表,**一次 emit** 合并信号:
```mermaid
sequenceDiagram
participant U as 用户
participant S as UF20FunctionSearchPage
participant M as MainWindow
participant B as BatchConvertPage(全量)
participant N as NonLsConvertPage(按需)
U->>S: 双击 / 右键"一键添加功能"(可多选)
S->>S: 收集选区,按 LS_ 前缀分流
S->>M: addFunctionsToConvert(lsNames, nonLsNames)
M->>B: addFunctionsBatch(lsNames, notify=false) 静默
M->>N: addFunctionsBatch(nonLsNames, notify=false) 静默
Note over B,N: 各自去重(已存在跳过)、写 cust.json / cust_nonls.json;<br/>返回 {新增数, 跳过数},保存失败返回 {-1,-1} 并弹警告
M->>U: 合并为单个提示框(RichText):<br/>"已将 N 个功能加入全量转码,已将 M 个功能加入按需转码"
```
**要点:**
- **单次提示**:两工作台 `addFunctionsBatch` 支持静默模式(`notify=false` 不弹任何提示框),由 `MainWindow` 统一汇总——加入成功后**只弹一个**消息框,"全量转码"/"按需转码"字样经 `Qt::RichText` **加粗**;重复项自动跳过并注明"另有 N 个已存在";全部已存在时提示"均已在转码列表中";
- **右键交互**:右键行不在选区内时设为唯一选中(Windows 惯例),在选区内则保持多选;空白区域右键不弹菜单;
- 批量添加**逐条免弹窗**,替代早期逐条模态确认。
> **UFT3 功能查询**:界面与 UF20 版一致(含批量关键词查询),仅用于核对 UFT3 元数据,**双击/右键不加入转码清单**;代码检索中"原代码已支持"行的功能号前缀即来自 UFT3 元数据。
#### 5.4.3 UF20 & 内存表映射查询页(TableSearchPage)
**职责**:检索 UF20(2.0)表与内存表(UFT3)之间的映射关系,四列结果:`UF20表名 / UF20表中文名 / 内存表名 / 内存表中文名`;**双击记录行可添加映射到「数据映射 → 表替换」页**。不继承 `FunctionSearchPageBase`(结果结构为 4 列表信息,与 `FunctionInfo` 3 列函数信息不同),UI 风格(边距/搜索框/结果表/主题感知 tip)与功能查询页对齐。
**数据源三件套**:
| 列 | 来源 |
| --- | --- |
| UF20表名 / 内存表名 | `bin/uf2touft3/config/table_mapping.json`(独立映射文件,顶层键 `tableCompareUf2_Uf3`:key=UF20表名,value=内存表名;value 可为字符串(一对一)或字符串数组(一对多,拆为多条映射对,一个 UF20 表展示多行)),每次查询现读(文件小) |
| UF20表中文名 | `uf20_cache.db` 的 `uf2_table`:`data` JSON 的 `cname` 字段 |
| 内存表中文名 | `uft3_cache.db` 的 `uft3_table`:`data` JSON 的 `chineseName` 字段 |
**查询流程**(每关键词,支持多关键词 OR 并集,`splitKeywords` 与功能查询页同口径):
1. **映射命中**(key 或 value)→ 完整行:以 key 取 UF20 中文名、以 value 取内存表中文名;一对多映射(value 为数组)拆为多条记录展示;
2. **映射未命中** → 双库单侧探测:命中 `uf2_table` 出单侧行(映射两列置空),命中 `uft3_table` 同理,两侧同时命中出两行;
3. **去重**:映射整体行已覆盖的表名不再补单侧行(covered 集合),同一映射对不被多关键词重复输出(seenPairs 集合);
4. 精确匹配为全等;模糊匹配为不区分大小写的包含。
**轻量数据访问**:两库 `data` 列为完整表定义 JSON(单行可达数十 KB),禁止整表搬运逐行解析——`DataCache` 新增 `getUf20TableNameMap()` / `getUft3TableNameMap()`,SQL 层 `json_extract(data, '$.cname' / '$.chineseName')` 只提取中文名,首次调用全量构建"表名→中文名"映射后**内存缓存**(懒加载,表数量远小于接口表,秒级内完成),元数据落库后随 `refreshFunctionCache()` 一并失效重建;构建失败不置有效、下次自动重试;`json_extract` 不可用时回退 `getAllRows` + C++ 逐行解析兜底。
**tip 反馈**(页面底部,不弹窗):默认提示"💡 输入 UF20 表名或内存表名进行查询,支持精确/模糊匹配;双击记录行可添加到「数据映射 → 表替换」";空输入提示输入表名;双库均为空提示"请先在「元数据更新」页面执行更新";无命中提示"未找到匹配的表";成功显示"已查询 X 条记录(其中 Y 个无映射关系,N 个表中文名缺失,M 个关键词无匹配)"——一对多映射按多条记录计;中文名缺失指映射里有该表但缓存库未收录(元数据更新未覆盖其来源工程,如 `table_mapping.json` 覆盖完整标准套件而本地仅更新了部分工程);`table_mapping.json` 读取失败时附加降级说明(仅展示数据库检索结果)。
#### 5.4.4 双击添加映射到表替换页
查询结果行**双击**即可将映射关系添加到「数据映射 → 表替换」页,跨页信号链路与 UF20 功能查询一键加入转码工作台同构:
```mermaid
sequenceDiagram
participant U as 用户
participant S as TableSearchPage(映射查询)
participant M as MainWindow
participant T as TableMappingPage(表替换)
U->>S: 双击结果行
S->>S: 取第 1 列(UF20表名)/第 3 列(内存表名),<br/>任一为空即无完整映射关系,tip 警告
S->>M: requestAddMapping(uf20Name, memName)
M->>T: appendPairIfAbsent(uf20Name, memName)
Note over T: trimmed 口径判重(与添加/收集同口径);<br/>已存在返回提示(含当前映射值,不覆盖);<br/>新增入模型置脏后立即落盘(自动保存)
M->>S: onMappingAddResult(error)
S->>U: tip 反馈:成功"已添加到表替换并自动保存" / 失败原因
```
**要点:**
- **完整映射校验**:仅当该行 UF20 表名与内存表名均非空(即有完整映射关系)才可添加;单侧行(无映射)双击提示警告不入库;
- **判重不覆盖**:UF20 表名在表替换中已存在时跳过,tip 注明当前映射值——与表替换页手动添加口径一致,避免隐式覆盖;
- **自动保存语义**:`appendPairIfAbsent` 追加到模型并置 `m_dirty` 后立即调用 `saveToDisk()` 落盘(无弹窗,成功静默)——`onSave` 的弹窗壳与落盘核心分离,保存窗口(监控抑制)逻辑共用;自动保存失败时原因带回映射查询页 tip 展示;
- **table_mapping.json 同步**:表替换页覆写基类落盘钩子 `onSavedToDisk()`——保存成功(含双击自动保存)后,以表替换模型为源与 `table_mapping.json` 的 `tableCompareUf2_Uf3` 对象逐成员对账:
- **新增**:模型中存在、tm 中缺失的 key(且 value 非空)按原缩进/换行格式追加到对象末尾;
- **修改**:tm 中为字符串值(一对一)且与模型 value 不同的条目就地更新为模型值;
- **一对多数组跳过**:tm 中 value 为数组的条目(41 个)一律保留原文不覆盖——表替换是单值模型,覆盖会丢失其余映射;跳过数计入同步日志;
- **删除暂不同步**:表替换删除条目仅作用于 cust.json(转码口径),tm 中同名条目保留(tm 是全量参考映射,含 328 个 cust 未维护的独有条目);空 value 也不同步;
- **实现要点**:成员级文本解析(key 串/冒号/value 串或跨行数组,括号配对且跳过字符串字面量,记录每个成员原文起止),未变更成员与数组保持原文字节不变,仅替换/追加成员行后按原缩进与换行重建对象内部;写前整体重解析校验,任何结构异常放弃写入并记错误日志;同步结果记 info 日志(新增 N,更新 M,一对多数组跳过 X,空值跳过 Y);
- 映射查询页每次查询现读该文件,同步后的新增/修改即时可查;
- **页面持有**:`MainWindow` 新增 `m_tableMappingPage` 成员,页面创建 lambda 运行时解引用(构造时序上映射查询页先建、表替换页后建,两页构造完成后成员指针非空)。
### 5.5 代码检索模块(CodeSearchPage + CallChainAnalyzer)
**职责**:功能**调用链路查询**(非内容检索)——展示某功能的向上调用方(谁调用了它)与向下被调(它调用了谁),用于转码前评估影响范围、转码后核对结果。
#### 5.5.1 双模式设计
页面顶部 ElaPivot 切换两种模式,各模式**独立的分析器实例、检索路径、调用链树**,互不干扰:
| 模式 | 检索入口 | 导出 |
| --- | --- | --- |
| 转码业务 | 左侧"业务转码函数列表"(每次显示时重读 `cust.json` 的 `convert.dir`),双击查询 | 导出调用文件(复制需转码文件)+ `_call_chain.txt` |
| 非转码业务 | 顶部输入行:**函数名**(支持 `功能号-函数名` 带前缀格式)或**功能号**(经 UFT3 元数据解析为函数英文名)查询 | 仅导出 `_call_chain.txt` |
检索路径按模式持久化于 `change_code.ini` 的 `[codeSearch]` 节(`convertPath` / `searchPath`)。
#### 5.5.2 索引构建与缓存加速
```mermaid
flowchart TD
A([显示页面 / 切换模式]) --> B{索引已就绪?}
B -- 是 --> Z([直接可用])
B -- "否,且路径未被手动更改" --> C{"cacheQuickValid<br/>快速粗判缓存(stat 根目录子项,毫秒级)"}
C -- 命中 --> D["后台 loadFromCache<br/>(跳过指纹校验,秒级加载)"]
C -- 未命中 --> E[后台 buildIndex 全量构建]
D -- 失败 --> E
E --> F[saveToCache 写缓存]
D --> Z
F --> Z
B -- "否,且路径被手动更改" --> G[提示用户点击"刷新索引"手动触发]
```
- 构建与缓存加载均在 `QtConcurrent` 后台线程执行,界面显示模态进度框;
- **手动更改路径**后需点"刷新索引"重新加载(弹窗提示);
- 每次转码任务完成后 `MainWindow` 调用 `refreshIndexSilent()`:双模式**静默**(无弹窗)依次后台刷新并写缓存。
#### 5.5.3 调用链查询与树渲染
```mermaid
flowchart TD
A([runQuery 函数名]) --> B["buildCallTree(向下被调链)<br/>buildCallerForest(向上调用方森林,每条链以向下链收尾)"]
B --> C[populateTree 渲染调用链树]
C --> D["树列:函数名 / 目录 / 模块<br/>默认展开 2 级,列宽按内容自适应"]
D --> E{节点状态}
E -- ok --> F[普通行]
E -- notfound --> G["[未找到] 红色字"]
E -- cycle --> H["[循环调用] 橙色字"]
E -- depthlimit --> I["[层级截断]"]
E -- supported --> J["[原代码已支持] 整行绿色高亮<br/>行前带 UFT3 功能号前缀(功能号来自 UFT3 元数据)"]
D --> K["当前查询节点整行蓝色高亮<br/>(自定义 Delegate 强制绘制,绕过 Ela 主题覆盖)"]
```
- 状态 `supported` 表示函数未在检索目录源码中找到,即无需转码、原代码已支持;
- 高亮经 `RowHighlightRole` / `RowHighlightBlueRole` + 自定义 `CallTreeDelegate` 强制绘制,避免 Ela 主题覆盖背景色。
#### 5.5.4 导出设计
- **导出调用文件**(转码业务):收集链路上所有非 `supported` 节点的源文件,按相对检索根的目录结构复制到 `<检索路径>\function_output\<查询函数名>\`,并生成链路文本 `_call_chain.txt`(缩进树形,含状态标记);
- **导出调用链路**(非转码业务):仅生成 `_call_chain.txt`,不复制源文件;
- 目录名经 `sanitizeDirName` 清洗 Windows 非法字符。
### 5.6 数据映射模块(MappingTablePage 基类 + TableMappingPage / FunctionMappingPage / NoChangeFullNamePage)
**职责**:维护 UF20 → UFT3 的**表替换**(`tableCompareUf2_Uf3`)与**函数替换**(`functionReplace`)映射,以及**不转码功能函数清单**(`noChangeFullNameCode`,字符串数组——转码时清单内功能函数名保持原样、不做全名替换)。三页共同继承公共基类 `MappingTablePage`(UI 骨架收编),读写 `cust.json` 对应顶层键经 `CustJsonIo`(见 §6.8)与 `Constants::CustKeys`(见 §6.4);转码引擎按映射替换代码中的表名/函数名。
**MappingTablePage 公共基类**(`src/pages/datamapping/mappingtablepage.h/.cpp`):收编三页原本同构的 UI 骨架与交互样板——搜索框 + `QStandardItemModel` + 参数化列数的 `MappingFilterProxyModel`(不区分大小写 contains 过滤)+ `ElaTableView` + 添加/删除选中/保存按钮 + tip 标签 + `JsonFileMonitor`(见 §6.9)+ 主题联动。三页仅以纯虚点注入差异:
| 虚点 | 说明 |
| --- | --- |
| `populateModel(custJson)` | 从 cust.json 填充模型(`load()` 内调用) |
| `collectMappingData(custJson, errorMessage)` | 保存时校验并回填数据,返回 `overrideRaw`(键名→格式化原文);校验失败置 errorMessage(弹警告不落盘) |
| `onAddRow()` | 添加一行(各页数据结构不同:对象条目 / 字符串数组) |
可覆写文案虚点:`loadFailureText` / `saveLogText` / `reloadLogText` / `deleteConfirmSingle` / `deleteConfirmMulti`。子类在构造体内调用非虚的 `load()`(规避构造期虚函数绑定到基类版本)。基类统一实现:删除多选二次确认、就地编辑脏标志(`m_dirty`,`onModelItemChanged` 中 `m_isLoading` 保护)、保存流程(见下时序)、外部修改重载(脏数据不覆盖、弹确认)与 tip 样式联动。
```mermaid
flowchart TD
A([进入 表替换 / 函数替换 / 不转码功能函数页]) --> B["readCustJson 读取映射节点"]
B --> C["populateModel 渲染表格<br/>QSortFilterProxyModel 不区分大小写过滤"]
C --> D{用户操作}
D -- 双击 / F2 编辑 --> E[更新模型 + 置脏标志]
D -- 添加 --> F["onAddRow:弹窗名称必填 + 查重"]
D -- 删除选中 --> G["onDeleteRow:二次确认(支持多选)"]
E --> H["onSave:beginSaveWindow"]
F --> H
G --> H
H --> I["collectMappingData 校验回填<br/>读取原始文本提取顶层 key 顺序"]
I --> J["CustJsonIo::writeCustJsonOrdered 按序原子写回<br/>endSave 结束保存窗口"]
K[外部编辑 cust.json] -. JsonFileMonitor::reloadRequested .-> L["onReloadRequested:脏数据弹确认<br/>否则 loadData 立即刷新"]
```
### 5.7 元数据更新页面(MetadataUpdatePage)
**职责**:元数据更新的人机入口——勾选更新范围、触发更新、展示进度与统计结果(见 §4.1 时序图)。
- 页面分 **UFT30更新范围** 与 **UF20更新范围** 两个勾选卡片(UFT30:数据类型/标准字段/表结构/接口数据;UF20:数据类型/标准字段/标准错误号/接口数据/常量/菜单功能/模块/表结构),提供全选/全不选快捷操作;
- **强制全量更新**复选框(勾选需二次确认):默认增量更新(mtime 比对),勾选后全量重扫重建;
- 源码扫描范围来自设置页配置的项目环境路径(见 §5.8)。
### 5.8 设置页面(SettingsPage)
**职责**:环境路径与界面主题配置。
| 配置项 | 说明 |
| --- | --- |
| UFT30 / UF20 / UF20账户项目环境路径 | 元数据更新与转码引擎的源码扫描根目录 |
| UF20清算项目环境路径 | UF20 清算参数项目目录,写 `config.ini` 的 `[projectPath] UftdbSett`,供清算相关元数据处理使用 |
| 转码生成路径 | 转码结果输出目录 |
- 路径保存于 `config.ini` 的 `[projectPath]` 节(含 `UftdbSett`);"保存"与"恢复默认"统一经 `ConfigManager::saveConfig()` **保留布局式写回**(手工注释/键顺序/外部新增键不丢失,见 §6.3),程序自动同步维护 `uft30pub = <UFT30路径>\upub\dev_codes`;
- 主题模式(日间/夜间)即时切换;"恢复默认"需二次确认。
---
## 6. 公共支撑模块
### 6.1 DataCache(单例)
- 元数据缓存的门面:双库(`uf20_cache.db` / `uft3_cache.db`,位于 `bin/uf2touft3/cache/`),由 QSqlDatabase 直接管理,无独立 SQLite 封装类;表结构统一为 `(name TEXT PRIMARY KEY, data TEXT)`(`data` 存 JSON),与 Python 侧 `src/store/schema.py` 对齐;
- **原子写**:`saveDataset()` 先写临时表再 `RENAME` 替换正式表,随后更新 `_meta` 表记录源文件 mtime,供增量更新判断;
- **写闸门**:`saveDataset()` 全程持有 `m_writeGate` 写闸门;`clearCache()` 以 `tryLock` 探测在途落库任务——在途时仅重置内存状态并记录警告、跳过连接重置,避免关闭正在写入的连接(闸门在 `database()` 取连接之后获取,锁序安全无死锁,详见 §8.4);
- **内存功能索引**:`refreshFunctionCache()` 将两库功能表加载至内存(`FunctionInfo{cname, eName, functionNo}`),支撑功能查询页即时检索;启动时 `MainWindow` 经 `warmupFunctionCacheAsync(db)` **异步预热**(后台线程构建内存索引,幂等:已就绪/在途跳过,详见 §8.6);
- **检索 API**:`searchUF20Functions()` / `searchUFT3Functions()` 支持按中文名/英文名/功能号字段过滤,均有**单词与多词两个重载**——多词版(`QStringList` + `keywordsWithoutMatch` 出参)实现 OR 并集、按 `cname|functionNo` 去重与零命中统计,单词版内部委托多词版;两库共用匿名命名空间 `filterFunctionInfos()` 过滤实现——代码检索"按功能号检索"即经 `searchUFT3Functions(raw, "function_no", true)` 将功能号解析为函数英文名;
- **数据集 API**:`datasetExists()` / `getAllRows()` / `getRowData()` 供功能查询页读取指定功能完整数据;
- **表查询映射缓存**:`getUf20TableNameMap()` / `getUft3TableNameMap()` 返回"表名→中文名"全量轻量映射(SQL 层 `json_extract` 只取中文名字段,避免整表搬运大 JSON;`json_extract` 不可用时回退 `getAllRows` + 逐行解析),首次调用构建后内存缓存,随 `refreshFunctionCache()` 一并失效重建,供 UF20 & 内存表映射查询页使用(见 §5.4.3)。
### 6.2 LogManager(单例)
- 两步初始化:`initLogConfig()` 读取 `change_code.ini` 的 `[log]` 节(级别/开关),`initLogFile()` 按"日期前缀"命名在 `exe/logs/` 目录创建日志文件;
- 提供 `log / logInfo / logWarning / logError` 四级接口,MainWindow、ConfigManager、ConfigReaderBase 及各页面统一经单例写入。
### 6.3 ConfigManager(单例)
- 管理 `config.ini`,启动时按节全量加载至内存缓存(UTF8/本地编码探测);路径白名单键(`uft30` / `uf20` / `ufAcct20` / `UftdbSett` / `createPath` / `uft30pub`)加载时统一归一化为正斜杠;
- `setValue()` 仅更新内存缓存并记录脏键(缓存中路径键恒为正斜杠,与加载侧对称,任何来源不再漂移),由设置页显式调 `saveConfig()` 落盘;
- `saveConfig()` 为**保留布局式写回**:以磁盘当前内容为基底逐行输出,仅替换"显式 setValue 过"的键行(写盘时换回反斜杠),**手工注释、空行、节顺序、外部新增键一律原样保留**,缓存中有而文件没有的键在对应节尾追加;落盘前经 mtime 探测外部手编,对应用未显式修改过的键采纳磁盘值(外部已有键的修改不被内存覆盖),并以 `QSaveFile` 原子写。
### 6.4 Constants.h(全局常量)
| 常量 | 值 | 用途 |
| --- | --- | --- |
| ToolDir / ChangeCodeDir / FunctionOutput / CacheDir | `uf2touft3` / `change_code` / `function_output` / `cache` | 引擎目录与输出子目录 |
| Exe | `uf2touft3.exe` | 转码引擎可执行文件名 |
| CustJson / CustNonLsJson | `config/cust.json` / `config/cust_nonls.json` | 转码配置文件 |
| CustKeys::ConvertDir / TableCompare / FunctionReplace / NoChangeFullName | `convert` / `tableCompareUf2_Uf3` / `functionReplace` / `noChangeFullNameCode` | cust.json 顶层键名集中定义(含 cust_nonls.json 的 `convert`),数据映射三页与转码工作台统一引用,避免散落字符串字面量 |
| Uf20DbFile / Uft3DbFile | `uf20_cache.db` / `uft3_cache.db` | 双缓存库文件名 |
| ChangeCodeIni | `change_code.ini` | 路径持久化与日志配置文件 |
| CodeSearch::ConvertPathKey / SearchPathKey | `codeSearch/convertPath` / `codeSearch/searchPath` | 代码检索路径配置键 |
| updateInfo() | `<Base>/download/uft3changecode/update_info.json` | 版本检查地址 |
另含界面表格列索引、递归扫描标志等常量,集中管理避免魔法数字。
### 6.5 CallChainAnalyzer(调用链分析)
代码检索页持有两个实例(转码业务 / 非转码业务),各自独立索引与缓存。
| 环节 | 方法 | 说明 |
| --- | --- | --- |
| 全量构建 | `buildIndex` | 扫描源码目录解析调用关系,完成后 `saveToCache` 持久化 |
| 快速粗判 | `cacheQuickValid` | 比对源目录特征与缓存指纹,秒级判断缓存是否可复用 |
| 缓存加载 | `loadFromCache(skipFingerprint)` | 命中时秒级恢复索引;`skipFingerprint=true` 跳过指纹校验强制重载 |
| 最优匹配 | `lookupBest` | 将输入函数名解析为索引中的目标函数 |
| 向下被调 | `buildCallTree` | 以查询函数为根展开"它调用了谁" |
| 向上调用方 | `buildCallerForest` | 反向展开"谁调用了它" |
- 节点状态:`ok` / `notfound`(索引中未找到)/ `cycle`(循环调用,终止展开)/ `depthlimit`(超出深度上限)/ `supported`(转码业务中该函数已支持转码,绿色标识);
- 查询流程:双击列表行 → `runQuery` → buildCallTree + buildCallerForest → 填充树控件 → `expandToDepth(2)` 默认展开两层;蓝色当前行、绿色 supported 行经自定义 Role 由 CallTreeDelegate 强制绘制。
### 6.6 PythonRunner(引擎进程封装)
- `RunMode` 枚举:`UsePackagedExe`(打包 exe)/ `UsePythonScript`(Python 脚本)/ `AutoDetect`;本程序固定采用 UsePackagedExe,指向 `bin/uf2touft3/uf2touft3.exe`;
- 封装 QProcess:`start()` 启动、`stop()` 终止、`runSync()` 同步等待;`start()` 为**异步启动**(不再 `waitForStarted` 阻塞 UI 线程),重复发起检查覆盖 `Starting/Running` 状态;
- 信号:`finished(exitCode)` / `standardOutput(QString)` / `standardError(QString)` / `errorOccurred(QProcess::ProcessError)`;MainWindow 将输出汇聚至页面日志区,`finished` 用于解除任务互斥,`errorOccurred` 的 `FailedToStart` 用于启动失败收尾(详见 §8.5)。
### 6.7 VersionChecker
- 启动时经 QNetworkAccessManager 拉取 `update_info.json`,解析多版本发布列表,取 `Version::compare` 比较后的最大版本为最新版;
- 与当前版本比较后发射 `newVersionAvailable(latestVersion, releaseNotes)` / `checkError(msg)` / `noUpdateNeeded()`,MainWindow 弹出更新提示,"关于"页亦可手动检查。
### 6.8 CustJsonIo(cust.json 保序读写工具)
命名空间级公共工具(`src/utils/custjsonio.h/.cpp`),供数据映射三个页面(表替换 / 函数替换 / 不转码功能函数)共用,消除原先两份重复实现:
| 函数 | 说明 |
| --- | --- |
| `extractTopLevelKeyOrder(rawJson)` | 深度感知扫描原始 JSON 文本(跳过字符串字面量),仅记录 depth==1 时后跟冒号的顶层键,返回键顺序;正则方案无法匹配中文/特殊字符键且会误捕嵌套键,故弃用 |
| `indentJsonValue(json)` | 对 JSON 文本整体缩进(每行前加 4 空格) |
| `formatJsonValue(value)` | 通用 JSON 值格式化:对象/数组走 `QJsonDocument::Indented` 再整体缩进,字符串手工转义,其余按 bool/number/null |
| `formatOrderedJsonObject(pairs)` | 按给定顺序格式化 string→string 对象(条目 8 空格缩进),供映射页输出 |
| `formatOrderedJsonArray(items)` | 按给定顺序格式化字符串数组(条目 8 空格缩进,空数组输出 `[]`),供不转码功能函数页输出 |
| `writeCustJsonOrdered(jsonPath, json, keyOrder, overrideRaw)` | `QSaveFile` 原子写:按 keyOrder 顺序输出顶层键,`overrideRaw` 中的键用调用方自定义格式化文本重写,其余键走 `formatJsonValue`;保存失败记录错误日志 |
### 6.9 JsonFileMonitor(JSON 配置外部修改监控组件)
公共监控组件(`src/utils/jsonfilemonitor.h/.cpp`),收编原先散落在 5 个页面(数据映射三页 + 全量转码页 + 非转码业务页)的**文件+目录双监控样板**——每处约 50 行的 QFileSystemWatcher 事件处理、防抖、回环过滤逻辑统一为一个可复用组件,页面侧仅剩 `watch()` + `connect(reloadRequested)` + 保存时 `beginSaveWindow()/endSave()` 三行。
| 成员/方法 | 说明 |
| --- | --- |
| `watch(filePath)` | 切换监控目标:文件 + 所在目录双监控(Windows 下外部程序整文件替换会使文件句柄失效,目录事件兜底补挂) |
| `refreshWatch()` | 补挂监控(fileChanged 后文件路径可能已从 watcher 移除,防抖触发前先补挂) |
| `currentMtime()` | 当前文件 mtime 基准(文件不存在返回 0) |
| `beginSaveWindow()` / `endSave()` | 保存窗口:窗口期内监控事件直接忽略;`endSave()` 立即提升保存代数(`m_saveSeq`),使窗口内已排队的防抖事件全部过期,替代旧实现"保存后固定 2 秒忽略"的硬等待 |
| `reloadRequested()` 信号 | 外部修改确认(300ms 防抖 + mtime 基准过滤自身写入回环后)单发;页面侧检查脏标志后重载 |
**回环过滤原理**:`beginSaveWindow()` 预置当前 mtime 为基准;监控事件到达时若 `mtime <= 基准` 或处于保存窗口/保存代数过期,视为自身写入引发的变化,不触发重载——自身保存经 `endSave()` 提升基准后,外部手编(mtime 变大)才会命中。
---
## 7. 数据设计
### 7.1 SQLite 缓存库
- 位置:`bin/uf2touft3/cache/`,双库 `uf20_cache.db` / `uft3_cache.db`;
- 每个元数据文件一张表:`(name TEXT PRIMARY KEY, data TEXT)`,`data` 为该文件解析结果的 JSON;
- `_meta` 表记录每张表对应的源文件 mtime,元数据更新时据此跳过未变化文件(增量更新);
- 原子写:新建临时表插入完成后 `RENAME` 覆盖正式表,避免中途失败留下半写状态。
### 7.2 cust.json(转码配置)
| 配置 | 内容 | 消费方 |
| --- | --- | --- |
| `tableCompareUf2_Uf3` | UF20→UFT3 表名映射 | 全量/按需转码的表替换 |
| `functionReplace` | 函数替换规则 | 转码的函数替换 |
| `noChangeFullNameCode` | 字符串数组(功能函数全名清单) | 转码引擎对清单内功能函数名不做全名替换 |
| `convert.dir` | 对象数组 `{name, core, viaUmgr, dialect}`(全量转码清单,V2 格式,见 §7.5) | 全量转码页维护、引擎读取 |
| 其余键 | 引擎自身配置 | 原样透传 |
- 数据映射三页各自只读写所属顶层键(键名统一引用 `Constants::CustKeys`,见 §6.4),其余内容原样保留;
- 程序保存时经 `CustJsonIo::extractTopLevelKeyOrder` 提取顶层 key 顺序、`CustJsonIo::writeCustJsonOrdered` 按原顺序 `QSaveFile` 原子写回,不破坏 JSON 键序(见 §6.8)。
### 7.3 cust_nonls.json(非转码业务配置)
```json
{
"convert": {
"dir": {
"功能名": { "depth": 0, "dialect": "mdb" },
"另一功能": { "depth": -1, "dialect": "db" },
"*": 0
},
"ext": ["c", "h"]
}
}
```
- `dir.<功能名>`:对象 `{depth, dialect}`——`depth` 为 `0` 纳入转码(单文件)、`-1` 递归转码;`dialect` 为 SQL 方言(`mdb` 默认 / `db`),对应界面"db模式"列(否=mdb、是=db);`"*"` 为兜底键(数值,程序保存时原样透传);
- `ext`:纳入转码的扩展名白名单;
- 兼容旧版数组格式(仅扩展名列表),读取时自动转换。
### 7.4 ini 配置
| 文件 | 节/键 | 说明 |
| --- | --- | --- |
| config.ini | `[projectPath]` | UFT30/UF20/UF20账户/清算项目环境路径(`uft30` / `uf20` / `ufAcct20` / `UftdbSett`)、转码生成路径;保存时同步维护 `uft30pub = <UFT30路径>\upub\dev_codes` |
| change_code.ini | `[log]` | 日志级别/开关,LogManager 启动时读取 |
### 7.5 转码配置格式契约(cust.json / cust_nonls.json)
> 版本:1.0(2026-09-16)
> 变更发起方:转码工具 UI(Uft30ChangeCode,Qt 客户端)
> 契约接收方:转码引擎(uf2touft3)
> 状态:已评审确认,待引擎侧实现联调
#### 1. 变更背景
全量转码页新增【核心】【是否柜面接口】两列,按需转码页新增【db模式】列(支持 SQL 的 db/mdb 语法转换)。相应地,两份配置文件的 `convert.dir` 节点格式升级,引擎需按本契约完成解析与转码逻辑适配。
#### 2. cust.json(全量转码清单)
##### 2.1 格式变更
```jsonc
// 旧格式(V1):字符串数组
"convert": {
"dir": [
"LS_证券参数_股票名单增加.service_design"
],
"ext": [ ".service_design", ".function_design", ... ]
}
// 新格式(V2):对象数组
"convert": {
"dir": [
{
"name": "LS_债券质押协议回购_初始交易委托.service_design",
"core": "ucbp",
"viaUmgr": true,
"dialect": "db"
}
],
"ext": [ ".service_design", ".function_design", ... ]
}
```
##### 2.2 字段定义
| 字段 | 类型 | 必填 | 语义 | 合法取值 |
|---|---|---|---|---|
| `name` | string | 是 | 功能短名 + 设计器扩展名(后缀由 UI 按清单扩展名拼接,当前固定 `.service_design`) | 非空字符串 |
| `core` | string | 是 | 目标核心 | `"ucbp"` \| `"uses"` \| `"uarg"` \| `"usms"` |
| `viaUmgr` | bool | 是 | 是否柜面接口 | `true` \| `false` |
| `dialect` | string | 是 | SQL 语法方言,由 `core` 派生,UI 不提供独立修改入口 | `"mdb"` \| `"db"` |
**dialect 派生规则(UI 侧保证,引擎可据此校验):**
| core | dialect |
|---|---|
| `ucbp` | `mdb` |
| `uses` | `mdb` |
| `uarg` | `db` |
| `usms` | `db` |
##### 2.3 引擎解析要求
1. **双格式兼容**:`dir` 元素为字符串时按旧格式处理(无 core/viaUmgr/dialect,行为与现有引擎一致);为对象时按新格式处理。两者可混合出现的可能性极低,但解析器不应因此崩溃。
2. **字段兜底建议**:对象缺字段时,建议按 UI 默认值兜底并记录警告日志——`core="ucbp"`、`viaUmgr=false`、`dialect` 按 2.2 派生表由 core 推导。
3. **非法值处理**:`core` 不在枚举内、`dialect` 与派生规则矛盾时,记录警告并按 `dialect` 字段实际值执行(以文件为准,不做强制纠正),或按团队约定明确拒绝该条目——**请引擎侧反馈选定哪种策略**。
4. `viaUmgr=true` 时柜面接口的转码行为差异由引擎侧定义并实现。
#### 3. cust_nonls.json(按需转码清单)
##### 3.1 格式变更
```jsonc
// 旧格式(V1):功能名 → 数值
"convert": {
"dir": {
"*": -1,
"AS_证券日终(用户)_证券信息更新": 0
},
"ext": [ ".service_design", ... ]
}
// 新格式(V2):清单条目为对象;引擎兜底键 "*" 保持数值原样
"convert": {
"dir": {
"AS_证券参数_交易风险提示证券代码批量导入": { "depth": 0, "dialect": "db" },
"*": -1
},
"ext": [ ".service_design", ... ]
}
```
##### 3.2 字段定义
| 字段 | 类型 | 必填 | 语义 | 合法取值 |
|---|---|---|---|---|
| `depth` | int | 是 | 递归深度标志,语义与旧格式数值完全一致:`0`=否(单文件转码)、`-1`=是(递归转码) | `0` \| `-1` |
| `dialect` | string | 是 | SQL 语法方言,对应 UI【db模式】列:选"是"→`db`,选"否"→`mdb` | `"db"` \| `"mdb"` |
##### 3.3 特殊键与解析要求
1. **`"*"` 兜底键**:保持旧格式数值(`0`/`-1`),**不升级为对象、不携带 dialect**。引擎对非清单功能的默认行为不变,UI 侧原样透传、不展示、不修改。
2. **双格式兼容**:`dir` 条目值为数值时按旧格式处理(dialect 默认 `mdb`);为对象时按新格式解析。
3. **字段兜底建议**:对象缺 `depth` 按 `0`,缺 `dialect` 按 `mdb`,均记警告日志。
4. **旧格式数组兼容**:更早期版本 `dir` 为字符串数组(无递归标志),引擎若仍有该格式在途配置,维持现有兼容逻辑即可。
#### 4. UI 侧写入保证(引擎可信赖的前提)
| 保证项 | 说明 |
|---|---|
| 原子写入 | 两份配置均经 `QSaveFile` 原子提交,不会出现写一半的截断文件 |
| JSON 合法性 | cust.json 文本级替换写回前做整体 JSON 解析校验,非法即中止保存 |
| 字段完整性 | V2 对象条目四字段/两字段总是齐全,不输出缺字段条目(兜底逻辑仅为防御外部手改) |
| `ext` 节点 | 不变,引擎按现状使用 |
| 顶层键顺序 | cust.json 其余顶层键与格式不被重排(文本级区间替换) |
#### 5. 版本与联调约定
1. **不兼容变更**:V2 格式对旧引擎不可解析(全量转码清单会为空)。**UI 与引擎必须同版本发布**,禁止单独升级任一侧。
2. **升级方向兼容**:新引擎需兼容读 V1(存量用户配置),新 UI 兼容读 V1/V2。
3. **回退方案**:若需回退到旧 UI,旧 UI 读 V2 配置会得到空清单,需手工将配置改回 V1 或删除配置重新生成。
4. **待引擎侧反馈**:
- 2.3-3 非法值处理策略(告警放行 or 拒绝条目);
- `viaUmgr=true` 柜面接口转码的具体行为定义;
- `dialect` 驱动 SQL 语法转换(mdb/db)的实现范围与差异点清单。
#### 6. 格式速查(供引擎实现对照)
```
cust.json convert.dir : V1 string[] → V2 {name, core, viaUmgr, dialect}[]
core 枚举 : ucbp | uses | uarg | usms
dialect 派生 : ucbp/uses→mdb ; uarg/usms→db
cust_nonls.json convert.dir : V1 {name: 0|-1, "*": -1}
→ V2 {name: {depth: 0|-1, dialect: "db"|"mdb"}, "*": -1}
depth 语义 : 0=单文件转码 ; -1=递归转码(与 V1 数值一致)
"*" 键 : 保持数值,恒不升级为对象
```
---
## 8. 关键设计决策与问题记录
### 8.1 表格刷新不使用 blockSignals
按需转码页 `updateTable()` 全量刷新模型时**有意不用** blockSignals:QTableView 依赖 `rowsRemoved` / `rowsInserted` 信号更新内部布局缓存,屏蔽信号会导致新增行不可见。替代方案:先 `removeRows` 清空再逐行重建;`m_recursionMap` 已预填充,值相等时不写盘,避免刷新回环。
### 8.2 启动时引擎存在性校验
MainWindow 启动即校验 `bin/uf2touft3/uf2touft3.exe` 是否存在:缺失时记录错误日志并界面提示,后续转码请求直接拒绝,避免 QProcess 启动失败的报错含糊难排查。
### 8.3 外部配置修改的监听与防抖
两套机制并行,按页面职责选择:
| 页面 | 监听对象 | 机制 |
| --- | --- | --- |
| 全量转码 / 按需转码 | cust.json / cust_nonls.json | 同时监听**文件与所在目录**(Windows 编辑器原子替换"写临时文件→重命名"常使 fileChanged 失效,directoryChanged 更可靠);变更后经 300ms 防抖定时器,比对 mtime 有变化才重载 |
| 数据映射(表/函数替换) | cust.json | `fileChanged` 后立即重载;程序自身保存后 2 秒内(`m_isSaving` + `QTimer::singleShot(2000)`)忽略变更,防止保存回环 |
### 8.4 落库与清缓存的并发保护(写闸门)
`saveDataset()`(后台线程落库)与 `clearCache()`(重置缓存连接)并发时,直接关闭连接会中断在途写入。设计:`saveDataset` 取得数据库连接后全程持有 `m_writeGate` 写闸门;`clearCache` 等待预热任务结束后以 `tryLock` 探测闸门——加锁失败说明落库在途,仅重置内存状态并记录警告、跳过连接重置;加锁成功则持闸门完成连接清理。锁序安全:`saveDataset` 在取闸门前已完成 `database()` 调用(`m_connMutex` 短暂持锁即释放),与 `clearCache` 的"闸门 → 连接锁"顺序不构成死锁环。
### 8.5 转码引擎异步启动与幂等收尾
`PythonRunner::start()` 原在主线程 `waitForStarted(3000)` 等待启动,杀毒软件扫描引擎 exe 时界面可卡顿达 3 秒。现改为异步启动:`start()` 触发 `QProcess::start` 后立即返回,启动结果经 `started` / `errorOccurred` 信号异步上报。MainWindow 连接 `errorOccurred`,对 `FailedToStart` 立即执行失败收尾(恢复两 Tab 界面并提示);配合 `m_taskActive` 在途守卫,正常结束(`finished`)与启动失败(Qt 对 FailedToStart 亦会补发 `finished(-1, CrashExit)`)两条路径的收尾**幂等**——先到者执行、后到者跳过,避免重复弹窗。`runSync()` 同步语义不受影响(启动失败视为"已结束",`waitForFinished` 立即返回)。
### 8.6 功能缓存启动预热
功能查询内存缓存(2 万~5 万条)原先在**首次进入页面时于主线程同步重建**(uf20 约 4.3s、uft3 约 2.7s),且 uft3 无触发点、每次进页都重建。现改为**启动即预热**:`MainWindow` 构造尾部经 `QTimer::singleShot(0, ...)` 在首帧渲染完成后调用 `DataCache::warmupFunctionCacheAsync("uf20"/"uft3")`,向 QtConcurrent 线程池提交后台重建;接口**幂等**(缓存已就绪或预热在途时直接返回),进页面时的同步路径仅在预热未完成且等待超限(≤5s)时才兜底重建,正常情况下进页面"接管"预热结果即可,无感知加载。元数据更新落库后 `refreshFunctionCache` 触发重建,预热不干扰该链路。
### 8.7 转码失败的准确判定(FAIL 行检测)
引擎在**个别文件转码失败时退出码仍为 0**(实测:`[N/M] FAIL 函数名` 行后流水线正常收尾),仅凭退出码会把失败误报为"转换完成"。设计:`MainWindow::onPythonRunnerOutput` 在解析链**最前端**做失败检测——正则累计 `FAIL` 行数(含 `[n/m]` 计数格式),并匹配汇总行 `批量转码完成: X/Y 通过, Z 失败` 取最大值,累计至 `m_failCount`;`[n/m]` 翻译进度解析仅在无 FAIL 行时执行(FAIL 行含 `[n/m]` 会被误判为进度百分比)。收尾(`onPythonRunnerFinished`)按 `正常退出 && 退出码==0 && m_failCount==0` 路由成败:失败时调用两页 `onConvertFailed`——进度条**保持当前进度**(不置 100%)、状态显示"转换失败(N 个文件转换失败)"、日志记录 ERROR 并弹窗提示查看 `logs\` 日志。
---
## 9. 构建与部署
### 9.1 构建
- Qt 6.6.2(MinGW 64-bit)+ ElaWidgetTools + CMake;
- 第三方库经 `3rdparty/` 引入,构建产出 `bin/Uft30ChangeCode.exe`。
### 9.2 部署清单(bin/)
| 路径 | 说明 |
| --- | --- |
| `Uft30ChangeCode.exe` | 主程序 |
| `uf2touft3/uf2touft3.exe` | 转码引擎(外部进程) |
| `uf2touft3/config/cust.json` | 表/函数映射与不转码功能函数清单配置(数据映射页读写) |
| `uf2touft3/config/cust_nonls.json` | 非转码业务配置(业务转码页读写) |
| `uf2touft3/cache/uf20_cache.db` / `uft3_cache.db` | 元数据缓存库(首次元数据更新生成) |
| `config.ini` / `change_code.ini` | 程序路径配置 / 日志配置 |
| `logs/` | 日志输出目录(运行生成) |
| `change_code/`、`uf2touft3/function_output/` | 转码输出 / 调用链导出目录(运行生成) |
---
## 10. 附录:页面与源码对照
| 页面/功能 | 源码位置 |
| --- | --- |
| 主窗口与导航、引擎调度 | `src/mainwindow/` |
| 业务转码 | `src/pages/businessconvert/`(容器)+ `src/pages/batchconvert/`(全量)+ `src/pages/nonlsconvert/`(按需) |
| 功能查询 | `src/pages/functionsearch/`(UF20 / UFT3 双模式 + UF20 & 内存表映射查询) |
| 代码检索 | `src/pages/codesearch/`(含 CallChainAnalyzer) |
| 数据映射 | `src/pages/datamapping/`(TableMappingPage / FunctionMappingPage / NoChangeFullNamePage) |
| 元数据更新 | `src/pages/metadataupdate/`(页面)+ `src/metadataupdate/`(解析器组) |
| 设置 / 关于 | `src/pages/settings/` / `src/pages/about/` |
| 公共支撑 | `src/utils/`(DataCache、LogManager、ConfigManager、CustJsonIo、VersionChecker、Constants 等)、`src/pythonrunner/`、`src/components/` |