feat: add BOS single-field migration
This commit is contained in:
@@ -40,8 +40,8 @@
|
|||||||
### 启动与界面
|
### 启动与界面
|
||||||
|
|
||||||
- `App.xaml`:应用资源、HandyControl 主题和 `MainViewModel` 资源注册;启动页是 `Views/HomeWindow.xaml`。
|
- `App.xaml`:应用资源、HandyControl 主题和 `MainViewModel` 资源注册;启动页是 `Views/HomeWindow.xaml`。
|
||||||
- `App.xaml.cs`:Velopack 初始化、后台/手动更新、更新前后 `data/` 与 `pre/` 备份恢复、全局异常日志。
|
- `App.xaml.cs`:Velopack 初始化、后台/手动更新、更新前后 `config.ini`、`data/` 与 `pre/` 备份恢复、全局异常日志。
|
||||||
- `Views/HomeWindow.xaml`:单窗口主界面,包含四个迁移 Tab、连接状态、SQL 编辑区和批量操作。
|
- `Views/HomeWindow.xaml`:单窗口主界面,包含五个迁移 Tab、连接状态、SQL 编辑区和批量操作。
|
||||||
- `Views/HomeWindow.xaml.cs`:只处理窗口关闭和手动检查更新等窗口事件。
|
- `Views/HomeWindow.xaml.cs`:只处理窗口关闭和手动检查更新等窗口事件。
|
||||||
- `Views/ConnectionDialog.*`:源库/目标库连接配置与测试。
|
- `Views/ConnectionDialog.*`:源库/目标库连接配置与测试。
|
||||||
- `Views/ErrorDialog.*`:错误详情展示和复制。
|
- `Views/ErrorDialog.*`:错误详情展示和复制。
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
### 状态与编排
|
### 状态与编排
|
||||||
|
|
||||||
- `ViewModels/MainViewModel.cs`:主状态容器和命令编排;初始化四个 `TabServices`,管理数据库连接、单行/批量读写、SQL 编辑与断点续写。
|
- `ViewModels/MainViewModel.cs`:主状态容器和命令编排;初始化五个 `TabServices`,管理数据库连接、单行/批量读写、SQL 编辑与断点续写。
|
||||||
- `Models/TableRowModel.cs`:每个迁移对象的名称、所属 Tab、读写状态。
|
- `Models/TableRowModel.cs`:每个迁移对象的名称、所属 Tab、读写状态。
|
||||||
- `Enums/`:Tab、状态、工作类型和输出扩展名枚举。
|
- `Enums/`:Tab、状态、工作类型和输出扩展名枚举。
|
||||||
|
|
||||||
@@ -60,34 +60,41 @@
|
|||||||
- `Services/DbServices.cs`:Oracle SQL 的提取与执行核心:
|
- `Services/DbServices.cs`:Oracle SQL 的提取与执行核心:
|
||||||
- `GetTableSql`:读取表 DDL、注释、主外键、唯一键和索引;
|
- `GetTableSql`:读取表 DDL、注释、主外键、唯一键和索引;
|
||||||
- `GetTableBos`:读取 BOS 配置;
|
- `GetTableBos`:读取 BOS 配置;
|
||||||
|
- `GetTableBosField`:按 `TABLE.COLUMN` 读取单个 BOS 字段配置,生成目标物理字段补充 SQL,并包含关联限定值/引用元数据;
|
||||||
- `GetPrcSql` / `GetAllPrc`:读取存储过程或函数源码;
|
- `GetPrcSql` / `GetAllPrc`:读取存储过程或函数源码;
|
||||||
- `GetTableData`:生成表数据 INSERT SQL;
|
- `GetTableData`:生成表数据 INSERT SQL;
|
||||||
- `ExecuteCreate`:按 `\n/` 分隔 SQL,在目标库逐条执行并检查 `USER_ERRORS`;
|
- `ExecuteCreate`:按 `\n/` 分隔 SQL,在目标库逐条执行并检查 `USER_ERRORS`。
|
||||||
- `PreProcess`:在源库执行 `pre/` 下全部脚本。
|
|
||||||
- `Common/SqlSugarHelper.cs`:维护 `orig`(源库)和 `dest`(目标库)两个 SqlSugar 连接。
|
- `Common/SqlSugarHelper.cs`:维护 `orig`(源库)和 `dest`(目标库)两个 SqlSugar 连接。
|
||||||
- `Common/SysConfig.cs`:读取和写入运行目录下的 `config.ini`。
|
- `Common/SysConfig.cs`:读取和写入运行目录下的 `config.ini`。
|
||||||
- `Common/CommonHelper.cs`:运行目录、`data/`、`data/from/`、`pre/` 路径及文本读写。
|
- `Common/CommonHelper.cs`:运行目录、`data/`、`data/from/`、`pre/` 路径及文本读写。
|
||||||
|
|
||||||
## 真实数据流
|
## 真实数据流
|
||||||
|
|
||||||
四个 Tab 的映射由 `MainViewModel` 构造函数定义:
|
五个 Tab 的映射由 `MainViewModel` 构造函数定义:
|
||||||
|
|
||||||
| Tab | 索引文件 | 内容文件 | 源库读取方法 |
|
| Tab | 索引文件 | 内容文件 | 源库读取方法 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 表配置 | `data/from/table.txt` | `data/{对象名}_creat.txt` | `DbServices.GetTableSql` |
|
| 表配置 | `data/from/table.txt` | `data/{对象名}_creat.txt` | `DbServices.GetTableSql` |
|
||||||
| BOS 配置 | `data/from/bos.txt` | `data/{对象名}_bos.txt` | `DbServices.GetTableBos` |
|
| BOS 配置 | `data/from/bos.txt` | `data/{对象名}_bos.txt` | `DbServices.GetTableBos` |
|
||||||
|
| BOS 字段配置 | `data/from/bos_field.txt` | `data/{表名}.{字段名}_bos_field.txt` | `DbServices.GetTableBosField` |
|
||||||
| 存储过程 | `data/from/prc.txt` | `data/{对象名}_prc.txt` | `DbServices.GetPrcSql` |
|
| 存储过程 | `data/from/prc.txt` | `data/{对象名}_prc.txt` | `DbServices.GetPrcSql` |
|
||||||
| 表数据 | `data/from/data.txt` | `data/{对象名}_data.txt` | `DbServices.GetTableData` |
|
| 表数据 | `data/from/data.txt` | `data/{对象名}_data.txt` | `DbServices.GetTableData` |
|
||||||
|
|
||||||
读取流程:索引项 -> 调用源库读取委托 -> 生成 SQL 文本 -> 保存到 `data/` -> 更新读取状态并显示内容。
|
读取流程:索引项 -> 调用源库读取委托 -> 生成 SQL 文本 -> 保存到 `data/` -> 更新读取状态并显示内容。
|
||||||
|
|
||||||
|
读取 BOS 配置前会检查源库当前用户下的辅助过程。整表读取使用 `HMIG_GET_TABLE_BOS_31`,单字段读取使用 `HMIG_GET_TABLE_BOS_FIELD_31`;若过程不存在,程序从运行目录的同名 `pre/*.prc` 脚本安装,检查 `USER_ERRORS` 和对象状态后再读取。单字段列表项必须采用 `TABLE.COLUMN` 格式。界面不提供手动“预处理”入口。
|
||||||
|
|
||||||
|
单字段 SQL 写入目标库时,限定值组按名称、限定值按组和值判断,只补充缺失项;引用元数据按引用表名和字段 `DBNAME` 映射,目标引用字段不存在或不唯一时直接报错,不自动创建。
|
||||||
|
|
||||||
|
单字段读取还会查询源库 `USER_TABLES`、`USER_TAB_COLUMNS`。源对象是物理表且源字段存在时,输出文件首先检查目标库物理表和字段:目标表不存在时报错,目标字段不存在时按源字段的数据类型、长度、精度和字符语义执行 `ALTER TABLE ... ADD`。源对象是视图或 BOS 计算字段时不生成物理字段 DDL。
|
||||||
|
|
||||||
写入流程:索引项 -> 读取对应 `data/*_{后缀}.txt` -> `DbServices.ExecuteCreate` -> 在目标库执行 -> 更新写入状态。批量写入遇到第一条失败即停止,并在内存中记录断点;断点不会跨进程持久化。
|
写入流程:索引项 -> 读取对应 `data/*_{后缀}.txt` -> `DbServices.ExecuteCreate` -> 在目标库执行 -> 更新写入状态。批量写入遇到第一条失败即停止,并在内存中记录断点;断点不会跨进程持久化。
|
||||||
|
|
||||||
删除列表项会同时删除对应内容文件。调整顺序会立即覆盖索引文件。新增对象名会被转为小写,数据库读取时再按需要转大写。
|
删除列表项会同时删除对应内容文件。调整顺序会立即覆盖索引文件。新增对象名会被转为小写,数据库读取时再按需要转大写。
|
||||||
|
|
||||||
## 配置和运行时文件
|
## 配置和运行时文件
|
||||||
|
|
||||||
- `config.ini` 保存源库和目标库的 IP/服务名、用户名、密码。当前实现是明文读写,不要把注释或 UI 文案误当成加密能力。
|
- `config.ini` 保存源库和目标库的 IP/服务名、用户名、密码。当前实现是明文读写,首次运行缺失时创建空配置;发布包不得携带本机配置。
|
||||||
- 不要在日志、回答、补丁或测试输出中展示真实连接串和密码。
|
- 不要在日志、回答、补丁或测试输出中展示真实连接串和密码。
|
||||||
- 修改或发布时不要用样例配置覆盖用户现有 `config.ini`。
|
- 修改或发布时不要用样例配置覆盖用户现有 `config.ini`。
|
||||||
- `data/` 和 `pre/` 是用户可编辑、更新时需要保留的运行数据;不要无故清空、批量改名或删除。
|
- `data/` 和 `pre/` 是用户可编辑、更新时需要保留的运行数据;不要无故清空、批量改名或删除。
|
||||||
@@ -113,11 +120,11 @@
|
|||||||
源库和目标库不是等价环境:
|
源库和目标库不是等价环境:
|
||||||
|
|
||||||
- `Get*` 方法通常查询源库;`ExecuteCreate` 明确写目标库。
|
- `Get*` 方法通常查询源库;`ExecuteCreate` 明确写目标库。
|
||||||
- `PreProcess` 会在源库执行 `pre/` 下的 SQL/PLSQL,虽然名为预处理,但不是只读操作。
|
- 首次读取整表或单字段 BOS 配置时可能在源库自动创建对应的 `HMIG_GET_TABLE_BOS_31` 或 `HMIG_GET_TABLE_BOS_FIELD_31`;这不是只读操作,源库账号需要相应的 `CREATE PROCEDURE` 权限。
|
||||||
- 连接按钮会把界面中的连接信息写回 `config.ini`,并重建同时包含源库和目标库的 SqlSugarScope。
|
- 连接按钮会把界面中的连接信息写回 `config.ini`,并重建同时包含源库和目标库的 SqlSugarScope。
|
||||||
- 界面的“断开”当前只更新连接状态,并未释放或关闭底层连接对象。
|
- 界面的“断开”当前只更新连接状态,并未释放或关闭底层连接对象。
|
||||||
|
|
||||||
除非用户明确要求并确认数据库、对象和影响范围,否则不要连接真实数据库、执行预处理、执行写入、清理数据或测试破坏性 SQL。需要验证数据库写入逻辑时优先使用代码审查、可控测试库或事务化的最小复现。真实目标库写入前必须提醒备份并确认权限。
|
除非用户明确要求并确认数据库、对象和影响范围,否则不要连接真实数据库、触发 BOS 辅助过程安装、执行目标库写入、清理数据或测试破坏性 SQL。需要验证数据库写入逻辑时优先使用代码审查、可控测试库或事务化的最小复现。真实目标库写入前必须提醒备份并确认权限。
|
||||||
|
|
||||||
## 构建与验证
|
## 构建与验证
|
||||||
|
|
||||||
@@ -138,7 +145,7 @@ dotnet build .\henlo_migration_wpf10\henlo_migration_wpf10.csproj -c Debug --no-
|
|||||||
|
|
||||||
1. 所有 C#/XAML 修改至少执行一次主项目构建。
|
1. 所有 C#/XAML 修改至少执行一次主项目构建。
|
||||||
2. 仅文档修改可不构建,但要重新读取文件检查 Markdown、路径和命令。
|
2. 仅文档修改可不构建,但要重新读取文件检查 Markdown、路径和命令。
|
||||||
3. XAML/绑定修改除构建外,应在 Windows 上启动应用,检查四个 Tab、连接弹窗、按钮可用性和错误弹窗;不要因此连接生产库。
|
3. XAML/绑定修改除构建外,应在 Windows 上启动应用,检查五个 Tab、连接弹窗、按钮可用性和错误弹窗;不要因此连接生产库。
|
||||||
4. SQL 生成/拆分修改需用不含真实数据的代表性 SQL 验证表 DDL、注释、约束、PL/SQL 和 INSERT。
|
4. SQL 生成/拆分修改需用不含真实数据的代表性 SQL 验证表 DDL、注释、约束、PL/SQL 和 INSERT。
|
||||||
5. 更新逻辑修改需分别考虑普通 `dotnet run`(非 Velopack 安装)和 Velopack 安装态。
|
5. 更新逻辑修改需分别考虑普通 `dotnet run`(非 Velopack 安装)和 Velopack 安装态。
|
||||||
|
|
||||||
@@ -154,12 +161,11 @@ dotnet run --project .\henlo_migration_wpf10\henlo_migration_wpf10.csproj
|
|||||||
|
|
||||||
## 版本与发布
|
## 版本与发布
|
||||||
|
|
||||||
- 版本号只修改 `.csproj` 中的 `<Version>`,必须单调递增;显示版本来自程序集元数据。
|
- 仓库根目录的 `RELEASE.md` 是当前发版流程、产物、上传和自动更新风险的权威说明。凡任务涉及版本号、Release 构建、Velopack、打包、便携包、自动更新、服务器部署或上传,必须先完整读取 `RELEASE.md`,再执行或给出结论。
|
||||||
- 正式发布入口是 `henlo_migration_wpf10/pack.bat`。脚本会清理 `publish/`、执行 `dotnet publish -c Release --self-contained false -r win-x64`,再调用 `vpk pack --noInst` 输出到 `releases/`。
|
- 正式版本的用户可见变更记录维护在仓库根目录 `CHANGELOG.md`,发版时必须新增对应版本条目。
|
||||||
- `pack.bat` 依赖本机 `dotnet`、`vpk` 和 Windows 批处理环境;不要把“构建通过”等同于“打包通过”。
|
- 版本号只修改 `.csproj` 中的 `<Version>`,必须单调递增;正式发布入口是 `henlo_migration_wpf10/pack.bat`。
|
||||||
- 发布是显式任务。未经要求不要运行打包、删除发布目录、修改版本号或上传更新服务器。
|
- 发布是显式任务。未经要求不要运行打包、删除发布目录、修改版本号或上传更新服务器;构建通过不等于打包或自动更新验证通过。
|
||||||
- 打包后核对便携 ZIP、full/delta nupkg、`RELEASES`/JSON 元数据以及实际版本号。上传服务器属于外部写操作,必须另行获得明确授权。
|
- 若代码、脚本和 `RELEASE.md` 不一致,以实际代码和 `pack.bat` 为准,并在同一任务中同步修正文档。
|
||||||
- 若修改运行时必需文件,检查 `.csproj` 的 `CopyToOutputDirectory` 规则和 `App.xaml.cs` 的更新备份/恢复范围。
|
|
||||||
|
|
||||||
## 完成任务时的交付说明
|
## 完成任务时的交付说明
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 更新记录
|
||||||
|
|
||||||
|
## 1.0.17 - 2026-08-26
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 新增“BOS字段配置”页签,使用 `TABLE.COLUMN` 单独迁移一个 BOS 字段配置。
|
||||||
|
- 单字段迁移包含限定值组及限定值:目标库不存在时自动补充,已有数据不覆盖。
|
||||||
|
- 单字段迁移支持引用元数据重新映射;目标引用字段不存在或不唯一时明确报错。
|
||||||
|
- 单字段写入前检查目标物理表字段;目标字段缺失时按源库字段类型、长度、精度和字符语义自动新增。
|
||||||
|
- 新增 `HMIG_GET_TABLE_BOS_FIELD_31` 辅助过程,并使用 Oracle CLOB 返回完整迁移 SQL。
|
||||||
|
|
||||||
|
### 调整
|
||||||
|
|
||||||
|
- 取消界面“预处理”按钮。读取整表或单字段 BOS 配置时,缺少对应辅助过程才从 `pre/` 自动安装并检查编译状态。
|
||||||
|
- 整理项目、构建和发版约定,新增仓库级 `AGENTS.md` 与 `RELEASE.md`。
|
||||||
|
- 发布包不再携带本机 `config.ini` 和 `data/from/*.txt`;首次运行创建空配置和运行目录,后续更新备份并恢复用户的 `config.ini`、`data/` 和 `pre/`。
|
||||||
|
|
||||||
|
### 兼容性
|
||||||
|
|
||||||
|
- 保留原有整表 BOS 配置、表结构、存储过程/函数和表数据迁移流程。
|
||||||
|
- 仍为 Windows x64、.NET 10 framework-dependent 便携发布,不包含安装器。
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
# 恒诺迁移工具构建与发版说明
|
||||||
|
|
||||||
|
本文是 `henlo_migration_wpf10/` 的构建、Velopack 打包、更新服务器部署和客户端自动更新说明。涉及版本、发布或更新的任务必须先完整读取本文;若本文与实际代码冲突,以 `.csproj`、`pack.bat` 和 `App.xaml.cs` 为准,并同步更新本文。
|
||||||
|
|
||||||
|
## 权限边界
|
||||||
|
|
||||||
|
- 普通 Debug/Release 构建可以用于本地验证。
|
||||||
|
- 修改版本号、运行 `pack.bat`、删除或覆盖发布产物、上传更新服务器都属于显式发布动作,必须有用户当前任务的明确授权。
|
||||||
|
- 打包脚本不会上传任何文件;服务器部署是独立的外部写操作,需要另行授权。
|
||||||
|
- 不要把“项目构建成功”描述成“发版成功”或“自动更新已验证”。
|
||||||
|
|
||||||
|
## 版本来源
|
||||||
|
|
||||||
|
- 唯一版本来源是 `henlo_migration_wpf10/henlo_migration_wpf10.csproj` 的 `<Version>`。
|
||||||
|
- 正式发版前必须把版本号单调递增,不要重新打包已存在的版本号。
|
||||||
|
- 程序显示版本来自程序集元数据,不在脚本或文档中另设版本常量。
|
||||||
|
|
||||||
|
## 日常构建
|
||||||
|
|
||||||
|
在仓库根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet restore .\henlo_migration_wpf10\henlo_migration_wpf10.csproj
|
||||||
|
dotnet build .\henlo_migration_wpf10\henlo_migration_wpf10.csproj -c Debug
|
||||||
|
```
|
||||||
|
|
||||||
|
依赖已经恢复且不希望访问包源时:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet build .\henlo_migration_wpf10\henlo_migration_wpf10.csproj -c Debug --no-restore
|
||||||
|
```
|
||||||
|
|
||||||
|
Debug 产物位于 `henlo_migration_wpf10/bin/Debug/net10.0-windows/`,只用于开发验证,不是 Velopack 发布包。项目现有 `CS8632` 和 `NU1701` 警告不等于构建错误,但不得新增编译错误。
|
||||||
|
|
||||||
|
## 正式打包流程
|
||||||
|
|
||||||
|
唯一正式入口:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd .\henlo_migration_wpf10
|
||||||
|
.\pack.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
`pack.bat` 当前执行顺序:
|
||||||
|
|
||||||
|
1. 从 `.csproj` 读取 `<Version>`。
|
||||||
|
2. 删除并重建 `publish/`。
|
||||||
|
3. 执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet publish henlo_migration_wpf10.csproj -c Release -o publish --self-contained false -r win-x64
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 删除 `publish/config.ini`,确保本机数据库配置不会进入产物。
|
||||||
|
5. 确保 `publish/data/`、`publish/data/from/`、`publish/pre/` 存在。
|
||||||
|
6. 执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
vpk pack -u henlo_migration -v <Version> -p publish -e "恒诺迁移工具.exe" -o releases --noInst
|
||||||
|
```
|
||||||
|
|
||||||
|
7. 保留 `releases/` 中的历史包,供 Velopack 生成差量包;脚本只清理 `publish/`,不会清理 `releases/`。
|
||||||
|
|
||||||
|
打包依赖 Windows、对应 .NET SDK、可调用的 `dotnet` 和 `vpk`。不要使用 `_build*`、`_make_pack.bat`、`_write_pack.ps1` 等历史辅助文件作为正式入口。
|
||||||
|
|
||||||
|
## 当前发布形态
|
||||||
|
|
||||||
|
- 目标平台:Windows x64。
|
||||||
|
- 发布方式:framework-dependent,`--self-contained false`;目标机器需要匹配的 .NET 10 Desktop Runtime。
|
||||||
|
- Velopack 参数包含 `--noInst`,因此不生成 Setup 安装器,只生成便携包和更新源文件。
|
||||||
|
- `Properties/PublishProfiles/FolderProfile.pubxml` 虽配置 `PublishSingleFile=true`,但 `pack.bat` 没有使用该 Profile;正式产物不是单文件,会包含 EXE、DLL 和 runtimeconfig/deps 文件。
|
||||||
|
|
||||||
|
## 运行时文件进入发布包的规则
|
||||||
|
|
||||||
|
- `.csproj` 只复制 `pre/*.txt|*.fnc|*.prc` 等程序辅助脚本,不复制本机 `config.ini` 或 `data/from/*.txt`;`pack.bat` 还会在打包前再次删除 `publish/config.ini`,并创建空的 `data/from/` 目录。
|
||||||
|
- `pre/hmig_get_table_bos_31.prc`、`pre/hmig_get_table_bos_field_31.prc` 等运行时辅助过程必须出现在发布输出中。
|
||||||
|
- 新安装的索引列表为空;程序添加第一条对应配置时会在 `data/from/` 创建 `table.txt`、`bos.txt`、`bos_field.txt`、`prc.txt` 或 `data.txt`。
|
||||||
|
- 新安装首次运行时会创建值为空的 `config.ini`,由用户在界面配置;不得把真实或样例账号密码放入发布产物。
|
||||||
|
|
||||||
|
## Velopack 产物
|
||||||
|
|
||||||
|
正常情况下 `releases/` 至少包含:
|
||||||
|
|
||||||
|
```text
|
||||||
|
henlo_migration-win-Portable.zip
|
||||||
|
henlo_migration-<Version>-full.nupkg
|
||||||
|
henlo_migration-<Version>-delta.nupkg
|
||||||
|
RELEASES
|
||||||
|
releases.win.json
|
||||||
|
assets.win.json
|
||||||
|
```
|
||||||
|
|
||||||
|
便携 ZIP 内应包含 `.portable`、根启动器、`Update.exe`、`current/` 应用文件以及需要的 `data/`、`pre/` 内容。差量包是否生成取决于 `releases/` 中是否有可用的历史版本。
|
||||||
|
|
||||||
|
## 更新服务器部署
|
||||||
|
|
||||||
|
客户端更新地址硬编码在 `App.xaml.cs`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/
|
||||||
|
```
|
||||||
|
|
||||||
|
当前没有自动上传脚本。获得上传授权后,应部署并核对:
|
||||||
|
|
||||||
|
- 新版本 full nupkg;
|
||||||
|
- 新版本 delta nupkg(若生成);
|
||||||
|
- `RELEASES`;
|
||||||
|
- `releases.win.json`;
|
||||||
|
- `assets.win.json`;
|
||||||
|
- 需要提供便携下载时再上传 `henlo_migration-win-Portable.zip`。
|
||||||
|
|
||||||
|
上传后至少验证服务器能直接访问元数据文件和其中引用的包,文件大小及版本号与本地产物一致。不要只上传 nupkg 而遗漏更新后的元数据。
|
||||||
|
|
||||||
|
## 客户端自动更新流程
|
||||||
|
|
||||||
|
`App.xaml.cs` 当前行为:
|
||||||
|
|
||||||
|
1. UI 启动前调用 `VelopackApp.Build().Run()`。
|
||||||
|
2. 从系统临时目录恢复上次更新前备份的 `config.ini`、`data/`、`pre/`。
|
||||||
|
3. 创建指向更新服务器的 `UpdateManager`;非 Velopack 环境创建失败时禁用自动更新。
|
||||||
|
4. 后台静默检查并下载更新;手动“检查更新”使用相同更新源。
|
||||||
|
5. 用户同意立即重启时,先备份 `config.ini`、`data/`、`pre/`,再调用 `ApplyUpdatesAndRestart()`。
|
||||||
|
6. 新版本首次启动时恢复备份并删除临时备份目录。
|
||||||
|
|
||||||
|
当前限制和未验证项:
|
||||||
|
|
||||||
|
- 更新备份范围包含 `config.ini`、`data/` 和 `pre/`;恢复失败时会记录到 `error.log`,发版验收仍需验证用户配置未丢失。
|
||||||
|
- 用户下载完成后选择“不立即重启”时,代码没有明确实现下次启动自动应用已下载更新的流程。
|
||||||
|
- 直接运行普通 `publish/` 中的 EXE 时通常不属于 Velopack 安装态,程序会禁用更新或把检查异常写入 `error.log`。
|
||||||
|
- `--noInst` 生成的是便携模式;便携包的检查、下载、应用和重启更新链路尚未完成实际端到端回归,不能声称自动更新可靠可用。
|
||||||
|
|
||||||
|
## 发版验收清单
|
||||||
|
|
||||||
|
1. 确认工作区改动和准备发布的提交范围,避免把无关或敏感文件带入。
|
||||||
|
2. 递增并核对 `.csproj` 版本号。
|
||||||
|
3. 先执行主项目构建,确保 `0` 个错误。
|
||||||
|
4. 获得明确授权后运行 `pack.bat`,确认 publish 和 vpk 两阶段都成功。
|
||||||
|
5. 检查便携 ZIP、full/delta 包、`RELEASES` 和两个 JSON 的版本、大小及时间。
|
||||||
|
6. 检查发布包中的 `pre/` 和必要运行文件,并确认产物中不存在 `config.ini` 及开发机 `data/from/*.txt`。
|
||||||
|
7. 在未安装态测试普通 EXE/便携包启动;若任务涉及自动更新,再在对应 Velopack 运行形态下单独验证更新。
|
||||||
|
8. 只有获得上传授权后才能部署服务器;上传后验证 URL 和包完整性。
|
||||||
|
9. 交付说明必须区分:构建验证、打包验证、便携运行验证、自动更新验证和服务器部署验证。
|
||||||
@@ -164,8 +164,9 @@ namespace henlo_migration
|
|||||||
var rootDir = AppDomain.CurrentDomain.BaseDirectory;
|
var rootDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
var dataDir = Path.Combine(rootDir, "data");
|
var dataDir = Path.Combine(rootDir, "data");
|
||||||
var preDir = Path.Combine(rootDir, "pre");
|
var preDir = Path.Combine(rootDir, "pre");
|
||||||
|
var configPath = Path.Combine(rootDir, "config.ini");
|
||||||
|
|
||||||
if (!Directory.Exists(dataDir) && !Directory.Exists(preDir)) return;
|
if (!Directory.Exists(dataDir) && !Directory.Exists(preDir) && !File.Exists(configPath)) return;
|
||||||
|
|
||||||
if (Directory.Exists(BackupDir))
|
if (Directory.Exists(BackupDir))
|
||||||
Directory.Delete(BackupDir, true);
|
Directory.Delete(BackupDir, true);
|
||||||
@@ -176,6 +177,9 @@ namespace henlo_migration
|
|||||||
|
|
||||||
if (Directory.Exists(preDir))
|
if (Directory.Exists(preDir))
|
||||||
CopyDirectory(preDir, Path.Combine(BackupDir, "pre"));
|
CopyDirectory(preDir, Path.Combine(BackupDir, "pre"));
|
||||||
|
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
File.Copy(configPath, Path.Combine(BackupDir, "config.ini"), true);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -200,6 +204,10 @@ namespace henlo_migration
|
|||||||
if (Directory.Exists(backupPre))
|
if (Directory.Exists(backupPre))
|
||||||
CopyDirectory(backupPre, Path.Combine(rootDir, "pre"));
|
CopyDirectory(backupPre, Path.Combine(rootDir, "pre"));
|
||||||
|
|
||||||
|
var backupConfig = Path.Combine(BackupDir, "config.ini");
|
||||||
|
if (File.Exists(backupConfig))
|
||||||
|
File.Copy(backupConfig, Path.Combine(rootDir, "config.ini"), true);
|
||||||
|
|
||||||
Directory.Delete(BackupDir, true);
|
Directory.Delete(BackupDir, true);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -227,4 +235,4 @@ namespace henlo_migration
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ namespace henlo_migration.Common
|
|||||||
if (!System.IO.File.Exists(ConfigPath))
|
if (!System.IO.File.Exists(ConfigPath))
|
||||||
{
|
{
|
||||||
System.IO.File.WriteAllText(ConfigPath, @"[oracle]
|
System.IO.File.WriteAllText(ConfigPath, @"[oracle]
|
||||||
orig_ip=127.0.0.1
|
orig_ip=
|
||||||
orig_exam=orcl
|
orig_exam=
|
||||||
orig_user=system
|
orig_user=
|
||||||
orig_password=system
|
orig_password=
|
||||||
desc_ip=127.0.0.1
|
desc_ip=
|
||||||
desc_exam=orcl
|
desc_exam=
|
||||||
desc_user=system
|
desc_user=
|
||||||
desc_password=system
|
desc_password=
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
_init = parser.ReadFile(ConfigPath);
|
_init = parser.ReadFile(ConfigPath);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ namespace henlo_migration.Enums
|
|||||||
public enum CodeExt
|
public enum CodeExt
|
||||||
{
|
{
|
||||||
bos,
|
bos,
|
||||||
|
bos_field,
|
||||||
creat,
|
creat,
|
||||||
data,
|
data,
|
||||||
prc
|
prc
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ namespace henlo_migration.Enums
|
|||||||
{
|
{
|
||||||
表 = 0,
|
表 = 0,
|
||||||
BOS表配置 = 1,
|
BOS表配置 = 1,
|
||||||
存储过程 = 2,
|
BOS字段配置 = 2,
|
||||||
表数据 = 3
|
存储过程 = 3,
|
||||||
|
表数据 = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
| **表结构迁移** | 读取源库表结构(DDL)、表注释、字段注释、主键/外键/索引,写入目标库 |
|
| **表结构迁移** | 读取源库表结构(DDL)、表注释、字段注释、主键/外键/索引,写入目标库 |
|
||||||
| **存储过程迁移** | 读取/写入存储过程和函数源码 |
|
| **存储过程迁移** | 读取/写入存储过程和函数源码 |
|
||||||
| **BOS 配置迁移** | 读取 BOS 表配置写入目标库 |
|
| **BOS 配置迁移** | 读取 BOS 表配置写入目标库 |
|
||||||
|
| **BOS 字段配置迁移** | 按 `TABLE.COLUMN` 补充目标物理字段并迁移字段配置、限定值和引用元数据 |
|
||||||
| **表数据迁移** | 支持表数据的导出/导入 |
|
| **表数据迁移** | 支持表数据的导出/导入 |
|
||||||
| **自动更新** | 支持通过私有服务器 Velopack 自动检查更新 |
|
| **自动更新** | 支持通过私有服务器 Velopack 自动检查更新 |
|
||||||
| **断点续写** | 批量写入失败后,下次从失败行继续,无需从头开始 |
|
| **断点续写** | 批量写入失败后,下次从失败行继续,无需从头开始 |
|
||||||
@@ -57,7 +58,7 @@ henlo_migration_wpf10/
|
|||||||
│ ├── HomeWindow.xaml # 主窗口
|
│ ├── HomeWindow.xaml # 主窗口
|
||||||
│ └── ConnectionDialog.xaml # 连接配置
|
│ └── ConnectionDialog.xaml # 连接配置
|
||||||
├── data/from/ # 索引文件(table.txt 等)
|
├── data/from/ # 索引文件(table.txt 等)
|
||||||
├── pre/ # 预处理脚本
|
├── pre/ # 运行时辅助过程/函数脚本(BOS 读取缺失时自动安装)
|
||||||
├── pack.bat # Velopack 打包脚本
|
├── pack.bat # Velopack 打包脚本
|
||||||
└── henlo_migration_wpf10.csproj
|
└── henlo_migration_wpf10.csproj
|
||||||
```
|
```
|
||||||
@@ -89,7 +90,7 @@ henlo_migration_wpf10/
|
|||||||
- **源库**:读取数据的数据库
|
- **源库**:读取数据的数据库
|
||||||
- **目标库**:写入数据的数据库
|
- **目标库**:写入数据的数据库
|
||||||
|
|
||||||
连接信息保存在 `config.ini` 中。
|
连接信息保存在运行目录的 `config.ini` 中。发布包不携带该文件,首次运行会创建空配置。
|
||||||
|
|
||||||
### 2. 索引文件
|
### 2. 索引文件
|
||||||
|
|
||||||
@@ -99,10 +100,16 @@ henlo_migration_wpf10/
|
|||||||
|------|------|
|
|------|------|
|
||||||
| `table.txt` | 表结构列表 |
|
| `table.txt` | 表结构列表 |
|
||||||
| `bos.txt` | BOS 配置列表 |
|
| `bos.txt` | BOS 配置列表 |
|
||||||
|
| `bos_field.txt` | BOS 单字段配置列表,每行使用 `TABLE.COLUMN` 格式 |
|
||||||
| `prc.txt` | 存储过程/函数列表 |
|
| `prc.txt` | 存储过程/函数列表 |
|
||||||
| `data.txt` | 表数据列表 |
|
| `data.txt` | 表数据列表 |
|
||||||
|
|
||||||
每行一个对象名称,读取数据库时会自动更新内容。
|
每行一个对象名称,读取数据库时会自动更新内容。BOS 单字段配置生成
|
||||||
|
`data/{表名}.{字段名}_bos_field.txt`;源库缺少单字段读取过程时,程序会从
|
||||||
|
`pre/hmig_get_table_bos_field_31.prc` 自动安装后再读取。
|
||||||
|
|
||||||
|
单字段配置写入时,目标库缺少的限定值组和限定值会自动补充;引用字段只做存在性和唯一性检查,不存在或不唯一时停止并报错。
|
||||||
|
若源对象是物理表且存在同名物理字段,生成的 SQL 还会检查目标表字段,并在字段缺失时按源字段类型执行 `ALTER TABLE ... ADD`;目标物理表不存在时直接报错。视图字段和 BOS 计算字段不会创建物理字段。
|
||||||
|
|
||||||
### 3. 基本操作流程
|
### 3. 基本操作流程
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
|
using Oracle.ManagedDataAccess.Client;
|
||||||
|
using Oracle.ManagedDataAccess.Types;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
@@ -132,12 +134,77 @@ namespace henlo_migration.Services
|
|||||||
private static string EscapeQuote(string s)
|
private static string EscapeQuote(string s)
|
||||||
=> s?.Replace("'", "''") ?? "";
|
=> s?.Replace("'", "''") ?? "";
|
||||||
|
|
||||||
/// <summary>获取BOS表配置 - 调用 hmig_get_table_bos_31 存储过程获取 r_table,再查 tabletext</summary>
|
private const string BosReaderProcedureName = "HMIG_GET_TABLE_BOS_31";
|
||||||
|
private const string BosReaderProcedureFileName = "hmig_get_table_bos_31.prc";
|
||||||
|
private const string BosFieldReaderProcedureName = "HMIG_GET_TABLE_BOS_FIELD_31";
|
||||||
|
private const string BosFieldReaderProcedureFileName = "hmig_get_table_bos_field_31.prc";
|
||||||
|
|
||||||
|
/// <summary>确保源库存在且启用了指定辅助过程;缺失时从 pre 目录安装</summary>
|
||||||
|
private static void EnsureReaderProcedure(string procedureName, string procedureFileName)
|
||||||
|
{
|
||||||
|
var db = SqlSugarHelper.DbOrig;
|
||||||
|
var objectResult = db.Ado.GetDataTable(
|
||||||
|
"SELECT STATUS FROM USER_OBJECTS WHERE OBJECT_NAME = :name AND OBJECT_TYPE = 'PROCEDURE'",
|
||||||
|
new SugarParameter("name", procedureName));
|
||||||
|
|
||||||
|
if (objectResult.Rows.Count > 0)
|
||||||
|
{
|
||||||
|
if (string.Equals(objectResult.Rows[0]["STATUS"]?.ToString(), "VALID", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ThrowProcedureErrors(procedureName, $"源库过程 {procedureName} 状态无效");
|
||||||
|
throw new InvalidOperationException($"源库过程 {procedureName} 状态无效,请检查后重试。");
|
||||||
|
}
|
||||||
|
|
||||||
|
string scriptPath = Path.Combine(CommonHelper.PrePath, procedureFileName);
|
||||||
|
if (!File.Exists(scriptPath))
|
||||||
|
throw new FileNotFoundException($"缺少 BOS 配置读取过程脚本:{scriptPath}", scriptPath);
|
||||||
|
|
||||||
|
string script = File.ReadAllText(scriptPath).TrimEnd();
|
||||||
|
// 兼容带 SQL*Plus 结束符的脚本;ODP.NET 执行时不能包含末尾的独立斜杠。
|
||||||
|
if (script.EndsWith("/", StringComparison.Ordinal))
|
||||||
|
script = script.Substring(0, script.Length - 1).TrimEnd();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(script))
|
||||||
|
throw new InvalidDataException($"BOS 配置读取过程脚本为空:{scriptPath}");
|
||||||
|
|
||||||
|
db.Ado.ExecuteCommand(script);
|
||||||
|
|
||||||
|
ThrowProcedureErrors(procedureName, $"自动安装 {procedureName} 后存在编译错误");
|
||||||
|
|
||||||
|
var installedResult = db.Ado.GetDataTable(
|
||||||
|
"SELECT STATUS FROM USER_OBJECTS WHERE OBJECT_NAME = :name AND OBJECT_TYPE = 'PROCEDURE'",
|
||||||
|
new SugarParameter("name", procedureName));
|
||||||
|
if (installedResult.Rows.Count == 0 ||
|
||||||
|
!string.Equals(installedResult.Rows[0]["STATUS"]?.ToString(), "VALID", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"自动安装 {procedureName} 失败,源库中未找到有效过程。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ThrowProcedureErrors(string procedureName, string title)
|
||||||
|
{
|
||||||
|
var errors = SqlSugarHelper.DbOrig.Ado.GetDataTable(
|
||||||
|
"SELECT LINE, POSITION, TEXT FROM USER_ERRORS WHERE NAME = :name AND TYPE = 'PROCEDURE' ORDER BY SEQUENCE",
|
||||||
|
new SugarParameter("name", procedureName));
|
||||||
|
if (errors.Rows.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var message = new StringBuilder();
|
||||||
|
message.AppendLine(title + ":");
|
||||||
|
foreach (DataRow row in errors.Rows)
|
||||||
|
message.AppendLine($"Line {row["LINE"]}, Pos {row["POSITION"]}: {row["TEXT"]}");
|
||||||
|
throw new InvalidOperationException(message.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>获取BOS表配置 - 确保过程存在后,调用过程获取 r_table,再查 tabletext</summary>
|
||||||
public static string GetTableBos(string tableName)
|
public static string GetTableBos(string tableName)
|
||||||
{
|
{
|
||||||
var db = SqlSugarHelper.DbOrig;
|
var db = SqlSugarHelper.DbOrig;
|
||||||
string t = tableName.Trim().ToUpperInvariant();
|
string t = tableName.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
EnsureReaderProcedure(BosReaderProcedureName, BosReaderProcedureFileName);
|
||||||
|
|
||||||
// 1. 调用存储过程,获取 r_id 和 r_table
|
// 1. 调用存储过程,获取 r_id 和 r_table
|
||||||
var pRId = new SugarParameter("r_id", null, true) { DbType = System.Data.DbType.Decimal };
|
var pRId = new SugarParameter("r_id", null, true) { DbType = System.Data.DbType.Decimal };
|
||||||
var pRTable = new SugarParameter("r_table", null, true) { DbType = System.Data.DbType.String, Size = 200 };
|
var pRTable = new SugarParameter("r_table", null, true) { DbType = System.Data.DbType.String, Size = 200 };
|
||||||
@@ -162,6 +229,165 @@ namespace henlo_migration.Services
|
|||||||
return textResult.Rows.Count > 0 ? textResult.Rows[0][colName]?.ToString() ?? "" : "";
|
return textResult.Rows.Count > 0 ? textResult.Rows[0][colName]?.ToString() ?? "" : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>解析 TABLE.COLUMN 格式的 BOS 字段名称</summary>
|
||||||
|
public static bool TryParseBosFieldName(string qualifiedFieldName, out string tableName, out string columnName)
|
||||||
|
{
|
||||||
|
tableName = "";
|
||||||
|
columnName = "";
|
||||||
|
if (string.IsNullOrWhiteSpace(qualifiedFieldName))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string value = qualifiedFieldName.Trim();
|
||||||
|
int separator = value.IndexOf('.');
|
||||||
|
if (separator <= 0 || separator != value.LastIndexOf('.') || separator >= value.Length - 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
tableName = value.Substring(0, separator).Trim().ToUpperInvariant();
|
||||||
|
columnName = value.Substring(separator + 1).Trim().ToUpperInvariant();
|
||||||
|
return tableName.Length > 0 && columnName.Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>获取单个 BOS 字段配置,返回可在目标库执行的完整 PL/SQL</summary>
|
||||||
|
public static string GetTableBosField(string qualifiedFieldName)
|
||||||
|
{
|
||||||
|
if (!TryParseBosFieldName(qualifiedFieldName, out string tableName, out string columnName))
|
||||||
|
throw new ArgumentException("BOS 字段配置必须使用 TABLE.COLUMN 格式。", nameof(qualifiedFieldName));
|
||||||
|
|
||||||
|
string physicalColumnSql = BuildPhysicalColumnEnsureSql(tableName, columnName);
|
||||||
|
EnsureReaderProcedure(BosFieldReaderProcedureName, BosFieldReaderProcedureFileName);
|
||||||
|
|
||||||
|
var connection = SqlSugarHelper.DbOrig.Ado.Connection as OracleConnection
|
||||||
|
?? throw new InvalidOperationException("源数据库连接不是 OracleConnection,无法读取 CLOB 输出参数。");
|
||||||
|
bool closeAfterRead = connection.State != ConnectionState.Open;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (closeAfterRead)
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.BindByName = true;
|
||||||
|
command.CommandText =
|
||||||
|
"BEGIN hmig_get_table_bos_field_31(:p_table_name, :p_column_name, :r_sql); END;";
|
||||||
|
command.Parameters.Add(new OracleParameter("p_table_name", OracleDbType.Varchar2)
|
||||||
|
{
|
||||||
|
Direction = ParameterDirection.Input,
|
||||||
|
Value = tableName
|
||||||
|
});
|
||||||
|
command.Parameters.Add(new OracleParameter("p_column_name", OracleDbType.Varchar2)
|
||||||
|
{
|
||||||
|
Direction = ParameterDirection.Input,
|
||||||
|
Value = columnName
|
||||||
|
});
|
||||||
|
var output = new OracleParameter("r_sql", OracleDbType.Clob)
|
||||||
|
{
|
||||||
|
Direction = ParameterDirection.Output
|
||||||
|
};
|
||||||
|
command.Parameters.Add(output);
|
||||||
|
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
string bosSql;
|
||||||
|
if (output.Value == null || output.Value == DBNull.Value)
|
||||||
|
bosSql = "";
|
||||||
|
else if (output.Value is OracleClob clob)
|
||||||
|
bosSql = clob.IsNull ? "" : clob.Value;
|
||||||
|
else
|
||||||
|
bosSql = output.Value.ToString() ?? "";
|
||||||
|
|
||||||
|
// 旧版过程输出中的说明只针对其自身生成的 BOS 块;最终文件已在开头加入物理字段检查。
|
||||||
|
bosSql = bosSql.Replace(
|
||||||
|
"-- 注意:本文件不包含 Oracle 物理字段 DDL。",
|
||||||
|
"-- BOS 字段配置块;物理字段检查位于本文件开头。",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
return physicalColumnSql + bosSql;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (closeAfterRead && connection.State != ConnectionState.Closed)
|
||||||
|
connection.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>生成目标库物理字段存在性检查;源对象不是物理表或字段时只输出说明</summary>
|
||||||
|
private static string BuildPhysicalColumnEnsureSql(string tableName, string columnName)
|
||||||
|
{
|
||||||
|
var db = SqlSugarHelper.DbOrig;
|
||||||
|
int sourceTableCount = db.Ado.GetInt(
|
||||||
|
"SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = :table_name",
|
||||||
|
new SugarParameter("table_name", tableName));
|
||||||
|
if (sourceTableCount == 0)
|
||||||
|
return $"-- 源库对象 {tableName} 不是物理表,不生成物理字段 DDL。\n";
|
||||||
|
|
||||||
|
var columnResult = db.Ado.GetDataTable(@"
|
||||||
|
SELECT DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE,
|
||||||
|
CHAR_LENGTH, CHAR_USED
|
||||||
|
FROM USER_TAB_COLUMNS
|
||||||
|
WHERE TABLE_NAME = :table_name
|
||||||
|
AND COLUMN_NAME = :column_name",
|
||||||
|
new SugarParameter("table_name", tableName),
|
||||||
|
new SugarParameter("column_name", columnName));
|
||||||
|
if (columnResult.Rows.Count == 0)
|
||||||
|
return $"-- 源库物理表 {tableName} 不存在字段 {columnName},仅导出 BOS 字段配置。\n";
|
||||||
|
|
||||||
|
string columnType = BuildOracleColumnType(columnResult.Rows[0]);
|
||||||
|
string quotedTable = QuoteOracleIdentifier(tableName);
|
||||||
|
string quotedColumn = QuoteOracleIdentifier(columnName);
|
||||||
|
string alterSql = $"ALTER TABLE {quotedTable} ADD ({quotedColumn} {columnType})";
|
||||||
|
|
||||||
|
var sql = new StringBuilder();
|
||||||
|
sql.AppendLine($"-- 确保目标库物理字段存在:{tableName}.{columnName}");
|
||||||
|
sql.AppendLine("DECLARE");
|
||||||
|
sql.AppendLine(" v_table_count NUMBER;");
|
||||||
|
sql.AppendLine(" v_column_count NUMBER;");
|
||||||
|
sql.AppendLine("BEGIN");
|
||||||
|
sql.AppendLine($" SELECT COUNT(*) INTO v_table_count FROM user_tables WHERE table_name = '{EscapeQuote(tableName)}';");
|
||||||
|
sql.AppendLine(" IF v_table_count = 0 THEN");
|
||||||
|
sql.AppendLine($" RAISE_APPLICATION_ERROR(-20201, '目标库缺少物理表:{EscapeQuote(tableName)}');");
|
||||||
|
sql.AppendLine(" END IF;");
|
||||||
|
sql.AppendLine($" SELECT COUNT(*) INTO v_column_count FROM user_tab_columns WHERE table_name = '{EscapeQuote(tableName)}' AND column_name = '{EscapeQuote(columnName)}';");
|
||||||
|
sql.AppendLine(" IF v_column_count = 0 THEN");
|
||||||
|
sql.AppendLine($" EXECUTE IMMEDIATE '{EscapeQuote(alterSql)}';");
|
||||||
|
sql.AppendLine(" END IF;");
|
||||||
|
sql.AppendLine("END;");
|
||||||
|
sql.AppendLine("/");
|
||||||
|
return sql.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildOracleColumnType(DataRow row)
|
||||||
|
{
|
||||||
|
string dataType = row["DATA_TYPE"]?.ToString()?.ToUpperInvariant() ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(dataType))
|
||||||
|
throw new InvalidDataException("无法读取源库物理字段的数据类型。");
|
||||||
|
|
||||||
|
long dataLength = Convert.ToInt64(row["DATA_LENGTH"]);
|
||||||
|
long? precision = row["DATA_PRECISION"] == DBNull.Value
|
||||||
|
? null
|
||||||
|
: Convert.ToInt64(row["DATA_PRECISION"]);
|
||||||
|
long? scale = row["DATA_SCALE"] == DBNull.Value
|
||||||
|
? null
|
||||||
|
: Convert.ToInt64(row["DATA_SCALE"]);
|
||||||
|
long charLength = row["CHAR_LENGTH"] == DBNull.Value
|
||||||
|
? dataLength
|
||||||
|
: Convert.ToInt64(row["CHAR_LENGTH"]);
|
||||||
|
string charUsed = row["CHAR_USED"]?.ToString()?.ToUpperInvariant() ?? "";
|
||||||
|
|
||||||
|
return dataType switch
|
||||||
|
{
|
||||||
|
"CHAR" or "VARCHAR2" =>
|
||||||
|
$"{dataType}({(charUsed == "C" ? charLength : dataLength)} {(charUsed == "C" ? "CHAR" : "BYTE")})",
|
||||||
|
"NCHAR" or "NVARCHAR2" => $"{dataType}({charLength})",
|
||||||
|
"NUMBER" when precision.HasValue && scale.HasValue => $"NUMBER({precision.Value},{scale.Value})",
|
||||||
|
"NUMBER" when precision.HasValue => $"NUMBER({precision.Value})",
|
||||||
|
"FLOAT" when precision.HasValue => $"FLOAT({precision.Value})",
|
||||||
|
"RAW" => $"RAW({dataLength})",
|
||||||
|
"UROWID" => $"UROWID({dataLength})",
|
||||||
|
_ => dataType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string QuoteOracleIdentifier(string value)
|
||||||
|
=> $"\"{value.Replace("\"", "\"\"")}\"";
|
||||||
|
|
||||||
/// <summary>获取所有存储过程和函数列表 - 查 USER_OBJECTS,返回纯名称(不带前缀),不依赖存储过程</summary>
|
/// <summary>获取所有存储过程和函数列表 - 查 USER_OBJECTS,返回纯名称(不带前缀),不依赖存储过程</summary>
|
||||||
public static string GetAllPrc()
|
public static string GetAllPrc()
|
||||||
{
|
{
|
||||||
@@ -396,18 +622,6 @@ namespace henlo_migration.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>执行预处理存储过程</summary>
|
|
||||||
public static void PreProcess()
|
|
||||||
{
|
|
||||||
var dbOrig = SqlSugarHelper.DbOrig;
|
|
||||||
foreach (var fname in Directory.GetFiles(CommonHelper.PrePath))
|
|
||||||
{
|
|
||||||
string name = Path.GetFileNameWithoutExtension(fname);
|
|
||||||
string content = File.ReadAllText(fname);
|
|
||||||
dbOrig.Ado.ExecuteCommand(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string[] SplitStatements(string sql)
|
private static string[] SplitStatements(string sql)
|
||||||
{
|
{
|
||||||
// Oracle PL/SQL 块用 \n/ 分隔
|
// Oracle PL/SQL 块用 \n/ 分隔
|
||||||
@@ -423,4 +637,4 @@ namespace henlo_migration.Services
|
|||||||
return result.ToArray();
|
return result.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ namespace henlo_migration.ViewModels
|
|||||||
{
|
{
|
||||||
public class MainViewModel : INotifyPropertyChanged
|
public class MainViewModel : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
// 四个 Tab 的数据
|
// 五个 Tab 的数据
|
||||||
public TabServices TableTab { get; }
|
public TabServices TableTab { get; }
|
||||||
public TabServices BosTab { get; }
|
public TabServices BosTab { get; }
|
||||||
|
public TabServices BosFieldTab { get; }
|
||||||
public TabServices PrcTab { get; }
|
public TabServices PrcTab { get; }
|
||||||
public TabServices DataTab { get; }
|
public TabServices DataTab { get; }
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ namespace henlo_migration.ViewModels
|
|||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(TablesData));
|
OnPropertyChanged(nameof(TablesData));
|
||||||
OnPropertyChanged(nameof(BosTablesData));
|
OnPropertyChanged(nameof(BosTablesData));
|
||||||
|
OnPropertyChanged(nameof(BosFieldsData));
|
||||||
OnPropertyChanged(nameof(StoredProcsData));
|
OnPropertyChanged(nameof(StoredProcsData));
|
||||||
OnPropertyChanged(nameof(CurrentDataData));
|
OnPropertyChanged(nameof(CurrentDataData));
|
||||||
}
|
}
|
||||||
@@ -50,13 +52,15 @@ namespace henlo_migration.ViewModels
|
|||||||
{
|
{
|
||||||
0 => TableTab,
|
0 => TableTab,
|
||||||
1 => BosTab,
|
1 => BosTab,
|
||||||
2 => PrcTab,
|
2 => BosFieldTab,
|
||||||
3 => DataTab,
|
3 => PrcTab,
|
||||||
|
4 => DataTab,
|
||||||
_ => TableTab
|
_ => TableTab
|
||||||
};
|
};
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(TablesData));
|
OnPropertyChanged(nameof(TablesData));
|
||||||
OnPropertyChanged(nameof(BosTablesData));
|
OnPropertyChanged(nameof(BosTablesData));
|
||||||
|
OnPropertyChanged(nameof(BosFieldsData));
|
||||||
OnPropertyChanged(nameof(StoredProcsData));
|
OnPropertyChanged(nameof(StoredProcsData));
|
||||||
OnPropertyChanged(nameof(CurrentDataData));
|
OnPropertyChanged(nameof(CurrentDataData));
|
||||||
OnPropertyChanged(nameof(CurrentTabName));
|
OnPropertyChanged(nameof(CurrentTabName));
|
||||||
@@ -64,9 +68,10 @@ namespace henlo_migration.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 四个Tab数据源别名(供DataGrid绑定)
|
// 五个Tab数据源别名(供DataGrid绑定)
|
||||||
public ObservableCollection<TableRowModel> TablesData => TableTab.List;
|
public ObservableCollection<TableRowModel> TablesData => TableTab.List;
|
||||||
public ObservableCollection<TableRowModel> BosTablesData => BosTab.List;
|
public ObservableCollection<TableRowModel> BosTablesData => BosTab.List;
|
||||||
|
public ObservableCollection<TableRowModel> BosFieldsData => BosFieldTab.List;
|
||||||
public ObservableCollection<TableRowModel> StoredProcsData => PrcTab.List;
|
public ObservableCollection<TableRowModel> StoredProcsData => PrcTab.List;
|
||||||
public ObservableCollection<TableRowModel> CurrentDataData => DataTab.List;
|
public ObservableCollection<TableRowModel> CurrentDataData => DataTab.List;
|
||||||
|
|
||||||
@@ -74,8 +79,9 @@ namespace henlo_migration.ViewModels
|
|||||||
{
|
{
|
||||||
0 => "表配置",
|
0 => "表配置",
|
||||||
1 => "BOS配置",
|
1 => "BOS配置",
|
||||||
2 => "存储过程",
|
2 => "BOS字段配置",
|
||||||
3 => "表数据",
|
3 => "存储过程",
|
||||||
|
4 => "表数据",
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,7 +246,6 @@ namespace henlo_migration.ViewModels
|
|||||||
public ICommand DisconnectDestCommand { get; }
|
public ICommand DisconnectDestCommand { get; }
|
||||||
public ICommand AddRowCommand { get; }
|
public ICommand AddRowCommand { get; }
|
||||||
public ICommand OpenDataFolderCommand { get; }
|
public ICommand OpenDataFolderCommand { get; }
|
||||||
public ICommand PreProcessCommand { get; }
|
|
||||||
public ICommand BatchReadCommand { get; }
|
public ICommand BatchReadCommand { get; }
|
||||||
public ICommand BatchWriteCommand { get; }
|
public ICommand BatchWriteCommand { get; }
|
||||||
|
|
||||||
@@ -264,13 +269,16 @@ namespace henlo_migration.ViewModels
|
|||||||
DestUser = SysConfig.GetDestUser();
|
DestUser = SysConfig.GetDestUser();
|
||||||
DestPwd = SysConfig.GetDestPwd();
|
DestPwd = SysConfig.GetDestPwd();
|
||||||
|
|
||||||
// 初始化四个 TabServices(索引文件在 from/,内容文件在 data/)
|
// 初始化五个 TabServices(索引文件在 from/,内容文件在 data/)
|
||||||
TableTab = new TabServices(new TabConfig(TabsEnum.表, CommonHelper.DataFromPath, CommonHelper.DataPath, "table.txt", CodeExt.creat,
|
TableTab = new TabServices(new TabConfig(TabsEnum.表, CommonHelper.DataFromPath, CommonHelper.DataPath, "table.txt", CodeExt.creat,
|
||||||
DbServices.GetTableSql, (p, c) => SqlCode = c));
|
DbServices.GetTableSql, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
BosTab = new TabServices(new TabConfig(TabsEnum.BOS表配置, CommonHelper.DataFromPath, CommonHelper.DataPath, "bos.txt", CodeExt.bos,
|
BosTab = new TabServices(new TabConfig(TabsEnum.BOS表配置, CommonHelper.DataFromPath, CommonHelper.DataPath, "bos.txt", CodeExt.bos,
|
||||||
DbServices.GetTableBos, (p, c) => SqlCode = c));
|
DbServices.GetTableBos, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
|
BosFieldTab = new TabServices(new TabConfig(TabsEnum.BOS字段配置, CommonHelper.DataFromPath, CommonHelper.DataPath, "bos_field.txt", CodeExt.bos_field,
|
||||||
|
DbServices.GetTableBosField, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
PrcTab = new TabServices(new TabConfig(TabsEnum.存储过程, CommonHelper.DataFromPath, CommonHelper.DataPath, "prc.txt", CodeExt.prc,
|
PrcTab = new TabServices(new TabConfig(TabsEnum.存储过程, CommonHelper.DataFromPath, CommonHelper.DataPath, "prc.txt", CodeExt.prc,
|
||||||
name => DbServices.GetPrcSql(name), (p, c) => SqlCode = c));
|
name => DbServices.GetPrcSql(name), (p, c) => SqlCode = c));
|
||||||
|
|
||||||
@@ -278,7 +286,7 @@ namespace henlo_migration.ViewModels
|
|||||||
DbServices.GetTableData, (p, c) => SqlCode = c));
|
DbServices.GetTableData, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
// Tab行点击 -> 显示SQL
|
// Tab行点击 -> 显示SQL
|
||||||
foreach (var tab in new[] { TableTab, BosTab, PrcTab, DataTab })
|
foreach (var tab in new[] { TableTab, BosTab, BosFieldTab, PrcTab, DataTab })
|
||||||
{
|
{
|
||||||
tab.RowClickEvent += (path, code) =>
|
tab.RowClickEvent += (path, code) =>
|
||||||
{
|
{
|
||||||
@@ -297,7 +305,6 @@ namespace henlo_migration.ViewModels
|
|||||||
DisconnectDestCommand = new RelayCommand(DisconnectDest, () => DestConnected);
|
DisconnectDestCommand = new RelayCommand(DisconnectDest, () => DestConnected);
|
||||||
AddRowCommand = new RelayCommand(AddRow, () => CurrentTab != null && !string.IsNullOrWhiteSpace(InputText));
|
AddRowCommand = new RelayCommand(AddRow, () => CurrentTab != null && !string.IsNullOrWhiteSpace(InputText));
|
||||||
OpenDataFolderCommand = new RelayCommand(OpenDataFolder);
|
OpenDataFolderCommand = new RelayCommand(OpenDataFolder);
|
||||||
PreProcessCommand = new RelayCommand(PreProcess);
|
|
||||||
BatchReadCommand = new RelayCommand(async () => await BatchReadAsync());
|
BatchReadCommand = new RelayCommand(async () => await BatchReadAsync());
|
||||||
BatchWriteCommand = new RelayCommand(async () => await BatchWriteAsync());
|
BatchWriteCommand = new RelayCommand(async () => await BatchWriteAsync());
|
||||||
|
|
||||||
@@ -372,7 +379,18 @@ namespace henlo_migration.ViewModels
|
|||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(InputText))
|
if (!string.IsNullOrWhiteSpace(InputText))
|
||||||
{
|
{
|
||||||
CurrentTab?.AddRow(InputText.Trim().ToLower());
|
string itemName = InputText.Trim();
|
||||||
|
if (CurrentTab == BosFieldTab)
|
||||||
|
{
|
||||||
|
if (!DbServices.TryParseBosFieldName(itemName, out string tableName, out string columnName))
|
||||||
|
{
|
||||||
|
HandyControl.Controls.Growl.Warning("BOS 字段配置必须使用 TABLE.COLUMN 格式");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
itemName = $"{tableName}.{columnName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentTab?.AddRow(itemName.ToLowerInvariant());
|
||||||
InputText = "";
|
InputText = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,12 +414,6 @@ namespace henlo_migration.ViewModels
|
|||||||
Process.Start("explorer.exe", CommonHelper.DataPath);
|
Process.Start("explorer.exe", CommonHelper.DataPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PreProcess()
|
|
||||||
{
|
|
||||||
try { DbServices.PreProcess(); }
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task BatchReadAsync()
|
private async Task BatchReadAsync()
|
||||||
{
|
{
|
||||||
if (CurrentTab == null) return;
|
if (CurrentTab == null) return;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
<!-- 输入框 -->
|
<!-- 输入框 -->
|
||||||
<hc:TextBox Grid.Column="0"
|
<hc:TextBox Grid.Column="0"
|
||||||
hc:InfoElement.Placeholder="输入表名或存储过程名,按回车添加..."
|
hc:InfoElement.Placeholder="输入表名、TABLE.COLUMN 或存储过程名,按回车添加..."
|
||||||
Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"
|
Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Height="32" VerticalContentAlignment="Center">
|
Height="32" VerticalContentAlignment="Center">
|
||||||
<hc:TextBox.InputBindings>
|
<hc:TextBox.InputBindings>
|
||||||
@@ -86,9 +86,6 @@
|
|||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Button.Content>
|
</Button.Content>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Content="预处理" Width="70" Height="32" Margin="8,0,0,0"
|
|
||||||
Style="{StaticResource ButtonWarning}"
|
|
||||||
Command="{Binding PreProcessCommand}"/>
|
|
||||||
<Button Content="打开数据目录" Width="100" Height="32" Margin="8,0,0,0"
|
<Button Content="打开数据目录" Width="100" Height="32" Margin="8,0,0,0"
|
||||||
Style="{StaticResource ButtonDefault}"
|
Style="{StaticResource ButtonDefault}"
|
||||||
Command="{Binding OpenDataFolderCommand}"/>
|
Command="{Binding OpenDataFolderCommand}"/>
|
||||||
@@ -420,6 +417,78 @@
|
|||||||
</DataGrid>
|
</DataGrid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- BOS字段配置 -->
|
||||||
|
<TabItem Header="BOS字段配置">
|
||||||
|
<DataGrid x:Name="BosFieldGrid" ItemsSource="{Binding BosFieldsData}"
|
||||||
|
SelectedItem="{Binding SelectedRow}"
|
||||||
|
RowStyle="{StaticResource DataGridRowStyleFix}"
|
||||||
|
CellStyle="{StaticResource DataGridCellStyleFix}"
|
||||||
|
AutoGenerateColumns="False" CanUserAddRows="False"
|
||||||
|
IsReadOnly="True" HeadersVisibility="Column"
|
||||||
|
GridLinesVisibility="Horizontal" BorderThickness="1"
|
||||||
|
BorderBrush="#d0d7de">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTemplateColumn Header="表名.字段名" Width="*" MinWidth="180">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Name}" Style="{StaticResource CopyableNameWhenSelected}" b:ClickToCopy.Selectable="True" VerticalAlignment="Center"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
<DataGridTemplateColumn Header="状态" Width="160">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Border Style="{StaticResource StatusBadge}">
|
||||||
|
<Border.Background>
|
||||||
|
<MultiBinding Converter="{StaticResource ReadStatusColorConverter}">
|
||||||
|
<Binding Path="RStatus"/>
|
||||||
|
</MultiBinding>
|
||||||
|
</Border.Background>
|
||||||
|
<TextBlock Text="{Binding ReadStatusText}" Style="{StaticResource StatusBadgeText}"/>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource StatusBadge}" Margin="6,0,0,0">
|
||||||
|
<Border.Background>
|
||||||
|
<MultiBinding Converter="{StaticResource WriteStatusColorConverter}">
|
||||||
|
<Binding Path="WStatus"/>
|
||||||
|
</MultiBinding>
|
||||||
|
</Border.Background>
|
||||||
|
<TextBlock Text="{Binding WriteStatusText}" Style="{StaticResource StatusBadgeText}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
<DataGridTemplateColumn Header="操作" Width="320">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="读取" Style="{StaticResource SmallBtn}"
|
||||||
|
Command="{Binding DataContext.ReadCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="写入" Style="{StaticResource SmallSuccessBtn}"
|
||||||
|
Command="{Binding DataContext.WriteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="删除" Style="{StaticResource SmallWarnBtn}"
|
||||||
|
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="↑" Style="{StaticResource SmallDefaultBtn}" MinWidth="36"
|
||||||
|
Command="{Binding DataContext.MoveUpCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="↓" Style="{StaticResource SmallDefaultBtn}" MinWidth="36"
|
||||||
|
Command="{Binding DataContext.MoveDownCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="Top" Style="{StaticResource SmallDefaultBtn}" MinWidth="40"
|
||||||
|
Command="{Binding DataContext.MoveTopCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
<!-- 存储过程 -->
|
<!-- 存储过程 -->
|
||||||
<TabItem Header="存储过程">
|
<TabItem Header="存储过程">
|
||||||
<DataGrid x:Name="PrcGrid" ItemsSource="{Binding StoredProcsData}"
|
<DataGrid x:Name="PrcGrid" ItemsSource="{Binding StoredProcsData}"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
<ApplicationIcon>Resources\favicon.ico</ApplicationIcon>
|
<ApplicationIcon>Resources\favicon.ico</ApplicationIcon>
|
||||||
<Deterministic>true</Deterministic>
|
<Deterministic>true</Deterministic>
|
||||||
<Version>1.0.16</Version>
|
<Version>1.0.17</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -22,21 +22,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="config.ini">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="data\from\table.txt">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="data\from\bos.txt">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="data\from\prc.txt">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="data\from\data.txt">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="pre\*.txt">
|
<None Update="pre\*.txt">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
@@ -51,4 +36,4 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Resource Include="Resources\favicon.ico" />
|
<Resource Include="Resources\favicon.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ if errorlevel 1 (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
REM Never ship the developer's local Oracle connection configuration.
|
||||||
|
if exist "!PUBLISH_DIR!\config.ini" del /q "!PUBLISH_DIR!\config.ini"
|
||||||
|
|
||||||
echo [2.5/3] Create data directories in publish (user runtime data)...
|
echo [2.5/3] Create data directories in publish (user runtime data)...
|
||||||
if not exist "!PUBLISH_DIR!\data" mkdir "!PUBLISH_DIR!\data"
|
if not exist "!PUBLISH_DIR!\data" mkdir "!PUBLISH_DIR!\data"
|
||||||
echo. 2>nul > "!PUBLISH_DIR!\data\.gitkeep"
|
echo. 2>nul > "!PUBLISH_DIR!\data\.gitkeep"
|
||||||
|
|||||||
@@ -0,0 +1,423 @@
|
|||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_table_bos_field_31(
|
||||||
|
p_table_name IN VARCHAR2,
|
||||||
|
p_column_name IN VARCHAR2,
|
||||||
|
r_sql OUT CLOB
|
||||||
|
) AS
|
||||||
|
/*
|
||||||
|
按 BOS 表名 + 字段 DBNAME 导出单个 AD_COLUMN 配置。
|
||||||
|
|
||||||
|
输出是可在目标库执行的 PL/SQL:
|
||||||
|
1. 在目标库按 AD_TABLE.NAME 重新解析 AD_TABLE_ID;
|
||||||
|
2. 按 AD_TABLE_ID + DBNAME 判断字段是否已经存在;
|
||||||
|
3. 已存在则保留目标字段 ID 并更新配置;
|
||||||
|
4. 不存在则通过 get_sequences('AD_COLUMN') 生成新 ID 后插入。
|
||||||
|
|
||||||
|
限定值处理:
|
||||||
|
- 通过 AD_LIMITVALUE_GROUP.NAME 在目标库定位限定值组;
|
||||||
|
- 组不存在时新增;
|
||||||
|
- 组内的 AD_LIMITVALUE.VALUE 不存在时逐项新增。
|
||||||
|
|
||||||
|
引用字段处理:
|
||||||
|
- 将源 REF_COLUMN_ID 解析为 AD_TABLE.NAME + AD_COLUMN.DBNAME;
|
||||||
|
- 在目标库按名称重新定位;
|
||||||
|
- 引用字段不存在或不唯一时直接报错,不自动创建。
|
||||||
|
|
||||||
|
当前版本不包含 Oracle 物理表字段 DDL。
|
||||||
|
|
||||||
|
SQL Developer 验证示例:
|
||||||
|
DECLARE
|
||||||
|
v_sql CLOB;
|
||||||
|
BEGIN
|
||||||
|
hmig_get_table_bos_field_31('M_RETAIL', 'DOCNO', v_sql);
|
||||||
|
DBMS_OUTPUT.PUT_LINE(DBMS_LOB.SUBSTR(v_sql, 32767, 1));
|
||||||
|
END;
|
||||||
|
/
|
||||||
|
*/
|
||||||
|
|
||||||
|
c_source_table CONSTANT VARCHAR2(30) := 'AD_COLUMN';
|
||||||
|
|
||||||
|
v_table_name VARCHAR2(128) := UPPER(TRIM(p_table_name));
|
||||||
|
v_column_name VARCHAR2(128) := UPPER(TRIM(p_column_name));
|
||||||
|
v_source_table_id NUMBER;
|
||||||
|
v_source_column_id NUMBER;
|
||||||
|
v_source_limit_group_id NUMBER;
|
||||||
|
v_source_ref_column_id NUMBER;
|
||||||
|
v_limit_group_name VARCHAR2(200);
|
||||||
|
v_limit_group_comments VARCHAR2(4000);
|
||||||
|
v_limit_group_valuetype VARCHAR2(200);
|
||||||
|
v_limit_group_ownerid NUMBER;
|
||||||
|
v_ref_table_name VARCHAR2(128);
|
||||||
|
v_ref_column_name VARCHAR2(128);
|
||||||
|
v_value VARCHAR2(32767);
|
||||||
|
v_value_sql VARCHAR2(32767);
|
||||||
|
v_select_sql VARCHAR2(32767);
|
||||||
|
v_insert_columns VARCHAR2(32767);
|
||||||
|
v_insert_values VARCHAR2(32767);
|
||||||
|
v_update_values VARCHAR2(32767);
|
||||||
|
v_lob_length NUMBER;
|
||||||
|
v_separator VARCHAR2(2);
|
||||||
|
|
||||||
|
PROCEDURE append_text(p_text IN VARCHAR2) AS
|
||||||
|
BEGIN
|
||||||
|
IF p_text IS NOT NULL THEN
|
||||||
|
DBMS_LOB.WRITEAPPEND(r_sql, LENGTH(p_text), p_text);
|
||||||
|
END IF;
|
||||||
|
END append_text;
|
||||||
|
|
||||||
|
FUNCTION quoted(p_value IN VARCHAR2) RETURN VARCHAR2 AS
|
||||||
|
BEGIN
|
||||||
|
IF p_value IS NULL THEN
|
||||||
|
RETURN 'NULL';
|
||||||
|
END IF;
|
||||||
|
RETURN '''' || REPLACE(p_value, '''', '''''') || '''';
|
||||||
|
END quoted;
|
||||||
|
|
||||||
|
FUNCTION number_literal(p_value IN NUMBER) RETURN VARCHAR2 AS
|
||||||
|
BEGIN
|
||||||
|
IF p_value IS NULL THEN
|
||||||
|
RETURN 'NULL';
|
||||||
|
END IF;
|
||||||
|
RETURN TO_CHAR(p_value, 'TM9', 'NLS_NUMERIC_CHARACTERS=''.,''');
|
||||||
|
END number_literal;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
IF v_table_name IS NULL THEN
|
||||||
|
RAISE_APPLICATION_ERROR(-20201, 'BOS表名不能为空');
|
||||||
|
END IF;
|
||||||
|
IF v_column_name IS NULL THEN
|
||||||
|
RAISE_APPLICATION_ERROR(-20201, 'BOS字段DBNAME不能为空');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
SELECT id
|
||||||
|
INTO v_source_table_id
|
||||||
|
FROM ad_table
|
||||||
|
WHERE UPPER(name) = v_table_name;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN NO_DATA_FOUND THEN
|
||||||
|
RAISE_APPLICATION_ERROR(-20201, 'BOS表[' || v_table_name || ']不存在');
|
||||||
|
WHEN TOO_MANY_ROWS THEN
|
||||||
|
RAISE_APPLICATION_ERROR(-20201, 'BOS表[' || v_table_name || ']存在多条AD_TABLE配置');
|
||||||
|
END;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
SELECT id
|
||||||
|
INTO v_source_column_id
|
||||||
|
FROM ad_column
|
||||||
|
WHERE ad_table_id = v_source_table_id
|
||||||
|
AND UPPER(dbname) = v_column_name;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN NO_DATA_FOUND THEN
|
||||||
|
RAISE_APPLICATION_ERROR(
|
||||||
|
-20201,
|
||||||
|
'BOS字段[' || v_table_name || '.' || v_column_name || ']不存在'
|
||||||
|
);
|
||||||
|
WHEN TOO_MANY_ROWS THEN
|
||||||
|
RAISE_APPLICATION_ERROR(
|
||||||
|
-20201,
|
||||||
|
'BOS字段[' || v_table_name || '.' || v_column_name || ']存在多条AD_COLUMN配置'
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
SELECT ad_limitvalue_group_id, ref_column_id
|
||||||
|
INTO v_source_limit_group_id, v_source_ref_column_id
|
||||||
|
FROM ad_column
|
||||||
|
WHERE id = v_source_column_id;
|
||||||
|
|
||||||
|
IF v_source_limit_group_id IS NOT NULL THEN
|
||||||
|
BEGIN
|
||||||
|
SELECT name, comments, valuetype, ownerid
|
||||||
|
INTO v_limit_group_name,
|
||||||
|
v_limit_group_comments,
|
||||||
|
v_limit_group_valuetype,
|
||||||
|
v_limit_group_ownerid
|
||||||
|
FROM ad_limitvalue_group
|
||||||
|
WHERE id = v_source_limit_group_id;
|
||||||
|
|
||||||
|
IF v_limit_group_name IS NULL THEN
|
||||||
|
RAISE_APPLICATION_ERROR(
|
||||||
|
-20201,
|
||||||
|
'字段关联的限定值组名称为空,ID=' || v_source_limit_group_id
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN NO_DATA_FOUND THEN
|
||||||
|
RAISE_APPLICATION_ERROR(
|
||||||
|
-20201,
|
||||||
|
'字段关联的限定值组不存在,ID=' || v_source_limit_group_id
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_source_ref_column_id IS NOT NULL THEN
|
||||||
|
BEGIN
|
||||||
|
SELECT UPPER(t.name), UPPER(c.dbname)
|
||||||
|
INTO v_ref_table_name, v_ref_column_name
|
||||||
|
FROM ad_column c
|
||||||
|
JOIN ad_table t ON t.id = c.ad_table_id
|
||||||
|
WHERE c.id = v_source_ref_column_id;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN NO_DATA_FOUND THEN
|
||||||
|
RAISE_APPLICATION_ERROR(
|
||||||
|
-20201,
|
||||||
|
'字段关联的引用元数据不存在,REF_COLUMN_ID=' || v_source_ref_column_id
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DBMS_LOB.CREATETEMPORARY(r_sql, TRUE);
|
||||||
|
|
||||||
|
/*
|
||||||
|
动态遍历 AD_COLUMN 的实际字段,避免在不同 BOS 版本间硬编码列清单。
|
||||||
|
ID、AD_TABLE_ID、AD_LIMITVALUE_GROUP_ID 和 REF_COLUMN_ID 在目标库重新映射;
|
||||||
|
其余字段按源值输出。
|
||||||
|
*/
|
||||||
|
FOR col IN (
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM user_tab_columns
|
||||||
|
WHERE table_name = c_source_table
|
||||||
|
ORDER BY column_id
|
||||||
|
) LOOP
|
||||||
|
IF col.column_name = 'ID' THEN
|
||||||
|
v_value_sql := 'v_target_column_id';
|
||||||
|
ELSIF col.column_name = 'AD_TABLE_ID' THEN
|
||||||
|
v_value_sql := 'v_target_table_id';
|
||||||
|
ELSIF col.column_name = 'AD_LIMITVALUE_GROUP_ID' THEN
|
||||||
|
IF v_source_limit_group_id IS NULL THEN
|
||||||
|
v_value_sql := 'NULL';
|
||||||
|
ELSE
|
||||||
|
v_value_sql := 'v_target_limit_group_id';
|
||||||
|
END IF;
|
||||||
|
ELSIF col.column_name = 'REF_COLUMN_ID' THEN
|
||||||
|
IF v_source_ref_column_id IS NULL THEN
|
||||||
|
v_value_sql := 'NULL';
|
||||||
|
ELSE
|
||||||
|
v_value_sql := 'v_target_ref_column_id';
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
v_value := NULL;
|
||||||
|
v_lob_length := NULL;
|
||||||
|
|
||||||
|
IF col.data_type IN ('NUMBER', 'FLOAT', 'BINARY_FLOAT', 'BINARY_DOUBLE') THEN
|
||||||
|
v_select_sql :=
|
||||||
|
'SELECT CASE WHEN "' || col.column_name || '" IS NULL THEN NULL ' ||
|
||||||
|
'ELSE TO_CHAR("' || col.column_name || '", ''TM9'', ' ||
|
||||||
|
'''NLS_NUMERIC_CHARACTERS=''''.,'''''') END ' ||
|
||||||
|
'FROM AD_COLUMN WHERE ID = :id';
|
||||||
|
EXECUTE IMMEDIATE v_select_sql INTO v_value USING v_source_column_id;
|
||||||
|
v_value_sql := NVL(v_value, 'NULL');
|
||||||
|
|
||||||
|
ELSIF col.data_type = 'DATE' THEN
|
||||||
|
v_select_sql :=
|
||||||
|
'SELECT TO_CHAR("' || col.column_name || '", ''YYYY-MM-DD HH24:MI:SS'') ' ||
|
||||||
|
'FROM AD_COLUMN WHERE ID = :id';
|
||||||
|
EXECUTE IMMEDIATE v_select_sql INTO v_value USING v_source_column_id;
|
||||||
|
IF v_value IS NULL THEN
|
||||||
|
v_value_sql := 'NULL';
|
||||||
|
ELSE
|
||||||
|
v_value_sql := 'TO_DATE(' || quoted(v_value) ||
|
||||||
|
', ''YYYY-MM-DD HH24:MI:SS'')';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF col.data_type LIKE 'TIMESTAMP%WITH TIME ZONE' THEN
|
||||||
|
v_select_sql :=
|
||||||
|
'SELECT TO_CHAR("' || col.column_name ||
|
||||||
|
'", ''YYYY-MM-DD HH24:MI:SS.FF6 TZH:TZM'') ' ||
|
||||||
|
'FROM AD_COLUMN WHERE ID = :id';
|
||||||
|
EXECUTE IMMEDIATE v_select_sql INTO v_value USING v_source_column_id;
|
||||||
|
IF v_value IS NULL THEN
|
||||||
|
v_value_sql := 'NULL';
|
||||||
|
ELSE
|
||||||
|
v_value_sql := 'TO_TIMESTAMP_TZ(' || quoted(v_value) ||
|
||||||
|
', ''YYYY-MM-DD HH24:MI:SS.FF6 TZH:TZM'')';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF col.data_type LIKE 'TIMESTAMP%' THEN
|
||||||
|
v_select_sql :=
|
||||||
|
'SELECT TO_CHAR("' || col.column_name ||
|
||||||
|
'", ''YYYY-MM-DD HH24:MI:SS.FF6'') ' ||
|
||||||
|
'FROM AD_COLUMN WHERE ID = :id';
|
||||||
|
EXECUTE IMMEDIATE v_select_sql INTO v_value USING v_source_column_id;
|
||||||
|
IF v_value IS NULL THEN
|
||||||
|
v_value_sql := 'NULL';
|
||||||
|
ELSE
|
||||||
|
v_value_sql := 'TO_TIMESTAMP(' || quoted(v_value) ||
|
||||||
|
', ''YYYY-MM-DD HH24:MI:SS.FF6'')';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF col.data_type = 'RAW' THEN
|
||||||
|
v_select_sql :=
|
||||||
|
'SELECT RAWTOHEX("' || col.column_name || '") ' ||
|
||||||
|
'FROM AD_COLUMN WHERE ID = :id';
|
||||||
|
EXECUTE IMMEDIATE v_select_sql INTO v_value USING v_source_column_id;
|
||||||
|
IF v_value IS NULL THEN
|
||||||
|
v_value_sql := 'NULL';
|
||||||
|
ELSE
|
||||||
|
v_value_sql := 'HEXTORAW(' || quoted(v_value) || ')';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF col.data_type = 'CLOB' THEN
|
||||||
|
v_select_sql :=
|
||||||
|
'SELECT DBMS_LOB.GETLENGTH("' || col.column_name || '"), ' ||
|
||||||
|
'DBMS_LOB.SUBSTR("' || col.column_name || '", 32000, 1) ' ||
|
||||||
|
'FROM AD_COLUMN WHERE ID = :id';
|
||||||
|
EXECUTE IMMEDIATE v_select_sql
|
||||||
|
INTO v_lob_length, v_value
|
||||||
|
USING v_source_column_id;
|
||||||
|
IF v_lob_length > 32000 THEN
|
||||||
|
RAISE_APPLICATION_ERROR(
|
||||||
|
-20201,
|
||||||
|
'字段配置列AD_COLUMN.' || col.column_name || '超过32000字符,暂不支持导出'
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
IF v_value IS NULL THEN
|
||||||
|
v_value_sql := 'NULL';
|
||||||
|
ELSE
|
||||||
|
v_value_sql := 'TO_CLOB(' || quoted(v_value) || ')';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF col.data_type IN ('BLOB', 'LONG', 'LONG RAW') THEN
|
||||||
|
RAISE_APPLICATION_ERROR(
|
||||||
|
-20201,
|
||||||
|
'字段配置列AD_COLUMN.' || col.column_name ||
|
||||||
|
'的数据类型' || col.data_type || '暂不支持导出'
|
||||||
|
);
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
v_select_sql :=
|
||||||
|
'SELECT TO_CHAR("' || col.column_name || '") ' ||
|
||||||
|
'FROM AD_COLUMN WHERE ID = :id';
|
||||||
|
EXECUTE IMMEDIATE v_select_sql INTO v_value USING v_source_column_id;
|
||||||
|
v_value_sql := quoted(v_value);
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_insert_columns IS NULL THEN
|
||||||
|
v_insert_columns := col.column_name;
|
||||||
|
v_insert_values := v_value_sql;
|
||||||
|
ELSE
|
||||||
|
v_insert_columns := v_insert_columns || ', ' || col.column_name;
|
||||||
|
v_insert_values := v_insert_values || ', ' || v_value_sql;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF col.column_name NOT IN ('ID', 'AD_TABLE_ID') THEN
|
||||||
|
IF v_update_values IS NULL THEN
|
||||||
|
v_separator := '';
|
||||||
|
ELSE
|
||||||
|
v_separator := ', ';
|
||||||
|
END IF;
|
||||||
|
v_update_values := v_update_values || v_separator ||
|
||||||
|
col.column_name || ' = ' || v_value_sql;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
append_text('-- BOS单字段配置:' || v_table_name || '.' || v_column_name || CHR(10));
|
||||||
|
append_text('-- 来源:AD_COLUMN.ID=' || v_source_column_id || CHR(10));
|
||||||
|
append_text('-- 注意:本文件不包含 Oracle 物理字段 DDL。' || CHR(10));
|
||||||
|
append_text('DECLARE' || CHR(10));
|
||||||
|
append_text(' v_target_table_id NUMBER;' || CHR(10));
|
||||||
|
append_text(' v_target_column_id NUMBER;' || CHR(10));
|
||||||
|
append_text(' v_target_limit_group_id NUMBER;' || CHR(10));
|
||||||
|
append_text(' v_target_ref_column_id NUMBER;' || CHR(10));
|
||||||
|
append_text(' v_count NUMBER;' || CHR(10));
|
||||||
|
append_text('BEGIN' || CHR(10));
|
||||||
|
append_text(' SELECT id INTO v_target_table_id FROM ad_table' || CHR(10));
|
||||||
|
append_text(' WHERE UPPER(name) = ' || quoted(v_table_name) || ';' || CHR(10));
|
||||||
|
append_text(CHR(10));
|
||||||
|
|
||||||
|
IF v_source_limit_group_id IS NOT NULL THEN
|
||||||
|
append_text(' -- 限定值组按 NAME 匹配,只补充目标库缺失的数据。' || CHR(10));
|
||||||
|
append_text(' SELECT COUNT(*), MAX(id)' || CHR(10));
|
||||||
|
append_text(' INTO v_count, v_target_limit_group_id' || CHR(10));
|
||||||
|
append_text(' FROM ad_limitvalue_group' || CHR(10));
|
||||||
|
append_text(' WHERE UPPER(name) = UPPER(' || quoted(v_limit_group_name) || ');' || CHR(10));
|
||||||
|
append_text(' IF v_count > 1 THEN' || CHR(10));
|
||||||
|
append_text(' RAISE_APPLICATION_ERROR(-20201, ' ||
|
||||||
|
quoted('目标库存在多条同名限定值组:' || v_limit_group_name) || ');' || CHR(10));
|
||||||
|
append_text(' ELSIF v_count = 0 THEN' || CHR(10));
|
||||||
|
append_text(' v_target_limit_group_id := get_sequences(''AD_LIMITVALUE_GROUP'');' || CHR(10));
|
||||||
|
append_text(' INSERT INTO ad_limitvalue_group' || CHR(10));
|
||||||
|
append_text(' (id, name, comments, valuetype, creationdate, ownerid)' || CHR(10));
|
||||||
|
append_text(' VALUES' || CHR(10));
|
||||||
|
append_text(' (v_target_limit_group_id, ' || quoted(v_limit_group_name) || ', ' ||
|
||||||
|
quoted(v_limit_group_comments) || ', ' || quoted(v_limit_group_valuetype) ||
|
||||||
|
', SYSDATE, ' || number_literal(v_limit_group_ownerid) || ');' || CHR(10));
|
||||||
|
append_text(' END IF;' || CHR(10));
|
||||||
|
append_text(CHR(10));
|
||||||
|
|
||||||
|
FOR lv IN (
|
||||||
|
SELECT value, description, orderno, comments, valuetype, ownerid
|
||||||
|
FROM ad_limitvalue
|
||||||
|
WHERE ad_limitvalue_group_id = v_source_limit_group_id
|
||||||
|
ORDER BY orderno, id
|
||||||
|
) LOOP
|
||||||
|
append_text(' SELECT COUNT(*) INTO v_count' || CHR(10));
|
||||||
|
append_text(' FROM ad_limitvalue' || CHR(10));
|
||||||
|
append_text(' WHERE ad_limitvalue_group_id = v_target_limit_group_id' || CHR(10));
|
||||||
|
append_text(' AND (value = ' || quoted(lv.value) ||
|
||||||
|
' OR (value IS NULL AND ' || quoted(lv.value) || ' IS NULL));' || CHR(10));
|
||||||
|
append_text(' IF v_count = 0 THEN' || CHR(10));
|
||||||
|
append_text(' INSERT INTO ad_limitvalue' || CHR(10));
|
||||||
|
append_text(' (id, ad_limitvalue_group_id, value, description, orderno,' || CHR(10));
|
||||||
|
append_text(' comments, valuetype, creationdate, ownerid)' || CHR(10));
|
||||||
|
append_text(' VALUES' || CHR(10));
|
||||||
|
append_text(' (get_sequences(''AD_LIMITVALUE''), v_target_limit_group_id, ' ||
|
||||||
|
quoted(lv.value) || ', ' || quoted(lv.description) || ', ' ||
|
||||||
|
number_literal(lv.orderno) || ', ' || quoted(lv.comments) || ', ' ||
|
||||||
|
quoted(lv.valuetype) || ', SYSDATE, ' || number_literal(lv.ownerid) || ');' || CHR(10));
|
||||||
|
append_text(' END IF;' || CHR(10));
|
||||||
|
append_text(CHR(10));
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_source_ref_column_id IS NOT NULL THEN
|
||||||
|
append_text(' -- 引用元数据只校验,不自动创建引用字段。' || CHR(10));
|
||||||
|
append_text(' SELECT COUNT(*), MAX(c.id)' || CHR(10));
|
||||||
|
append_text(' INTO v_count, v_target_ref_column_id' || CHR(10));
|
||||||
|
append_text(' FROM ad_column c' || CHR(10));
|
||||||
|
append_text(' JOIN ad_table t ON t.id = c.ad_table_id' || CHR(10));
|
||||||
|
append_text(' WHERE UPPER(t.name) = ' || quoted(v_ref_table_name) || CHR(10));
|
||||||
|
append_text(' AND UPPER(c.dbname) = ' || quoted(v_ref_column_name) || ';' || CHR(10));
|
||||||
|
append_text(' IF v_count = 0 THEN' || CHR(10));
|
||||||
|
append_text(' RAISE_APPLICATION_ERROR(-20201, ' ||
|
||||||
|
quoted('目标库缺少引用字段:' || v_ref_table_name || '.' || v_ref_column_name) || ');' || CHR(10));
|
||||||
|
append_text(' ELSIF v_count > 1 THEN' || CHR(10));
|
||||||
|
append_text(' RAISE_APPLICATION_ERROR(-20201, ' ||
|
||||||
|
quoted('目标库引用字段不唯一:' || v_ref_table_name || '.' || v_ref_column_name) || ');' || CHR(10));
|
||||||
|
append_text(' END IF;' || CHR(10));
|
||||||
|
append_text(CHR(10));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
append_text(' SELECT COUNT(*), MAX(id)' || CHR(10));
|
||||||
|
append_text(' INTO v_count, v_target_column_id' || CHR(10));
|
||||||
|
append_text(' FROM ad_column' || CHR(10));
|
||||||
|
append_text(' WHERE ad_table_id = v_target_table_id' || CHR(10));
|
||||||
|
append_text(' AND UPPER(dbname) = ' || quoted(v_column_name) || ';' || CHR(10));
|
||||||
|
append_text(CHR(10));
|
||||||
|
append_text(' IF v_count > 1 THEN' || CHR(10));
|
||||||
|
append_text(' RAISE_APPLICATION_ERROR(-20201, ''目标库存在多条同名BOS字段配置'');' || CHR(10));
|
||||||
|
append_text(' ELSIF v_count = 0 THEN' || CHR(10));
|
||||||
|
append_text(' v_target_column_id := get_sequences(''AD_COLUMN'');' || CHR(10));
|
||||||
|
append_text(' INSERT INTO ad_column (' || v_insert_columns || ')' || CHR(10));
|
||||||
|
append_text(' VALUES (' || v_insert_values || ');' || CHR(10));
|
||||||
|
append_text(' ELSE' || CHR(10));
|
||||||
|
append_text(' UPDATE ad_column' || CHR(10));
|
||||||
|
append_text(' SET ' || v_update_values || CHR(10));
|
||||||
|
append_text(' WHERE id = v_target_column_id;' || CHR(10));
|
||||||
|
append_text(' END IF;' || CHR(10));
|
||||||
|
append_text(CHR(10));
|
||||||
|
append_text(' COMMIT;' || CHR(10));
|
||||||
|
append_text('END;' || CHR(10));
|
||||||
|
append_text('/' || CHR(10));
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF DBMS_LOB.ISTEMPORARY(r_sql) = 1 THEN
|
||||||
|
DBMS_LOB.FREETEMPORARY(r_sql);
|
||||||
|
END IF;
|
||||||
|
RAISE_APPLICATION_ERROR(
|
||||||
|
-20201,
|
||||||
|
'导出BOS字段配置失败[' || v_table_name || '.' || v_column_name || ']:' || SQLERRM
|
||||||
|
);
|
||||||
|
END hmig_get_table_bos_field_31;
|
||||||
|
/
|
||||||
Reference in New Issue
Block a user