Initial import of WPF migration tool
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Explicitly excluded legacy/deployment directories
|
||||||
|
/henlo_migration/
|
||||||
|
/henlo_migration_core10/
|
||||||
|
/产品部署/
|
||||||
|
|
||||||
|
# IDE and build outputs
|
||||||
|
**/.vs/
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
/henlo_migration_wpf10/publish/
|
||||||
|
/henlo_migration_wpf10/releases/
|
||||||
|
|
||||||
|
# Local database configuration and runtime data
|
||||||
|
/henlo_migration_wpf10/config.ini
|
||||||
|
/henlo_migration_wpf10/data/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# User-specific project settings
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|
||||||
|
# Historical one-off helpers and backup files
|
||||||
|
/henlo_migration_wpf10/_build.bat
|
||||||
|
/henlo_migration_wpf10/_build.ps1
|
||||||
|
/henlo_migration_wpf10/_debug_encoding.ps1
|
||||||
|
/henlo_migration_wpf10/_make_pack.bat
|
||||||
|
/henlo_migration_wpf10/_write_pack.ps1
|
||||||
|
/henlo_migration_wpf10/*.csproj.Backup*.tmp
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## 项目定位
|
||||||
|
|
||||||
|
恒诺迁移工具是一个仅运行于 Windows 的 Oracle 数据库迁移桌面工具。当前维护目标是 `henlo_migration_wpf10/`:WPF + .NET 10,程序集名为“恒诺迁移工具”。工具从源库读取表结构、BOS 配置、存储过程/函数和表数据,将 SQL 保存为本地文本;经用户确认后,再把本地 SQL 执行到目标库。
|
||||||
|
|
||||||
|
本文是仓库级开发约定。分析、修改和验证默认都以当前 WPF 项目为准,除非任务明确点名旧项目。
|
||||||
|
|
||||||
|
## 目录边界
|
||||||
|
|
||||||
|
- `henlo_migration_wpf10/`:当前唯一主程序,优先分析和修改。
|
||||||
|
- `henlo_migration/`:旧 .NET Framework 4.8 / WinForm 版本,已废弃;不要把旧版的窗体、AntdUI 或 AutoUpdater.NET 设计套到主程序。
|
||||||
|
- `henlo_migration_core10/`:未投入使用的 .NET Core 版本,默认不修改。
|
||||||
|
- `hc_preview/`:界面预览或辅助内容,不是主程序源码。
|
||||||
|
- `产品部署/`:部署相关资料或产物,只有发布任务才处理。
|
||||||
|
- `VELOPACK_MIGRATION.md`、`VELOPACK操作手册.md`:更新与发布背景资料;若与代码冲突,以当前代码和 `pack.bat` 为准。
|
||||||
|
|
||||||
|
## Git 仓库
|
||||||
|
|
||||||
|
- 项目远程仓库:`https://gitea.momozhua.site/qiangayi/henlo_migration_wpf10.git`
|
||||||
|
- 当前根目录已经初始化为 Git 工作区,默认分支为 `main`,远程名为 `origin`。
|
||||||
|
- 可以使用 `git status`、`git diff` 和提交历史检查改动。不要使用 `git reset --hard`、强制推送或其他会丢失本地/远程改动的命令。
|
||||||
|
- fetch、pull、commit、push 等会改变工作区、历史或远程状态的操作必须符合用户当前任务;上传远程仓库需要明确授权。
|
||||||
|
|
||||||
|
## 当前技术基线
|
||||||
|
|
||||||
|
- 项目文件:`henlo_migration_wpf10/henlo_migration_wpf10.csproj`
|
||||||
|
- 目标框架:`net10.0-windows`
|
||||||
|
- UI:WPF + HandyControl 3.5.1
|
||||||
|
- MVVM:手写 `INotifyPropertyChanged`/命令实现,引用 CommunityToolkit.Mvvm 8.3.2,但当前主 ViewModel 未使用其源生成器
|
||||||
|
- Oracle 访问:SqlSugar 5.1.4.157 + Oracle.ManagedDataAccess.Core 23.5.0
|
||||||
|
- 配置解析:ini-parser 2.5.2
|
||||||
|
- JSON:Newtonsoft.Json 13.0.3
|
||||||
|
- 自动更新:Velopack 0.0.1298
|
||||||
|
- 当前版本号:以 `.csproj` 的 `<Version>` 为唯一事实来源,不在本文硬编码版本
|
||||||
|
- Nullable:项目设置为 `<Nullable>disable</Nullable>`,源码中仍存在 `?` 注解,因此构建有既有 `CS8632` 警告
|
||||||
|
|
||||||
|
## 主程序结构
|
||||||
|
|
||||||
|
### 启动与界面
|
||||||
|
|
||||||
|
- `App.xaml`:应用资源、HandyControl 主题和 `MainViewModel` 资源注册;启动页是 `Views/HomeWindow.xaml`。
|
||||||
|
- `App.xaml.cs`:Velopack 初始化、后台/手动更新、更新前后 `data/` 与 `pre/` 备份恢复、全局异常日志。
|
||||||
|
- `Views/HomeWindow.xaml`:单窗口主界面,包含四个迁移 Tab、连接状态、SQL 编辑区和批量操作。
|
||||||
|
- `Views/HomeWindow.xaml.cs`:只处理窗口关闭和手动检查更新等窗口事件。
|
||||||
|
- `Views/ConnectionDialog.*`:源库/目标库连接配置与测试。
|
||||||
|
- `Views/ErrorDialog.*`:错误详情展示和复制。
|
||||||
|
- `Behaviors/ClickToCopyBehavior.cs`、`Converters/StatusColorConverter.cs`:界面辅助行为与状态颜色转换。
|
||||||
|
|
||||||
|
### 状态与编排
|
||||||
|
|
||||||
|
- `ViewModels/MainViewModel.cs`:主状态容器和命令编排;初始化四个 `TabServices`,管理数据库连接、单行/批量读写、SQL 编辑与断点续写。
|
||||||
|
- `Models/TableRowModel.cs`:每个迁移对象的名称、所属 Tab、读写状态。
|
||||||
|
- `Enums/`:Tab、状态、工作类型和输出扩展名枚举。
|
||||||
|
|
||||||
|
### 业务与数据访问
|
||||||
|
|
||||||
|
- `Services/TabConfig.cs` / `ITabConfig.cs`:将 Tab、索引文件、输出后缀和读取委托组合成配置。
|
||||||
|
- `Services/TabServices.cs`:索引列表加载/保存、单行读写、排序、删除和状态切换。
|
||||||
|
- `Services/DbServices.cs`:Oracle SQL 的提取与执行核心:
|
||||||
|
- `GetTableSql`:读取表 DDL、注释、主外键、唯一键和索引;
|
||||||
|
- `GetTableBos`:读取 BOS 配置;
|
||||||
|
- `GetPrcSql` / `GetAllPrc`:读取存储过程或函数源码;
|
||||||
|
- `GetTableData`:生成表数据 INSERT SQL;
|
||||||
|
- `ExecuteCreate`:按 `\n/` 分隔 SQL,在目标库逐条执行并检查 `USER_ERRORS`;
|
||||||
|
- `PreProcess`:在源库执行 `pre/` 下全部脚本。
|
||||||
|
- `Common/SqlSugarHelper.cs`:维护 `orig`(源库)和 `dest`(目标库)两个 SqlSugar 连接。
|
||||||
|
- `Common/SysConfig.cs`:读取和写入运行目录下的 `config.ini`。
|
||||||
|
- `Common/CommonHelper.cs`:运行目录、`data/`、`data/from/`、`pre/` 路径及文本读写。
|
||||||
|
|
||||||
|
## 真实数据流
|
||||||
|
|
||||||
|
四个 Tab 的映射由 `MainViewModel` 构造函数定义:
|
||||||
|
|
||||||
|
| Tab | 索引文件 | 内容文件 | 源库读取方法 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 表配置 | `data/from/table.txt` | `data/{对象名}_creat.txt` | `DbServices.GetTableSql` |
|
||||||
|
| BOS 配置 | `data/from/bos.txt` | `data/{对象名}_bos.txt` | `DbServices.GetTableBos` |
|
||||||
|
| 存储过程 | `data/from/prc.txt` | `data/{对象名}_prc.txt` | `DbServices.GetPrcSql` |
|
||||||
|
| 表数据 | `data/from/data.txt` | `data/{对象名}_data.txt` | `DbServices.GetTableData` |
|
||||||
|
|
||||||
|
读取流程:索引项 -> 调用源库读取委托 -> 生成 SQL 文本 -> 保存到 `data/` -> 更新读取状态并显示内容。
|
||||||
|
|
||||||
|
写入流程:索引项 -> 读取对应 `data/*_{后缀}.txt` -> `DbServices.ExecuteCreate` -> 在目标库执行 -> 更新写入状态。批量写入遇到第一条失败即停止,并在内存中记录断点;断点不会跨进程持久化。
|
||||||
|
|
||||||
|
删除列表项会同时删除对应内容文件。调整顺序会立即覆盖索引文件。新增对象名会被转为小写,数据库读取时再按需要转大写。
|
||||||
|
|
||||||
|
## 配置和运行时文件
|
||||||
|
|
||||||
|
- `config.ini` 保存源库和目标库的 IP/服务名、用户名、密码。当前实现是明文读写,不要把注释或 UI 文案误当成加密能力。
|
||||||
|
- 不要在日志、回答、补丁或测试输出中展示真实连接串和密码。
|
||||||
|
- 修改或发布时不要用样例配置覆盖用户现有 `config.ini`。
|
||||||
|
- `data/` 和 `pre/` 是用户可编辑、更新时需要保留的运行数据;不要无故清空、批量改名或删除。
|
||||||
|
- `error.log` 是全局异常日志,`execute_log.txt` 记录目标库执行的 SQL;两者都是运行时文件,排查时可能含敏感业务信息。
|
||||||
|
- `bin/`、`obj/`、`publish/`、`releases/`、`.vs/` 是构建、IDE 或发布产物。不要手工修改其中的 DLL、EXE、PDB、NuGet 包或生成源码。
|
||||||
|
- `*.csproj.Backup*.tmp`、`_build*`、`_make_pack.bat`、`_write_pack.ps1`、`_debug_encoding.ps1` 是历史/辅助文件;正常构建和发布不要以它们为权威入口。
|
||||||
|
|
||||||
|
## 开发约定
|
||||||
|
|
||||||
|
- 保持现有命名空间 `henlo_migration` 和现有目录分层。
|
||||||
|
- UI 状态、命令和业务编排放在 `MainViewModel`;可绑定的属性变更必须触发 `OnPropertyChanged`,命令可用性变化时同步触发 `RaiseCanExecuteChanged`。
|
||||||
|
- XAML 负责布局、样式和绑定;code-behind 仅保留必须依赖 Window/控件事件的逻辑。
|
||||||
|
- 通用 Oracle 提取/执行逻辑放在 `DbServices`,不要在 View 或 ViewModel 中复制 SQL。
|
||||||
|
- 新增 Tab 能力时同时检查 `TabsEnum`、`CodeExt`、`TabConfig`、`MainViewModel` 映射、XAML 绑定、索引文件复制规则和更新备份范围。
|
||||||
|
- 耗时数据库调用不得阻塞 UI 线程;沿用现有异步命令和 `Task.Run` 边界,UI 更新通过 WPF Dispatcher 或正常 await 回到 UI 上下文。
|
||||||
|
- 保持 SQL 文件的 Oracle 分隔约定:`DbServices.SplitStatements` 依赖换行后的 `/`(实际分隔符为 `\n/`)。修改生成 SQL 时必须验证普通 DDL、PL/SQL 块和末尾分隔符。
|
||||||
|
- 数据库对象名和用户数据不能直接拼入任意 SQL。查询参数优先使用 `SugarParameter`;确需生成 DDL 时必须处理标识符和字符串转义。
|
||||||
|
- 不要静默吞掉新异常。UI 可展示友好提示,但排障信息应保留在错误对话框或日志中;修改现有空 `catch` 时要评估用户体验。
|
||||||
|
- 不做与任务无关的大规模格式化、Nullable 改造、依赖升级或旧项目同步。
|
||||||
|
|
||||||
|
## 数据库安全边界
|
||||||
|
|
||||||
|
源库和目标库不是等价环境:
|
||||||
|
|
||||||
|
- `Get*` 方法通常查询源库;`ExecuteCreate` 明确写目标库。
|
||||||
|
- `PreProcess` 会在源库执行 `pre/` 下的 SQL/PLSQL,虽然名为预处理,但不是只读操作。
|
||||||
|
- 连接按钮会把界面中的连接信息写回 `config.ini`,并重建同时包含源库和目标库的 SqlSugarScope。
|
||||||
|
- 界面的“断开”当前只更新连接状态,并未释放或关闭底层连接对象。
|
||||||
|
|
||||||
|
除非用户明确要求并确认数据库、对象和影响范围,否则不要连接真实数据库、执行预处理、执行写入、清理数据或测试破坏性 SQL。需要验证数据库写入逻辑时优先使用代码审查、可控测试库或事务化的最小复现。真实目标库写入前必须提醒备份并确认权限。
|
||||||
|
|
||||||
|
## 构建与验证
|
||||||
|
|
||||||
|
在仓库根目录运行:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
当前没有测试项目。最低验证要求:
|
||||||
|
|
||||||
|
1. 所有 C#/XAML 修改至少执行一次主项目构建。
|
||||||
|
2. 仅文档修改可不构建,但要重新读取文件检查 Markdown、路径和命令。
|
||||||
|
3. XAML/绑定修改除构建外,应在 Windows 上启动应用,检查四个 Tab、连接弹窗、按钮可用性和错误弹窗;不要因此连接生产库。
|
||||||
|
4. SQL 生成/拆分修改需用不含真实数据的代表性 SQL 验证表 DDL、注释、约束、PL/SQL 和 INSERT。
|
||||||
|
5. 更新逻辑修改需分别考虑普通 `dotnet run`(非 Velopack 安装)和 Velopack 安装态。
|
||||||
|
|
||||||
|
既有 `CS8632` 警告不是本次修改自动引入的错误,但不得新增编译错误。若任务专门处理 Nullable,应统一决定启用 Nullable 还是移除注解,不要零散压制警告。
|
||||||
|
|
||||||
|
本地调试可运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project .\henlo_migration_wpf10\henlo_migration_wpf10.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
启动会读取或创建运行目录下的配置,并可能尝试初始化更新检查;不适合无界面的 CI 验证。
|
||||||
|
|
||||||
|
## 版本与发布
|
||||||
|
|
||||||
|
- 版本号只修改 `.csproj` 中的 `<Version>`,必须单调递增;显示版本来自程序集元数据。
|
||||||
|
- 正式发布入口是 `henlo_migration_wpf10/pack.bat`。脚本会清理 `publish/`、执行 `dotnet publish -c Release --self-contained false -r win-x64`,再调用 `vpk pack --noInst` 输出到 `releases/`。
|
||||||
|
- `pack.bat` 依赖本机 `dotnet`、`vpk` 和 Windows 批处理环境;不要把“构建通过”等同于“打包通过”。
|
||||||
|
- 发布是显式任务。未经要求不要运行打包、删除发布目录、修改版本号或上传更新服务器。
|
||||||
|
- 打包后核对便携 ZIP、full/delta nupkg、`RELEASES`/JSON 元数据以及实际版本号。上传服务器属于外部写操作,必须另行获得明确授权。
|
||||||
|
- 若修改运行时必需文件,检查 `.csproj` 的 `CopyToOutputDirectory` 规则和 `App.xaml.cs` 的更新备份/恢复范围。
|
||||||
|
|
||||||
|
## 完成任务时的交付说明
|
||||||
|
|
||||||
|
简要说明:修改了什么、影响哪一层、执行了哪些验证、构建是否有错误/警告、是否因缺少 Oracle 测试库或 Velopack 安装态而存在未验证项。不要声称已验证真实数据库迁移,除非确实在明确授权的测试环境完成。
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Velopack 迁移总结
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
将自动更新从 AutoUpdater.NET 迁移到 Velopack,解决 .NET 8 ZipExtractor 依赖问题。
|
||||||
|
|
||||||
|
## 根本原因
|
||||||
|
AutoUpdater.NET 1.9.0 内部嵌入了 ZipExtractor.exe(.NET 8 WinForms 程序),用户机器只有 .NET 10 时无法运行。
|
||||||
|
|
||||||
|
## 修改文件
|
||||||
|
|
||||||
|
### 1. `henlo_migration_wpf10.csproj`
|
||||||
|
- 移除: `Autoupdater.NET.Official` 1.9.0
|
||||||
|
- 添加: `Velopack` 0.0.1298
|
||||||
|
|
||||||
|
### 2. `App.xaml.cs`
|
||||||
|
- 移除所有 AutoUpdater 相关代码
|
||||||
|
- 添加 Velopack 初始化: `VelopackApp.Build().Run()`
|
||||||
|
- 添加 `UpdateManager` 用于检查/下载/应用更新
|
||||||
|
- 后台静默检查更新
|
||||||
|
- 手动检查更新支持弹窗确认
|
||||||
|
|
||||||
|
### 3. `Views/HomeWindow.xaml.cs`
|
||||||
|
- `CheckUpdate_Click` 改为 async void,调用 `CheckUpdateManualAsync`
|
||||||
|
|
||||||
|
### 4. `pack.bat` (新增)
|
||||||
|
Velopack 打包脚本,用法:
|
||||||
|
```batch
|
||||||
|
pack.bat 1.0.002
|
||||||
|
```
|
||||||
|
|
||||||
|
## Velopack 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
开发机:
|
||||||
|
dotnet publish → vpk pack → releases/
|
||||||
|
├── setup.exe (安装程序)
|
||||||
|
├── update.nupkg (更新包)
|
||||||
|
└── RELEASES (更新索引)
|
||||||
|
|
||||||
|
服务器:
|
||||||
|
上传 releases/ 到 http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/
|
||||||
|
|
||||||
|
客户端:
|
||||||
|
启动 → VelopackApp.Build().Run()
|
||||||
|
→ UpdateManager.CheckForUpdatesAsync()
|
||||||
|
→ 有新版本 → DownloadUpdatesAsync()
|
||||||
|
→ 提示用户重启 → ApplyUpdatesAndRestart()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键 API
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 初始化(必须在启动时调用)
|
||||||
|
VelopackApp.Build().Run();
|
||||||
|
|
||||||
|
// 检查更新
|
||||||
|
var mgr = new UpdateManager("https://.../updates");
|
||||||
|
var newVersion = await mgr.CheckForUpdatesAsync();
|
||||||
|
|
||||||
|
// 下载更新
|
||||||
|
await mgr.DownloadUpdatesAsync(newVersion);
|
||||||
|
|
||||||
|
// 应用并重启
|
||||||
|
mgr.ApplyUpdatesAndRestart(newVersion);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 打包命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 发布应用
|
||||||
|
dotnet publish -c Release -o ./publish --self-contained false -r win-x64
|
||||||
|
|
||||||
|
# 2. Velopack 打包
|
||||||
|
vpk pack -u "henlo_migration" -v "1.0.002" -p "./publish" -e "恒诺迁移工具.exe" -o "./releases"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 服务器部署
|
||||||
|
|
||||||
|
将 `releases/` 目录下的文件上传到:
|
||||||
|
```
|
||||||
|
http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/
|
||||||
|
```
|
||||||
|
|
||||||
|
Velopack 会自动读取该目录下的 `RELEASES` 文件检查更新。
|
||||||
|
|
||||||
|
## 优势
|
||||||
|
|
||||||
|
1. **Rust 核心** → Updater 完全不需要 .NET Runtime
|
||||||
|
2. **Delta 更新** → 只下载变更文件
|
||||||
|
3. **自动回滚** → 更新失败自动恢复旧版本
|
||||||
|
4. **后台静默更新** → 下次启动时自动应用
|
||||||
|
5. **经过大规模验证** → osu!、STranslate 等大牌在用
|
||||||
|
|
||||||
|
## 待办
|
||||||
|
|
||||||
|
- [ ] 测试打包流程
|
||||||
|
- [ ] 配置服务器更新目录
|
||||||
|
- [ ] 测试完整更新流程
|
||||||
|
- [ ] 更新 CI/CD 脚本(如有)
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
# Velopack 操作手册 — 恒诺迁移工具(便携模式)
|
||||||
|
|
||||||
|
## 一、Velopack 是什么
|
||||||
|
|
||||||
|
Velopack 是一个**自动更新框架**,核心功能:
|
||||||
|
1. **打包** — 把程序编译输出打包成更新包(.nupkg)
|
||||||
|
2. **自动更新** — 程序启动时自动检查服务器,发现新版本就下载并提示用户重启
|
||||||
|
|
||||||
|
**核心优势**:更新器用 Rust 编写,不需要 .NET Runtime,彻底解决 AutoUpdater.NET 的 .NET 8 依赖问题。
|
||||||
|
|
||||||
|
**本模式**:便携模式(`--noInst`),不生成安装包,用户直接解压 ZIP 使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、项目结构(打包相关)
|
||||||
|
|
||||||
|
```
|
||||||
|
henlo_migration_wpf10/
|
||||||
|
├── henlo_migration_wpf10.csproj # 项目文件(已添加 Velopack 包)
|
||||||
|
│ └── <Version>1.0.2</Version> # 【版本号唯一来源】
|
||||||
|
├── App.xaml.cs # 启动时初始化 Velopack
|
||||||
|
├── Views/HomeWindow.xaml.cs # "检查更新"按钮逻辑
|
||||||
|
├── publish/ # 【发布输出目录】dotnet publish 生成
|
||||||
|
├── releases/ # 【打包输出目录】vpk pack 生成
|
||||||
|
│ ├── henlo_migration-1.0.2-full.nupkg # 更新包(上传到服务器)
|
||||||
|
│ ├── henlo_migration-win-Portable.zip # 便携版(给用户第一次使用)
|
||||||
|
│ ├── RELEASES # 更新索引文件(上传到服务器)
|
||||||
|
│ ├── assets.win.json # 资源清单
|
||||||
|
│ └── releases.win.json # 发布配置
|
||||||
|
└── pack.bat # 一键打包脚本(已创建)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、版本号管理(重要)
|
||||||
|
|
||||||
|
### 唯一来源:csproj 文件
|
||||||
|
|
||||||
|
**版本号只存在于一个地方**:`henlo_migration_wpf10.csproj` 中的 `<Version>` 标签
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<PropertyGroup>
|
||||||
|
<Version>1.0.2</Version> <!-- 唯一需要修改的地方 -->
|
||||||
|
</PropertyGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 版本号格式(SemVer2)
|
||||||
|
|
||||||
|
```
|
||||||
|
主版本.次版本.补丁
|
||||||
|
```
|
||||||
|
|
||||||
|
| 格式 | 示例 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| ✅ 正确 | `1.0.2`、`1.1.0`、`2.0.0` | 三段数字,无前导零 |
|
||||||
|
| ❌ 错误 | `1.0.02`、`1.0.002` | 不能前导零 |
|
||||||
|
| ❌ 错误 | `1.0` | 必须是三段 |
|
||||||
|
|
||||||
|
### 版本号递增规则
|
||||||
|
|
||||||
|
| 位置 | 什么时候改 | 示例 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| 补丁(第3位) | Bug 修复 | `1.0.1` → `1.0.2` |
|
||||||
|
| 次版本(第2位) | 新增功能 | `1.0.2` → `1.1.0` |
|
||||||
|
| 主版本(第1位) | 重大更新/不兼容 | `1.1.0` → `2.0.0` |
|
||||||
|
|
||||||
|
**重要**:版本号必须递增,不能回退(Velopack 不允许降级)。
|
||||||
|
|
||||||
|
### 版本号同步机制
|
||||||
|
|
||||||
|
```
|
||||||
|
csproj <Version> 标签
|
||||||
|
│
|
||||||
|
├──→ 程序集版本(编译时自动嵌入 DLL)
|
||||||
|
├──→ 程序运行时显示(SysConfig.version 从程序集读取)
|
||||||
|
└──→ Velopack 打包版本(pack.bat 自动读取)
|
||||||
|
```
|
||||||
|
|
||||||
|
**只需要改一处**,其他地方全部自动同步。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、发布流程(完整步骤)
|
||||||
|
|
||||||
|
### 步骤 1:修改版本号
|
||||||
|
|
||||||
|
打开 `henlo_migration_wpf10.csproj`,修改 `<Version>`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- 修改前 -->
|
||||||
|
<Version>1.0.1</Version>
|
||||||
|
|
||||||
|
<!-- 修改后 -->
|
||||||
|
<Version>1.0.2</Version>
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
- 只改这个数字,不要改其他地方
|
||||||
|
- 确保新版本号比旧版本大
|
||||||
|
- 保存文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 步骤 2:编译发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入项目目录
|
||||||
|
cd D:\work\恒诺\长期支持\恒诺迁移工具\henlo_migration_wpf10
|
||||||
|
|
||||||
|
# 发布(Release 模式,输出到 publish/ 目录)
|
||||||
|
dotnet publish henlo_migration_wpf10.csproj -c Release -o publish --self-contained false -r win-x64
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明**:
|
||||||
|
| 参数 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| `-c Release` | Release 模式(优化过的,体积小) |
|
||||||
|
| `-o publish` | 输出到 `publish/` 目录 |
|
||||||
|
| `--self-contained false` | 不包含 .NET Runtime(假设用户已装 .NET 10) |
|
||||||
|
| `-r win-x64` | 目标平台:Windows x64 |
|
||||||
|
|
||||||
|
**输出**:`publish/` 目录下生成完整的程序文件(.exe、.dll 等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 步骤 3:Velopack 打包(便携模式)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 打包(使用 vpk 命令行工具,--noInst 表示不生成安装包)
|
||||||
|
vpk pack -u henlo_migration -v 1.0.2 -p publish -e 恒诺迁移工具.exe -o releases --noInst
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明**:
|
||||||
|
| 参数 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| `-u henlo_migration` | 应用唯一标识符(URL 友好的名字) |
|
||||||
|
| `-v 1.0.2` | 版本号(必须是 SemVer2 格式) |
|
||||||
|
| `-p publish` | 输入目录(步骤 2 的输出) |
|
||||||
|
| `-e 恒诺迁移工具.exe` | 程序入口 EXE 文件名 |
|
||||||
|
| `-o releases` | 输出目录(打包结果放这里) |
|
||||||
|
| `--noInst` | **不生成安装包**(便携模式) |
|
||||||
|
|
||||||
|
**输出**:`releases/` 目录下生成 5 个文件(无 Setup.exe)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 步骤 4:上传到服务器
|
||||||
|
|
||||||
|
把以下文件上传到更新服务器:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/
|
||||||
|
├── henlo_migration-1.0.2-full.nupkg ← 必须(更新包)
|
||||||
|
└── RELEASES ← 必须(更新索引)
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
- 每次发布新版本,上传新的 `.nupkg` 文件即可
|
||||||
|
- `RELEASES` 文件会被 Velopack 自动更新(包含所有版本列表)
|
||||||
|
- 不需要上传 `Portable.zip` 到服务器(那是给用户第一次下载用的)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、一键打包脚本
|
||||||
|
|
||||||
|
已创建 `pack.bat`,用法:
|
||||||
|
|
||||||
|
```batch
|
||||||
|
# 直接运行,版本号自动从 csproj 读取
|
||||||
|
pack.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
**脚本内容**:
|
||||||
|
```batch
|
||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
REM Velopack pack script (portable mode, no installer)
|
||||||
|
REM Reads <Version> from csproj automatically
|
||||||
|
|
||||||
|
set "PROJECT_DIR=%~dp0"
|
||||||
|
set "PUBLISH_DIR=%PROJECT_DIR%publish"
|
||||||
|
set "RELEASE_DIR=%PROJECT_DIR%releases"
|
||||||
|
set "CSPROJ=%PROJECT_DIR%henlo_migration_wpf10.csproj"
|
||||||
|
|
||||||
|
REM Read <Version> from csproj via PowerShell (handles UTF-8)
|
||||||
|
for /f "usebackq delims=" %%a in (`powershell -NoProfile -Command "[regex]::Match(([System.IO.File]::ReadAllText('%CSPROJ%', [System.Text.Encoding]::UTF8)), '<Version>(.+?)</Version>').Groups[1].Value"`) do set "VERSION=%%a"
|
||||||
|
|
||||||
|
if "!VERSION!"=="" (
|
||||||
|
echo ERROR: Cannot read version from csproj
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Henlo Migration Tool - Velopack Pack
|
||||||
|
echo Version: !VERSION!
|
||||||
|
echo ========================================
|
||||||
|
|
||||||
|
echo [1/3] Cleanup...
|
||||||
|
if exist "!PUBLISH_DIR!" rmdir /s /q "!PUBLISH_DIR!"
|
||||||
|
if exist "!RELEASE_DIR!" rmdir /s /q "!RELEASE_DIR!"
|
||||||
|
|
||||||
|
echo [2/3] dotnet publish...
|
||||||
|
dotnet publish "%CSPROJ%" -c Release -o "!PUBLISH_DIR!" --self-contained false -r win-x64
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: dotnet publish failed!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [3/3] vpk pack (--noInst)...
|
||||||
|
vpk pack -u "henlo_migration" -v "!VERSION!" -p "!PUBLISH_DIR!" -e "恒诺迁移工具.exe" -o "!RELEASE_DIR!" --noInst
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: vpk pack failed!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Pack complete!
|
||||||
|
echo Output:
|
||||||
|
echo henlo_migration-win-Portable.zip
|
||||||
|
echo henlo_migration-!VERSION!-full.nupkg
|
||||||
|
echo RELEASES
|
||||||
|
echo Server deploy to:
|
||||||
|
echo http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/
|
||||||
|
echo ========================================
|
||||||
|
|
||||||
|
endlocal
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五点五、重要:中文路径编码问题
|
||||||
|
|
||||||
|
### 问题描述
|
||||||
|
|
||||||
|
项目路径包含中文字符(如 `D:\work\恒诺\...`),在某些场景下会导致编码问题:
|
||||||
|
|
||||||
|
| 工具/场景 | 是否受影响 | 说明 |
|
||||||
|
|----------|----------|------|
|
||||||
|
| **pack.bat 双击运行** | ❌ 正常 | CMD 直接执行,编码正确 |
|
||||||
|
| **手动执行 PowerShell** | ❌ 正常 | PowerShell 能正确处理 UTF-8 |
|
||||||
|
| **通过 AI Agent exec 工具** | ⚠️ **受影响** | 中文路径可能被损坏,导致命令执行失败 |
|
||||||
|
|
||||||
|
### 问题表现
|
||||||
|
|
||||||
|
当 exec 工具执行包含中文路径的命令时,可能出现:
|
||||||
|
```
|
||||||
|
# 预期命令
|
||||||
|
dir /b "D:\work\恒诺\长期支持\..."
|
||||||
|
|
||||||
|
# 实际执行的命令(路径被截断)
|
||||||
|
dir /b "D:\b"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
|
||||||
|
如果需要通过 AI Agent 执行涉及中文路径的操作,使用以下策略:
|
||||||
|
|
||||||
|
1. **写入脚本文件再执行**
|
||||||
|
```
|
||||||
|
# 不要直接执行带中文路径的命令
|
||||||
|
❌ exec: dir "D:\work\恒诺\..."
|
||||||
|
|
||||||
|
# 应先写脚本文件,再执行简单路径
|
||||||
|
✅ write: 将 Node.js/Python 脚本写入 _helper.js/_helper.py
|
||||||
|
✅ exec: node "D:\work\恒诺\...\_helper.js" # 路径简单时可行
|
||||||
|
# 或使用英文路径的项目目录
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **使用相对路径**(如果在项目目录内操作)
|
||||||
|
```
|
||||||
|
cd /d "D:\work\恒诺\长期支持\恒诺迁移工具\henlo_migration_wpf10"
|
||||||
|
dir /b publish # 使用相对路径避免中文
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **创建符号链接**(临时方案)
|
||||||
|
```cmd
|
||||||
|
mklink /D C:\henlo "D:\work\恒诺\长期支持\恒诺迁移工具"
|
||||||
|
# 之后操作使用 C:\henlo\... 路径
|
||||||
|
```
|
||||||
|
|
||||||
|
### 最佳实践
|
||||||
|
|
||||||
|
- **打包操作**:直接在项目目录双击 `pack.bat` 运行,不要通过 AI Agent 执行
|
||||||
|
- **文件操作**:AI Agent 使用 `write`/`read` 工具(这些工具能正确处理中文路径)
|
||||||
|
- **命令执行**:尽量使用不含中文字符的路径,或先写脚本再执行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、打包目录说明
|
||||||
|
|
||||||
|
### 输入目录(`-p` 参数)
|
||||||
|
|
||||||
|
```
|
||||||
|
publish/ ← 你指定的输入目录
|
||||||
|
├── 恒诺迁移工具.exe ← 入口程序(-e 参数指定)
|
||||||
|
├── 恒诺迁移工具.dll
|
||||||
|
├── 各种依赖 DLL
|
||||||
|
├── HandyControl.dll
|
||||||
|
├── Velopack.dll
|
||||||
|
└── ...其他文件
|
||||||
|
```
|
||||||
|
|
||||||
|
**来源**:`dotnet publish` 的输出
|
||||||
|
|
||||||
|
### 输出目录(`-o` 参数)
|
||||||
|
|
||||||
|
```
|
||||||
|
releases/ ← 打包结果
|
||||||
|
├── henlo_migration-1.0.2-full.nupkg # 完整更新包(上传服务器)
|
||||||
|
├── henlo_migration-win-Portable.zip # 便携版(给用户第一次用)
|
||||||
|
├── RELEASES # 版本索引(上传服务器)
|
||||||
|
├── assets.win.json # 资源清单
|
||||||
|
└── releases.win.json # 发布配置
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:没有 `Setup.exe`(因为用了 `--noInst`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、用户侧体验
|
||||||
|
|
||||||
|
### 第一次使用(新用户)
|
||||||
|
|
||||||
|
1. 从服务器下载 `henlo_migration-win-Portable.zip`
|
||||||
|
2. 解压到任意目录(如 `D:\恒诺迁移工具\`)
|
||||||
|
3. 双击 `恒诺迁移工具.exe` 运行
|
||||||
|
|
||||||
|
### 以后更新(自动)
|
||||||
|
|
||||||
|
1. 打开程序(旧版本)
|
||||||
|
2. Velopack 自动检查服务器 `RELEASES` 文件
|
||||||
|
3. 发现新版本 → 后台下载 `.nupkg` 更新包
|
||||||
|
4. 弹出提示:"发现新版本 1.0.2,是否重启更新?"
|
||||||
|
5. 用户点击"是" → 程序关闭 → 自动安装更新 → 重启程序
|
||||||
|
6. 打开后就是新版本
|
||||||
|
|
||||||
|
### 手动检查更新
|
||||||
|
|
||||||
|
用户也可以点击菜单「帮助」→「检查更新」手动触发。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、服务器配置
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/www/henlo_migration/auto_update/ # Nginx 根目录
|
||||||
|
├── henlo_migration-1.0.0-full.nupkg # 旧版本(保留)
|
||||||
|
├── henlo_migration-1.0.1-full.nupkg # 旧版本(保留)
|
||||||
|
├── henlo_migration-1.0.2-full.nupkg # 当前版本
|
||||||
|
└── RELEASES # 版本索引(自动维护)
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:不需要放 `Setup.exe` 和 `Portable.zip` 在服务器上。
|
||||||
|
|
||||||
|
### RELEASES 文件示例
|
||||||
|
|
||||||
|
```
|
||||||
|
9467F3A6D8E1B2C henlo_migration-1.0.0-full.nupkg 5915018
|
||||||
|
A7B8C9D0E1F2A3B henlo_migration-1.0.1-full.nupkg 5915018
|
||||||
|
C3D4E5F6A7B8C9D henlo_migration-1.0.2-full.nupkg 5915018
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:不要手动修改 `RELEASES` 文件,Velopack 会自动维护。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、常见问题
|
||||||
|
|
||||||
|
### Q1:打包时提示 "VelopackApp.Run() was found in OnStartup"
|
||||||
|
|
||||||
|
**原因**:Velopack 建议把初始化代码放在 `Main()` 方法开头,而不是 WPF 的 `OnStartup`。
|
||||||
|
|
||||||
|
**解决**:可以忽略(当前位置也能工作),或者按提示移到 `Main()` 里。
|
||||||
|
|
||||||
|
### Q2:用户无法自动更新
|
||||||
|
|
||||||
|
**检查清单**:
|
||||||
|
1. 服务器 URL 是否正确?(`App.xaml.cs` 中的 `UpdateUrl`)
|
||||||
|
2. `RELEASES` 文件是否在服务器上?
|
||||||
|
3. `.nupkg` 文件是否上传到服务器?
|
||||||
|
4. 防火墙是否允许访问服务器?
|
||||||
|
5. 版本号是否递增?(Velopack 不允许降级)
|
||||||
|
|
||||||
|
### Q3:如何回滚版本?
|
||||||
|
|
||||||
|
Velopack **不支持回滚**。如果新版本有问题:
|
||||||
|
1. 修复 bug
|
||||||
|
2. 发布更高版本号(如 1.0.3)
|
||||||
|
3. 用户更新到 1.0.3
|
||||||
|
|
||||||
|
### Q4:用户第一次如何获取程序?
|
||||||
|
|
||||||
|
把 `henlo_migration-win-Portable.zip` 发给用户:
|
||||||
|
- 通过 QQ、微信、邮件发送
|
||||||
|
- 或放在网盘/服务器上下载
|
||||||
|
- 用户下载后解压即可使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、发布检查清单
|
||||||
|
|
||||||
|
每次发布前检查:
|
||||||
|
|
||||||
|
- [ ] 代码已提交 Git
|
||||||
|
- [ ] **版本号已更新**(只改 `csproj` 里的 `<Version>`)
|
||||||
|
- [ ] `dotnet publish` 成功
|
||||||
|
- [ ] `vpk pack --noInst` 成功(或 `pack.bat` 成功)
|
||||||
|
- [ ] 测试程序能正常启动
|
||||||
|
- [ ] 上传 `.nupkg` 和 `RELEASES` 到服务器
|
||||||
|
- [ ] 测试自动更新流程
|
||||||
|
- [ ] 把 `Portable.zip` 发给需要的新用户
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、命令速查表
|
||||||
|
|
||||||
|
| 操作 | 命令 |
|
||||||
|
|------|------|
|
||||||
|
| 修改版本号 | 编辑 `henlo_migration_wpf10.csproj` 中的 `<Version>` |
|
||||||
|
| 编译发布 | `dotnet publish -c Release -o publish --self-contained false -r win-x64` |
|
||||||
|
| Velopack 打包(便携模式) | `vpk pack -u henlo_migration -v 1.0.2 -p publish -e 恒诺迁移工具.exe -o releases --noInst` |
|
||||||
|
| 一键打包 | `pack.bat`(版本号自动从 csproj 读取) |
|
||||||
|
| 安装 vpk 工具 | `dotnet tool install -g vpk` |
|
||||||
|
| 更新 vpk 工具 | `dotnet tool update -g vpk` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档版本:2026-04-22(便携模式)*
|
||||||
|
*适用项目:恒诺迁移工具 WPF*
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
# 恒诺迁移工具 - Velopack 自动更新修复 (2026-04-22)
|
||||||
|
|
||||||
|
## 问题
|
||||||
|
|
||||||
|
error.log 报错:
|
||||||
|
```
|
||||||
|
[AutoUpdate] This operation can not be performed in an application that is not installed.
|
||||||
|
at Velopack.UpdateManager.EnsureInstalled()
|
||||||
|
at Velopack.UpdateManager.CheckForUpdatesAsync()
|
||||||
|
```
|
||||||
|
|
||||||
|
**根本原因**:`UpdateManager` 内部调用 `EnsureInstalled()` 检查应用是否通过 Velopack 安装,
|
||||||
|
直接双击 exe 运行(未安装)时抛出异常导致启动失败。
|
||||||
|
|
||||||
|
## 修复内容
|
||||||
|
|
||||||
|
修改 `App.xaml.cs`:
|
||||||
|
|
||||||
|
1. **Velopack 初始化兜底**:用 `try/catch` 包裹 `WindowsVelopackLocator` 构造,
|
||||||
|
捕获 "not installed" 相关异常后将 `_updateManager = null`
|
||||||
|
2. **`_updateManager == null` 时跳过后台检查**:`CheckUpdateInBackgroundAsync()` 开头加 null 检查
|
||||||
|
3. **手动检查时友好提示**:`CheckUpdateManualAsync()` 检测到 null 时弹窗提示"不支持自动更新"
|
||||||
|
4. **修复 API 调用**:补全 `WindowsVelopackLocator` 缺少的第3个参数(`IVelopackLogger?` 传 null)
|
||||||
|
5. **添加 `using System.Diagnostics`**:修复 `Process.GetCurrentProcess()` 找不到的问题
|
||||||
|
|
||||||
|
## 编译结果
|
||||||
|
|
||||||
|
✅ Release 编译成功,0 错误,80 个警告(均为 CS8632 nullable / NU1701 framework 兼容警告,不影响运行)
|
||||||
|
|
||||||
|
## 修复后行为
|
||||||
|
|
||||||
|
| 场景 | 行为 |
|
||||||
|
|------|------|
|
||||||
|
| 直接双击 exe(未安装) | `_updateManager = null`,跳过更新,静默启动 |
|
||||||
|
| 手动点击"检查更新" | 弹窗提示"不支持自动更新" |
|
||||||
|
| 通过 Velopack 安装 | 正常检查更新 |
|
||||||
|
|
||||||
|
## 待验证
|
||||||
|
|
||||||
|
- [ ] 重新 `dotnet publish` 输出到 publish/ 目录
|
||||||
|
- [ ] 在目标机器(无 .NET Runtime)上验证能否正常启动
|
||||||
|
- [ ] 测试 Velopack 正式安装后的自动更新流程
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<Application x:Class="henlo_migration.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:henlo_migration.ViewModels"
|
||||||
|
xmlns:b="clr-namespace:henlo_migration.Behaviors"
|
||||||
|
xmlns:converters="clr-namespace:henlo_migration.Converters"
|
||||||
|
StartupUri="Views/HomeWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/>
|
||||||
|
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
<vm:MainViewModel x:Key="MainVM"/>
|
||||||
|
<BooleanToVisibilityConverter x:Key="Boolean2VisibilityConverter"/>
|
||||||
|
<converters:ReadStatusColorConverter x:Key="ReadStatusColorConverter"/>
|
||||||
|
<converters:WriteStatusColorConverter x:Key="WriteStatusColorConverter"/>
|
||||||
|
<Style x:Key="CopyableName" TargetType="TextBlock">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="Margin" Value="4,0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Foreground" Value="#0969da"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="TextDecorations" Value="Underline"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
<!-- 单个状态图标:圆角矩形徽章,高度24 -->
|
||||||
|
<Style x:Key="StatusBadge" TargetType="Border">
|
||||||
|
<Setter Property="CornerRadius" Value="3"/>
|
||||||
|
<Setter Property="Padding" Value="6,0"/>
|
||||||
|
<Setter Property="Height" Value="24"/>
|
||||||
|
<Setter Property="MinHeight" Value="24"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
<!-- 状态图标内文字 -->
|
||||||
|
<Style x:Key="StatusBadgeText" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="SmallBtn" TargetType="Button" BasedOn="{StaticResource ButtonPrimary}">
|
||||||
|
<Setter Property="Padding" Value="10,4"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="MinWidth" Value="50"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,5,0"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="SmallSuccessBtn" TargetType="Button" BasedOn="{StaticResource ButtonSuccess}">
|
||||||
|
<Setter Property="Padding" Value="10,4"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="MinWidth" Value="50"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,5,0"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="SmallWarnBtn" TargetType="Button" BasedOn="{StaticResource ButtonWarning}">
|
||||||
|
<Setter Property="Padding" Value="10,4"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="MinWidth" Value="50"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,5,0"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="SmallDefaultBtn" TargetType="Button" BasedOn="{StaticResource ButtonDefault}">
|
||||||
|
<Setter Property="Padding" Value="10,4"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="MinWidth" Value="50"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,5,0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- 全局覆盖 DataGridRowStyle:选中行始终蓝底白字,失焦不变成灰 -->
|
||||||
|
<Style x:Key="DataGridRowStyleFix" TargetType="DataGridRow">
|
||||||
|
<Setter Property="Background" Value="White"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#e1e4e8"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Background" Value="#0969da"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#f6f8fa"/>
|
||||||
|
</Trigger>
|
||||||
|
<MultiTrigger>
|
||||||
|
<MultiTrigger.Conditions>
|
||||||
|
<Condition Property="IsSelected" Value="True"/>
|
||||||
|
<Condition Property="IsMouseOver" Value="True"/>
|
||||||
|
</MultiTrigger.Conditions>
|
||||||
|
<MultiTrigger.Setters>
|
||||||
|
<Setter Property="Background" Value="#0860ca"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</MultiTrigger.Setters>
|
||||||
|
</MultiTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ReadStatusBadge 已废弃,由 StatusBadge + StatusBadgeText 替代 -->
|
||||||
|
<!-- 全局覆盖 DataGridCellStyle:去除选中单元格的高亮边框 -->
|
||||||
|
<Style x:Key="DataGridCellStyleFix" TargetType="DataGridCell">
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Padding" Value="4,6"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="#24292e"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using Velopack;
|
||||||
|
using Velopack.Locators;
|
||||||
|
using Velopack.Sources;
|
||||||
|
|
||||||
|
namespace henlo_migration
|
||||||
|
{
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private static string LogPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "error.log");
|
||||||
|
private static string BackupDir => Path.Combine(Path.GetTempPath(), "henlo_migration_data_backup");
|
||||||
|
private const string UpdateFeedUrl = "http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/";
|
||||||
|
|
||||||
|
// Velopack 更新管理器
|
||||||
|
private UpdateManager _updateManager;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Velopack 初始化:必须在所有 UI 之前调用
|
||||||
|
VelopackApp.Build().Run();
|
||||||
|
|
||||||
|
// 更新后恢复用户数据(如果存在备份)
|
||||||
|
RestoreUserData();
|
||||||
|
|
||||||
|
// 初始化更新管理器
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_updateManager = new UpdateManager(UpdateFeedUrl);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex.GetType().Name.Contains("Velopack") || ex.Message.Contains("not installed") || ex.Message.Contains("not find"))
|
||||||
|
{
|
||||||
|
// 应用未通过 Velopack 安装,跳过自动更新
|
||||||
|
_updateManager = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静默检查更新(后台线程,不阻塞启动)
|
||||||
|
if (_updateManager != null)
|
||||||
|
_ = CheckUpdateInBackgroundAsync();
|
||||||
|
|
||||||
|
base.OnStartup(e);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogException("Startup", ex);
|
||||||
|
MessageBox.Show($"启动失败:{ex.Message}\n\n{ex.StackTrace}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckUpdateInBackgroundAsync()
|
||||||
|
{
|
||||||
|
if (_updateManager == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newVersion = await _updateManager.CheckForUpdatesAsync();
|
||||||
|
if (newVersion == null) return; // 无更新
|
||||||
|
|
||||||
|
// 后台下载更新包
|
||||||
|
await _updateManager.DownloadUpdatesAsync(newVersion);
|
||||||
|
|
||||||
|
// 下载完成,提示用户重启
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var result = MessageBox.Show(
|
||||||
|
$"发现新版本 {newVersion.TargetFullRelease.Version} 并已下载完成,是否立即重启应用以应用更新?",
|
||||||
|
"更新就绪",
|
||||||
|
MessageBoxButton.YesNo,
|
||||||
|
MessageBoxImage.Information);
|
||||||
|
|
||||||
|
if (result == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
BackupUserData();
|
||||||
|
_updateManager.ApplyUpdatesAndRestart(newVersion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogException("AutoUpdate", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>手动检查更新(带提示)</summary>
|
||||||
|
public async Task CheckUpdateManualAsync(Window owner)
|
||||||
|
{
|
||||||
|
if (_updateManager == null)
|
||||||
|
{
|
||||||
|
MessageBox.Show("当前版本不支持自动更新,请前往官网下载新版本。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newVersion = await _updateManager.CheckForUpdatesAsync();
|
||||||
|
if (newVersion == null)
|
||||||
|
{
|
||||||
|
MessageBox.Show("当前已是最新版本", "检查更新", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = MessageBox.Show(
|
||||||
|
$"发现新版本:{newVersion.TargetFullRelease.Version}\n\n是否立即下载并安装?",
|
||||||
|
"发现更新",
|
||||||
|
MessageBoxButton.YesNo,
|
||||||
|
MessageBoxImage.Question);
|
||||||
|
|
||||||
|
if (result == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
await _updateManager.DownloadUpdatesAsync(newVersion);
|
||||||
|
|
||||||
|
var restartResult = MessageBox.Show(
|
||||||
|
"更新已下载完成,是否立即重启应用?",
|
||||||
|
"更新完成",
|
||||||
|
MessageBoxButton.YesNo,
|
||||||
|
MessageBoxImage.Information);
|
||||||
|
|
||||||
|
if (restartResult == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
BackupUserData();
|
||||||
|
_updateManager.ApplyUpdatesAndRestart(newVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogException("AutoUpdateManual", ex);
|
||||||
|
MessageBox.Show("检查更新失败:" + ex.Message, "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (s, args) =>
|
||||||
|
{
|
||||||
|
var ex = args.ExceptionObject as Exception;
|
||||||
|
LogException("Unhandled", ex);
|
||||||
|
MessageBox.Show($"未处理异常:{ex?.Message}\n\n{ex?.StackTrace}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
};
|
||||||
|
|
||||||
|
DispatcherUnhandledException += (s, args) =>
|
||||||
|
{
|
||||||
|
LogException("Dispatcher", args.Exception);
|
||||||
|
MessageBox.Show($"UI异常:{args.Exception.Message}\n\n{args.Exception.StackTrace}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
args.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
TaskScheduler.UnobservedTaskException += (s, args) =>
|
||||||
|
{
|
||||||
|
LogException("Task", args.Exception);
|
||||||
|
args.SetObserved();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>备份用户数据到临时目录(更新前调用)</summary>
|
||||||
|
private void BackupUserData()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rootDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
var dataDir = Path.Combine(rootDir, "data");
|
||||||
|
var preDir = Path.Combine(rootDir, "pre");
|
||||||
|
|
||||||
|
if (!Directory.Exists(dataDir) && !Directory.Exists(preDir)) return;
|
||||||
|
|
||||||
|
if (Directory.Exists(BackupDir))
|
||||||
|
Directory.Delete(BackupDir, true);
|
||||||
|
Directory.CreateDirectory(BackupDir);
|
||||||
|
|
||||||
|
if (Directory.Exists(dataDir))
|
||||||
|
CopyDirectory(dataDir, Path.Combine(BackupDir, "data"));
|
||||||
|
|
||||||
|
if (Directory.Exists(preDir))
|
||||||
|
CopyDirectory(preDir, Path.Combine(BackupDir, "pre"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogException("BackupUserData", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>从临时目录恢复用户数据(更新后首次启动调用)</summary>
|
||||||
|
private void RestoreUserData()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(BackupDir)) return;
|
||||||
|
|
||||||
|
var rootDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
|
var backupData = Path.Combine(BackupDir, "data");
|
||||||
|
if (Directory.Exists(backupData))
|
||||||
|
CopyDirectory(backupData, Path.Combine(rootDir, "data"));
|
||||||
|
|
||||||
|
var backupPre = Path.Combine(BackupDir, "pre");
|
||||||
|
if (Directory.Exists(backupPre))
|
||||||
|
CopyDirectory(backupPre, Path.Combine(rootDir, "pre"));
|
||||||
|
|
||||||
|
Directory.Delete(BackupDir, true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogException("RestoreUserData", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyDirectory(string sourceDir, string targetDir)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(targetDir);
|
||||||
|
foreach (var file in Directory.GetFiles(sourceDir))
|
||||||
|
File.Copy(file, Path.Combine(targetDir, Path.GetFileName(file)), true);
|
||||||
|
foreach (var dir in Directory.GetDirectories(sourceDir))
|
||||||
|
CopyDirectory(dir, Path.Combine(targetDir, Path.GetFileName(dir)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogException(string type, Exception ex)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var msg = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{type}] {ex?.Message}\n{ex?.StackTrace}\n\n";
|
||||||
|
File.AppendAllText(LogPath, msg);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace henlo_migration.Behaviors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 附加行为:单击 TextBlock 将其 Text 复制到剪贴板。
|
||||||
|
/// 同时通过 DataGridSelectionChanged 更新选中行名称的前景色:白色/默认蓝。
|
||||||
|
/// 用法:<TextBlock Text="{Binding Name}" b:ClickToCopy.Selectable="True"/>
|
||||||
|
/// </summary>
|
||||||
|
public static class ClickToCopy
|
||||||
|
{
|
||||||
|
// 默认蓝色(GitHub 链接色)
|
||||||
|
private static readonly Brush DefaultBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#0969da"));
|
||||||
|
// 选中时白色
|
||||||
|
private static readonly Brush WhiteBrush = Brushes.White;
|
||||||
|
|
||||||
|
public static readonly DependencyProperty SelectableProperty =
|
||||||
|
DependencyProperty.RegisterAttached(
|
||||||
|
"Selectable", typeof(bool), typeof(ClickToCopy),
|
||||||
|
new PropertyMetadata(false, OnSelectableChanged));
|
||||||
|
|
||||||
|
public static bool GetSelectable(DependencyObject obj)
|
||||||
|
=> (bool)obj.GetValue(SelectableProperty);
|
||||||
|
|
||||||
|
public static void SetSelectable(DependencyObject obj, bool value)
|
||||||
|
=> obj.SetValue(SelectableProperty, value);
|
||||||
|
|
||||||
|
private static void OnSelectableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (d is not TextBlock tb) return;
|
||||||
|
|
||||||
|
if ((bool)e.NewValue)
|
||||||
|
{
|
||||||
|
tb.Cursor = Cursors.Hand;
|
||||||
|
tb.MouseLeftButtonDown += Tb_MouseLeftButtonDown;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tb.MouseLeftButtonDown -= Tb_MouseLeftButtonDown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Tb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
// 严格检查:1) OriginalSource 必须是这个 TextBlock 本身
|
||||||
|
// 2) 鼠标位置必须在 TextBlock 的边界内(防止按钮点击冒泡)
|
||||||
|
if (sender is not TextBlock tb || string.IsNullOrEmpty(tb.Text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!ReferenceEquals(e.OriginalSource, tb))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 获取 TextBlock 在屏幕上的位置
|
||||||
|
var rect = new Rect(tb.PointToScreen(new Point(0, 0)), tb.PointToScreen(new Point(tb.ActualWidth, tb.ActualHeight)));
|
||||||
|
var mousePos = e.GetPosition(tb);
|
||||||
|
var tbBounds = new Rect(0, 0, tb.ActualWidth, tb.ActualHeight);
|
||||||
|
|
||||||
|
// 检查鼠标是否在 TextBlock 范围内
|
||||||
|
if (!tbBounds.Contains(mousePos))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Clipboard.SetText(tb.Text);
|
||||||
|
HandyControl.Controls.Growl.Success($"已复制:{tb.Text}");
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataGrid SelectionChanged 时调用:设置已选项的名称列前景色为白色
|
||||||
|
public static void RefreshSelectionBrush(DataGrid grid)
|
||||||
|
{
|
||||||
|
foreach (var item in grid.SelectedItems)
|
||||||
|
{
|
||||||
|
var row = grid.ItemContainerGenerator.ContainerFromItem(item) as DataGridRow;
|
||||||
|
if (row == null) continue;
|
||||||
|
|
||||||
|
for (int i = 0; i < grid.Columns.Count; i++)
|
||||||
|
{
|
||||||
|
var cellContent = grid.Columns[i].GetCellContent(row);
|
||||||
|
if (cellContent is TextBlock tb && GetSelectable(tb))
|
||||||
|
{
|
||||||
|
tb.Foreground = WhiteBrush;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除已选中项的名称列前景色(恢复到默认)
|
||||||
|
public static void ClearSelectionBrush(DataGrid grid)
|
||||||
|
{
|
||||||
|
for (int r = 0; r < grid.Items.Count; r++)
|
||||||
|
{
|
||||||
|
var row = grid.ItemContainerGenerator.ContainerFromIndex(r) as DataGridRow;
|
||||||
|
if (row == null || row.IsSelected) continue;
|
||||||
|
|
||||||
|
for (int i = 0; i < grid.Columns.Count; i++)
|
||||||
|
{
|
||||||
|
var cellContent = grid.Columns[i].GetCellContent(row);
|
||||||
|
if (cellContent is TextBlock tb && GetSelectable(tb))
|
||||||
|
{
|
||||||
|
tb.Foreground = DefaultBrush;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using henlo_migration.Models;
|
||||||
|
|
||||||
|
namespace henlo_migration.Common
|
||||||
|
{
|
||||||
|
public static class CommonHelper
|
||||||
|
{
|
||||||
|
/// <summary>程序运行根目录</summary>
|
||||||
|
public static string RootPath => AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
|
/// <summary>源数据文件夹(存放 table.txt、bos.txt、prc.txt、data.txt 及各表的 .txt 文件)</summary>
|
||||||
|
public static string DataPath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var path = Path.Combine(RootPath, "data");
|
||||||
|
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>索引文件目录(存放 table.txt、bos.txt、prc.txt、data.txt)</summary>
|
||||||
|
public static string DataFromPath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var path = Path.Combine(DataPath, "from");
|
||||||
|
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>预处理文件夹(存放 .prc/.fnc 存储过程定义文件)</summary>
|
||||||
|
public static string PrePath => Path.Combine(RootPath, "pre");
|
||||||
|
|
||||||
|
/// <summary>读取文本文件,每行作为一个元素返回</summary>
|
||||||
|
public static List<TableRowModel> ReadText(string filePath)
|
||||||
|
{
|
||||||
|
var result = new List<TableRowModel>();
|
||||||
|
if (!File.Exists(filePath)) return result;
|
||||||
|
var lines = File.ReadAllLines(filePath);
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(line))
|
||||||
|
result.Add(new Models.TableRowModel { Name = line.Trim() });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>写入文本内容到文件</summary>
|
||||||
|
public static void WriteToFile(string filePath, string content)
|
||||||
|
{
|
||||||
|
string? dir = Path.GetDirectoryName(filePath);
|
||||||
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
File.WriteAllText(filePath, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace henlo_migration.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SqlSugar 多库连接管理(源库 orig + 目标库 dest,均为 Oracle)
|
||||||
|
/// </summary>
|
||||||
|
public class SqlSugarHelper
|
||||||
|
{
|
||||||
|
public static SqlSugarScope? Dbs { get; private set; }
|
||||||
|
|
||||||
|
public static SqlSugarScopeProvider? DbOrig
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Dbs == null) return null;
|
||||||
|
return Dbs.GetConnectionScope("orig");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SqlSugarScopeProvider? DbDest
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Dbs == null) return null;
|
||||||
|
return Dbs.GetConnectionScope("dest");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 连接两个 Oracle 数据库(源库 + 目标库)
|
||||||
|
/// </summary>
|
||||||
|
public static void Connect()
|
||||||
|
{
|
||||||
|
Dbs = new SqlSugarScope(new List<ConnectionConfig>()
|
||||||
|
{
|
||||||
|
new ConnectionConfig(){
|
||||||
|
ConfigId = "orig",
|
||||||
|
ConnectionString = SysConfig.origConnStr,
|
||||||
|
DbType = DbType.Oracle,
|
||||||
|
IsAutoCloseConnection = true
|
||||||
|
},
|
||||||
|
new ConnectionConfig(){
|
||||||
|
ConfigId = "dest",
|
||||||
|
ConnectionString = SysConfig.destConnStr,
|
||||||
|
DbType = DbType.Oracle,
|
||||||
|
IsAutoCloseConnection = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
db =>
|
||||||
|
{
|
||||||
|
db.Ado.CommandTimeOut = 30;
|
||||||
|
});
|
||||||
|
|
||||||
|
Dbs.Ado.CommandTimeOut = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查源库连接是否有效
|
||||||
|
/// </summary>
|
||||||
|
public static bool CheckOrigConnect()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return DbOrig != null && DbOrig.Ado.IsValidConnection();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查目标库连接是否有效
|
||||||
|
/// </summary>
|
||||||
|
public static bool CheckDescConnect()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return DbDest != null && DbDest.Ado.IsValidConnection();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using IniParser;
|
||||||
|
using IniParser.Model;
|
||||||
|
|
||||||
|
namespace henlo_migration.Common
|
||||||
|
{
|
||||||
|
public static class SysConfig
|
||||||
|
{
|
||||||
|
private static IniData _init;
|
||||||
|
|
||||||
|
private static string ConfigPath => System.IO.Path.Combine(CommonHelper.RootPath, "config.ini");
|
||||||
|
static SysConfig()
|
||||||
|
{
|
||||||
|
var parser = new FileIniDataParser();
|
||||||
|
|
||||||
|
// 确保 config.ini 存在
|
||||||
|
if (!System.IO.File.Exists(ConfigPath))
|
||||||
|
{
|
||||||
|
System.IO.File.WriteAllText(ConfigPath, @"[oracle]
|
||||||
|
orig_ip=127.0.0.1
|
||||||
|
orig_exam=orcl
|
||||||
|
orig_user=system
|
||||||
|
orig_password=system
|
||||||
|
desc_ip=127.0.0.1
|
||||||
|
desc_exam=orcl
|
||||||
|
desc_user=system
|
||||||
|
desc_password=system
|
||||||
|
");
|
||||||
|
}
|
||||||
|
_init = parser.ReadFile(ConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>源 Oracle 连接字符串</summary>
|
||||||
|
public static string origConnStr =>
|
||||||
|
$"Data Source={_init["oracle"]["orig_ip"]}/{_init["oracle"]["orig_exam"]};User ID={_init["oracle"]["orig_user"]};Password={_init["oracle"]["orig_password"]}";
|
||||||
|
|
||||||
|
/// <summary>目标 Oracle 连接字符串</summary>
|
||||||
|
public static string destConnStr =>
|
||||||
|
$"Data Source={_init["oracle"]["desc_ip"]}/{_init["oracle"]["desc_exam"]};User ID={_init["oracle"]["desc_user"]};Password={_init["oracle"]["desc_password"]}";
|
||||||
|
|
||||||
|
/// <summary>版本号(从程序集读取)</summary>
|
||||||
|
public static string version =>
|
||||||
|
System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "1.0.0";
|
||||||
|
|
||||||
|
private static void Set(string section, string key, string value)
|
||||||
|
{
|
||||||
|
_init[section][key] = value;
|
||||||
|
var parser = new FileIniDataParser();
|
||||||
|
parser.WriteFile(ConfigPath, _init);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>保存源库连接信息</summary>
|
||||||
|
public static void SetOrig(string ip, string exam, string user, string pwd)
|
||||||
|
{
|
||||||
|
Set("oracle", "orig_ip", ip);
|
||||||
|
Set("oracle", "orig_exam", exam);
|
||||||
|
Set("oracle", "orig_user", user);
|
||||||
|
Set("oracle", "orig_password", pwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>保存目标库连接信息</summary>
|
||||||
|
public static void SetDest(string ip, string exam, string user, string pwd)
|
||||||
|
{
|
||||||
|
Set("oracle", "desc_ip", ip);
|
||||||
|
Set("oracle", "desc_exam", exam);
|
||||||
|
Set("oracle", "desc_user", user);
|
||||||
|
Set("oracle", "desc_password", pwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 各字段独立读取(供 ViewModel 初始化填充界面用)
|
||||||
|
public static string GetOrigIp() => _init["oracle"]["orig_ip"];
|
||||||
|
public static string GetOrigExam() => _init["oracle"]["orig_exam"];
|
||||||
|
public static string GetOrigUser() => _init["oracle"]["orig_user"];
|
||||||
|
public static string GetOrigPwd() => _init["oracle"]["orig_password"];
|
||||||
|
public static string GetDestIp() => _init["oracle"]["desc_ip"];
|
||||||
|
public static string GetDestExam() => _init["oracle"]["desc_exam"];
|
||||||
|
public static string GetDestUser() => _init["oracle"]["desc_user"];
|
||||||
|
public static string GetDestPwd() => _init["oracle"]["desc_password"];
|
||||||
|
public static string GetVersion() => version;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using henlo_migration.Enums;
|
||||||
|
|
||||||
|
namespace henlo_migration.Converters
|
||||||
|
{
|
||||||
|
/// <summary>读取状态 -> 背景色:未读取=灰、读取中=蓝、已读取=绿、读取失败=红</summary>
|
||||||
|
public class ReadStatusColorConverter : IMultiValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (values.Length == 0 || values[0] == null || values[0] == System.Windows.DependencyProperty.UnsetValue)
|
||||||
|
return new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6c757d"));
|
||||||
|
|
||||||
|
if (values[0] is StatusEnum status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
StatusEnum.读取中 => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#0969da")),
|
||||||
|
StatusEnum.不存在 => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9e9e9e")),
|
||||||
|
StatusEnum.已存在 => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#d29922")),
|
||||||
|
StatusEnum.已读取 => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#3fb950")),
|
||||||
|
StatusEnum.读取失败 => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#f85149")),
|
||||||
|
_ => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6c757d"))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6c757d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>写入状态 -> 背景色:未写入=灰、写入中=蓝、已写入=绿、写入失败=红</summary>
|
||||||
|
public class WriteStatusColorConverter : IMultiValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (values.Length == 0 || values[0] == null || values[0] == System.Windows.DependencyProperty.UnsetValue)
|
||||||
|
return new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6c757d"));
|
||||||
|
|
||||||
|
if (values[0] is StatusEnum status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
StatusEnum.写入中 => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#0969da")),
|
||||||
|
StatusEnum.已写入 => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#3fb950")),
|
||||||
|
StatusEnum.写入失败 => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#f85149")),
|
||||||
|
_ => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6c757d"))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SolidColorBrush((Color)ColorConverter.ConvertFromString("#6c757d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace henlo_migration.Enums
|
||||||
|
{
|
||||||
|
public enum BtnsEnum
|
||||||
|
{
|
||||||
|
读取,
|
||||||
|
写入,
|
||||||
|
删除,
|
||||||
|
上,
|
||||||
|
下,
|
||||||
|
置顶
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace henlo_migration.Enums
|
||||||
|
{
|
||||||
|
public enum CodeExt
|
||||||
|
{
|
||||||
|
bos,
|
||||||
|
creat,
|
||||||
|
data,
|
||||||
|
prc
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace henlo_migration.Enums
|
||||||
|
{
|
||||||
|
public enum StatusEnum
|
||||||
|
{
|
||||||
|
未读取,
|
||||||
|
不存在,
|
||||||
|
读取中,
|
||||||
|
已存在,
|
||||||
|
已读取,
|
||||||
|
读取失败,
|
||||||
|
未写入,
|
||||||
|
写入中,
|
||||||
|
已写入,
|
||||||
|
写入失败,
|
||||||
|
编译失败
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace henlo_migration.Enums
|
||||||
|
{
|
||||||
|
public enum TabsEnum
|
||||||
|
{
|
||||||
|
表 = 0,
|
||||||
|
BOS表配置 = 1,
|
||||||
|
存储过程 = 2,
|
||||||
|
表数据 = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace henlo_migration.Enums
|
||||||
|
{
|
||||||
|
public enum WorkTypeEnum
|
||||||
|
{
|
||||||
|
读,
|
||||||
|
写
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using henlo_migration.Enums;
|
||||||
|
|
||||||
|
namespace henlo_migration.Models
|
||||||
|
{
|
||||||
|
public class TableRowModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public TabsEnum Tab { get; set; }
|
||||||
|
|
||||||
|
private string _name = "";
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set { _name = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private StatusEnum _rStatus = StatusEnum.未读取;
|
||||||
|
public StatusEnum RStatus
|
||||||
|
{
|
||||||
|
get => _rStatus;
|
||||||
|
set { _rStatus = value; OnPropertyChanged(); OnPropertyChanged(nameof(ReadStatusText)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private StatusEnum _wStatus = StatusEnum.未写入;
|
||||||
|
public StatusEnum WStatus
|
||||||
|
{
|
||||||
|
get => _wStatus;
|
||||||
|
set { _wStatus = value; OnPropertyChanged(); OnPropertyChanged(nameof(WriteStatusText)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Error { get; set; } = "";
|
||||||
|
|
||||||
|
public string ReadStatusText => RStatus switch
|
||||||
|
{
|
||||||
|
StatusEnum.未读取 => "未读取",
|
||||||
|
StatusEnum.不存在 => "不存在",
|
||||||
|
StatusEnum.读取中 => "读取中",
|
||||||
|
StatusEnum.已存在 => "已存在",
|
||||||
|
StatusEnum.已读取 => "已读取",
|
||||||
|
StatusEnum.读取失败 => "读取失败",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
public string WriteStatusText => WStatus switch
|
||||||
|
{
|
||||||
|
StatusEnum.未写入 => "未写入",
|
||||||
|
StatusEnum.写入中 => "写入中",
|
||||||
|
StatusEnum.已写入 => "已写入",
|
||||||
|
StatusEnum.写入失败 => "写入失败",
|
||||||
|
StatusEnum.编译失败 => "编译失败",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||||
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>Any CPU</Platform>
|
||||||
|
<PublishDir>publish</PublishDir>
|
||||||
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>false</SelfContained>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<PublishReadyToRun>false</PublishReadyToRun>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# 恒诺迁移工具
|
||||||
|
|
||||||
|
> Oracle 数据库迁移辅助工具,支持表结构、存储过程、BOS 配置的读取与写入
|
||||||
|
|
||||||
|
## 产品概述
|
||||||
|
|
||||||
|
恒诺迁移工具是一款基于 WPF + .NET 10 的 Windows 桌面应用,用于简化 Oracle 数据库对象(表、存储过程、BOS 配置)的跨库迁移工作。
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **表结构迁移** | 读取源库表结构(DDL)、表注释、字段注释、主键/外键/索引,写入目标库 |
|
||||||
|
| **存储过程迁移** | 读取/写入存储过程和函数源码 |
|
||||||
|
| **BOS 配置迁移** | 读取 BOS 表配置写入目标库 |
|
||||||
|
| **表数据迁移** | 支持表数据的导出/导入 |
|
||||||
|
| **自动更新** | 支持通过私有服务器 Velopack 自动检查更新 |
|
||||||
|
| **断点续写** | 批量写入失败后,下次从失败行继续,无需从头开始 |
|
||||||
|
|
||||||
|
## 技术架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ WPF (UI Layer) │
|
||||||
|
│ HomeWindow.xaml ← MVVM → MainViewModel.cs │
|
||||||
|
└─────────────────────┬───────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────▼───────────────────────────────────┐
|
||||||
|
│ Services (业务层) │
|
||||||
|
│ ├─ DbServices.cs - 核心 DDL 操作 │
|
||||||
|
│ ├─ TabServices.cs - Tab 数据读写 │
|
||||||
|
│ └─ TabConfig.cs - Tab 配置 │
|
||||||
|
└─────────────────────┬───────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────▼───────────────────────────────────┐
|
||||||
|
│ Data Access (数据层) │
|
||||||
|
│ └─ SqlSugar ORM + Oracle.ManagedDataAccess │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
henlo_migration_wpf10/
|
||||||
|
├── App.xaml / App.xaml.cs # WPF 入口,自动更新,全局异常
|
||||||
|
├── Common/ # 公共工具
|
||||||
|
│ ├── CommonHelper.cs # 文件读写、路径配置
|
||||||
|
│ ├── SqlSugarHelper.cs # 数据库连接管理
|
||||||
|
│ └── SysConfig.cs # INI 配置读写
|
||||||
|
├── Services/ # 业务服务
|
||||||
|
│ ├── DbServices.cs # 核心:Oracle DDL 读写
|
||||||
|
│ ├── TabServices.cs # Tab 数据读写
|
||||||
|
│ └── TabConfig.cs # Tab 配置
|
||||||
|
├── ViewModels/ # MVVM
|
||||||
|
│ └── MainViewModel.cs # 主 ViewModel
|
||||||
|
├── Views/ # UI
|
||||||
|
│ ├── HomeWindow.xaml # 主窗口
|
||||||
|
│ └── ConnectionDialog.xaml # 连接配置
|
||||||
|
├── data/from/ # 索引文件(table.txt 等)
|
||||||
|
├── pre/ # 预处理脚本
|
||||||
|
├── pack.bat # Velopack 打包脚本
|
||||||
|
└── henlo_migration_wpf10.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
| 类别 | 技术 |
|
||||||
|
|------|------|
|
||||||
|
| 框架 | WPF / .NET 10.0 |
|
||||||
|
| ORM | SqlSugar 5.1.4 |
|
||||||
|
| 数据库 | Oracle (ODP.NET) |
|
||||||
|
| UI 库 | HandyControl 3.5.1 |
|
||||||
|
| MVVM | CommunityToolkit.Mvvm 8.3 |
|
||||||
|
| 自动更新 | Velopack 0.0.1298 |
|
||||||
|
|
||||||
|
## 运行环境
|
||||||
|
|
||||||
|
- **操作系统**:Windows 10/11 (x64)
|
||||||
|
- **.NET**:.NET 10.0 Runtime(或 Self-contained 发布)
|
||||||
|
- **数据库**:Oracle 11g R2 及以上
|
||||||
|
- **.NET SDK**:10.0+(仅开发时需要)
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 1. 初始化配置
|
||||||
|
|
||||||
|
首次使用需配置源库和目标库的连接信息:
|
||||||
|
|
||||||
|
- **源库**:读取数据的数据库
|
||||||
|
- **目标库**:写入数据的数据库
|
||||||
|
|
||||||
|
连接信息保存在 `config.ini` 中。
|
||||||
|
|
||||||
|
### 2. 索引文件
|
||||||
|
|
||||||
|
索引文件位于 `data/from/` 目录:
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `table.txt` | 表结构列表 |
|
||||||
|
| `bos.txt` | BOS 配置列表 |
|
||||||
|
| `prc.txt` | 存储过程/函数列表 |
|
||||||
|
| `data.txt` | 表数据列表 |
|
||||||
|
|
||||||
|
每行一个对象名称,读取数据库时会自动更新内容。
|
||||||
|
|
||||||
|
### 3. 基本操作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 配置源库/目标库连接
|
||||||
|
2. 点击「读取」按钮 → 从源库读取对象定义,保存到文件
|
||||||
|
3. 在右侧编辑区查看/修改代码
|
||||||
|
4. 点击「写入」或「批量写入」→ 写入目标库
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 批量写入断点续写
|
||||||
|
|
||||||
|
当批量写入过程中某行失败:
|
||||||
|
- 记录失败行的索引(断点)
|
||||||
|
- 按钮显示「继续写入」
|
||||||
|
- 修复问题后再次点击「继续写入」,从失败行继续
|
||||||
|
- 全部成功后自动重置断点
|
||||||
|
|
||||||
|
### 5. 自动更新
|
||||||
|
|
||||||
|
支持通过私有服务器自动检查更新(基于 Velopack):
|
||||||
|
|
||||||
|
- 应用启动时自动检查更新
|
||||||
|
- 也可手动点击「检查更新」按钮
|
||||||
|
- 检测到新版本后,下载并自动安装,重启后生效
|
||||||
|
- 更新过程中用户数据(data/、pre/)会自动备份并恢复
|
||||||
|
|
||||||
|
## 版本管理
|
||||||
|
|
||||||
|
### 版本号来源
|
||||||
|
|
||||||
|
版本号**唯一数据源**是 `henlo_migration_wpf10.csproj` 中的 `<Version>` 标签:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<PropertyGroup>
|
||||||
|
<Version>1.0.15</Version>
|
||||||
|
<AssemblyVersion>1.0.15</AssemblyVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
编译时 MSBuild 自动将版本号写入 `Version.ini`,程序启动时读取并显示在标题栏。
|
||||||
|
|
||||||
|
### 版本号命名规范
|
||||||
|
|
||||||
|
| 场景 | 规则 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| Bug 修复 | 递增 patch | 1.0.15 → 1.0.16 |
|
||||||
|
| 新增功能(向下兼容) | 递增 minor | 1.0.15 → 1.1.0 |
|
||||||
|
| 破坏性变更 | 递增 major | 1.0.15 → 2.0.0 |
|
||||||
|
|
||||||
|
**建议**:日常维护使用 patch 版本,小功能用 minor 版本。
|
||||||
|
|
||||||
|
## 发布流程
|
||||||
|
|
||||||
|
### 第一步:确认版本号
|
||||||
|
|
||||||
|
1. 打开 `henlo_migration_wpf10.csproj`
|
||||||
|
2. 确认或修改 `<Version>` 标签为新版本号
|
||||||
|
3. 建议同步修改 `Version.ini` 中的版本号(保持一致)
|
||||||
|
|
||||||
|
> ⚠️ 版本号只能递增。Velopack 根据版本号判断是否需要更新。
|
||||||
|
|
||||||
|
### 第二步:本地打包
|
||||||
|
|
||||||
|
在项目根目录双击运行 `pack.bat`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# pack.bat 自动完成以下步骤:
|
||||||
|
# 1. 从 csproj 读取 <Version>
|
||||||
|
# 2. dotnet publish Release 版本
|
||||||
|
# 3. 创建 data/ 目录结构(.gitkeep 占位)
|
||||||
|
# 4. vpk pack 打包为 nupkg
|
||||||
|
# 5. 输出到 releases/ 目录
|
||||||
|
```
|
||||||
|
|
||||||
|
打包产物在 `releases/` 目录:
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `henlo_migration-win-Portable.zip` | 便携版压缩包,直接分发给用户 |
|
||||||
|
| `henlo_migration-<版本>-full.nupkg` | Velopack 更新包,上传到服务器 |
|
||||||
|
| `RELEASES` | Velopack 更新元数据文件,随 nupkg 一起上传 |
|
||||||
|
|
||||||
|
### 第三步:上传到更新服务器
|
||||||
|
|
||||||
|
将以下三个文件上传到服务器对应目录(当前配置:`http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/`):
|
||||||
|
|
||||||
|
1. `henlo_migration-<版本>-full.nupkg`
|
||||||
|
2. `RELEASES`
|
||||||
|
3. `henlo_migration-win-Portable.zip`(可选,用户可直接下载这个完整包)
|
||||||
|
|
||||||
|
> ⚠️ 必须同时上传 `RELEASES` 文件,Velopack 依赖它判断版本信息。
|
||||||
|
|
||||||
|
### 第四步:通知用户更新
|
||||||
|
|
||||||
|
用户端应用会在启动时自动检测到新版本,弹出更新提示。
|
||||||
|
|
||||||
|
如需手动触发测试更新,可删除程序目录下的 `.velopack` 缓存文件夹后重启。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 写入前请确认目标库表是否存在,工具不会自动建表(表结构 DDL 除外)
|
||||||
|
- 批量写入失败时会停止,后续行不会被处理
|
||||||
|
- 索引文件修改后需重新读取才能生效
|
||||||
|
- 便携版数据(data/、pre/)在更新时自动备份恢复,无需手动迁移
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**Q: 写入时报 ORA-00922 错误?**
|
||||||
|
A: 可能是表名或列名使用了 Oracle 保留字,需要加双引号。工具已自动处理大部分情况。
|
||||||
|
|
||||||
|
**Q: 如何只更新部分行?**
|
||||||
|
A: 使用上下箭头调整行的顺序,或使用「移到顶部」功能调整优先级。
|
||||||
|
|
||||||
|
**Q: 索引文件在哪里?**
|
||||||
|
A: `data/from/` 目录下。
|
||||||
|
|
||||||
|
**Q: 更新后用户数据会丢失吗?**
|
||||||
|
A: 不会。Velopack 更新前会自动将 data/ 和 pre/ 备份到 %TEMP%,更新后恢复。
|
||||||
|
|
||||||
|
**Q: 如何强制重新检测更新?**
|
||||||
|
A: 删除程序目录下的 `.velopack` 文件夹,然后重启应用。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
内部使用工具。
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,426 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using henlo_migration.Common;
|
||||||
|
using henlo_migration.Enums;
|
||||||
|
using henlo_migration.Models;
|
||||||
|
|
||||||
|
namespace henlo_migration.Services
|
||||||
|
{
|
||||||
|
public class DbServices
|
||||||
|
{
|
||||||
|
/// <summary>获取表建表SQL - 包含建表语句、表注释、字段注释、主键、唯一键、外键、索引,不依赖存储过程</summary>
|
||||||
|
public static string GetTableSql(string tableName)
|
||||||
|
{
|
||||||
|
var db = SqlSugarHelper.DbOrig;
|
||||||
|
string t = tableName.Trim().ToUpperInvariant();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
// 0. 关闭不需要的 DDL 附加属性
|
||||||
|
db.Ado.ExecuteCommand(@"
|
||||||
|
BEGIN
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'STORAGE', FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'TABLESPACE', FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'SEGMENT_ATTRIBUTES', FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'CONSTRAINTS', FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'REF_CONSTRAINTS', FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'CONSTRAINTS_AS_ALTER', TRUE);
|
||||||
|
END;");
|
||||||
|
|
||||||
|
// 1. 建表语句
|
||||||
|
var ddlResult = db.Ado.GetDataTable(
|
||||||
|
"SELECT DBMS_METADATA.GET_DDL('TABLE', :tbl) AS SQL_TEXT FROM DUAL",
|
||||||
|
new SugarParameter("tbl", t));
|
||||||
|
if (ddlResult.Rows.Count > 0)
|
||||||
|
{
|
||||||
|
sb.Append(ddlResult.Rows[0]["SQL_TEXT"]?.ToString() ?? "");
|
||||||
|
sb.AppendLine(";\n/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 表注释(每个语句末尾加 ;/\n,便于 SplitStatements 分割)
|
||||||
|
var tabCmt = db.Ado.GetDataTable(
|
||||||
|
"SELECT COMMENTS FROM USER_TAB_COMMENTS WHERE TABLE_NAME = :tbl AND COMMENTS IS NOT NULL",
|
||||||
|
new SugarParameter("tbl", t));
|
||||||
|
foreach (DataRow r in tabCmt.Rows)
|
||||||
|
sb.AppendLine($"COMMENT ON TABLE \"{t}\" IS '{EscapeQuote(r["COMMENTS"].ToString())}';\n/");
|
||||||
|
|
||||||
|
// 3. 字段注释
|
||||||
|
var colCmt = db.Ado.GetDataTable(
|
||||||
|
"SELECT COLUMN_NAME, COMMENTS FROM USER_COL_COMMENTS WHERE TABLE_NAME = :tbl AND COMMENTS IS NOT NULL ORDER BY COLUMN_NAME",
|
||||||
|
new SugarParameter("tbl", t));
|
||||||
|
foreach (DataRow r in colCmt.Rows)
|
||||||
|
{
|
||||||
|
string cName = r["COLUMN_NAME"].ToString() ?? "";
|
||||||
|
sb.AppendLine($"COMMENT ON COLUMN \"{t}\".\"{cName}\" IS '{EscapeQuote(r["COMMENTS"].ToString())}';\n/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 主键 & 唯一键
|
||||||
|
var pkUkCons = db.Ado.GetDataTable(
|
||||||
|
"SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM USER_CONSTRAINTS WHERE TABLE_NAME = :tbl AND CONSTRAINT_TYPE IN ('P','U') ORDER BY CONSTRAINT_TYPE, CONSTRAINT_NAME",
|
||||||
|
new SugarParameter("tbl", t));
|
||||||
|
foreach (DataRow cr in pkUkCons.Rows)
|
||||||
|
{
|
||||||
|
var cols = db.Ado.GetDataTable(
|
||||||
|
"SELECT COLUMN_NAME FROM USER_CONS_COLUMNS WHERE CONSTRAINT_NAME = :cn ORDER BY POSITION",
|
||||||
|
new SugarParameter("cn", cr["CONSTRAINT_NAME"].ToString()));
|
||||||
|
string colList = "";
|
||||||
|
for (int i = 0; i < cols.Rows.Count; i++)
|
||||||
|
colList += (i > 0 ? ", " : "") + $"\"{cols.Rows[i]["COLUMN_NAME"]}\"";
|
||||||
|
string kind = cr["CONSTRAINT_TYPE"].ToString() == "P" ? "PRIMARY KEY" : "UNIQUE";
|
||||||
|
sb.AppendLine($"ALTER TABLE \"{t}\" ADD CONSTRAINT \"{cr["CONSTRAINT_NAME"]}\" {kind} ({colList});\n/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 外键
|
||||||
|
var fkCons = db.Ado.GetDataTable(
|
||||||
|
"SELECT CONSTRAINT_NAME, R_CONSTRAINT_NAME, DELETE_RULE FROM USER_CONSTRAINTS WHERE TABLE_NAME = :tbl AND CONSTRAINT_TYPE = 'R' ORDER BY CONSTRAINT_NAME",
|
||||||
|
new SugarParameter("tbl", t));
|
||||||
|
foreach (DataRow fr in fkCons.Rows)
|
||||||
|
{
|
||||||
|
var localCols = db.Ado.GetDataTable(
|
||||||
|
"SELECT COLUMN_NAME FROM USER_CONS_COLUMNS WHERE CONSTRAINT_NAME = :cn ORDER BY POSITION",
|
||||||
|
new SugarParameter("cn", fr["CONSTRAINT_NAME"].ToString()));
|
||||||
|
var refCons = db.Ado.GetDataTable(
|
||||||
|
"SELECT TABLE_NAME FROM USER_CONSTRAINTS WHERE CONSTRAINT_NAME = :cn",
|
||||||
|
new SugarParameter("cn", fr["R_CONSTRAINT_NAME"].ToString()));
|
||||||
|
if (refCons.Rows.Count > 0)
|
||||||
|
{
|
||||||
|
var refCols = db.Ado.GetDataTable(
|
||||||
|
"SELECT COLUMN_NAME FROM USER_CONS_COLUMNS WHERE CONSTRAINT_NAME = :cn ORDER BY POSITION",
|
||||||
|
new SugarParameter("cn", fr["R_CONSTRAINT_NAME"].ToString()));
|
||||||
|
string localList = "";
|
||||||
|
for (int i = 0; i < localCols.Rows.Count; i++)
|
||||||
|
localList += (i > 0 ? ", " : "") + $"\"{localCols.Rows[i]["COLUMN_NAME"]}\"";
|
||||||
|
string refList = "";
|
||||||
|
for (int i = 0; i < refCols.Rows.Count; i++)
|
||||||
|
refList += (i > 0 ? ", " : "") + $"\"{refCols.Rows[i]["COLUMN_NAME"]}\"";
|
||||||
|
string delRule = fr["DELETE_RULE"].ToString();
|
||||||
|
string onDel = delRule == "CASCADE" ? " ON DELETE CASCADE"
|
||||||
|
: delRule == "SET NULL" ? " ON DELETE SET NULL" : "";
|
||||||
|
sb.AppendLine($"ALTER TABLE \"{t}\" ADD CONSTRAINT \"{fr["CONSTRAINT_NAME"]}\" FOREIGN KEY ({localList}) REFERENCES \"{refCons.Rows[0]["TABLE_NAME"]}\" ({refList}){onDel};\n/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 普通索引
|
||||||
|
var pkUkNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var pkUkRaw = db.Ado.GetDataTable(
|
||||||
|
"SELECT CONSTRAINT_NAME FROM USER_CONSTRAINTS WHERE TABLE_NAME = :tbl AND CONSTRAINT_TYPE IN ('P','U')",
|
||||||
|
new SugarParameter("tbl", t));
|
||||||
|
foreach (DataRow r in pkUkRaw.Rows) pkUkNames.Add(r["CONSTRAINT_NAME"].ToString());
|
||||||
|
|
||||||
|
var idxRaw = db.Ado.GetDataTable(
|
||||||
|
"SELECT INDEX_NAME, UNIQUENESS FROM USER_INDEXES WHERE TABLE_NAME = :tbl AND INDEX_TYPE IN ('NORMAL','FUNCTION-BASED NORMAL') ORDER BY INDEX_NAME",
|
||||||
|
new SugarParameter("tbl", t));
|
||||||
|
foreach (DataRow ir in idxRaw.Rows)
|
||||||
|
{
|
||||||
|
if (pkUkNames.Contains(ir["INDEX_NAME"].ToString())) continue;
|
||||||
|
var idxCols = db.Ado.GetDataTable(
|
||||||
|
"SELECT COLUMN_NAME FROM USER_IND_COLUMNS WHERE INDEX_NAME = :idx_nm ORDER BY COLUMN_POSITION",
|
||||||
|
new SugarParameter("idx_nm", ir["INDEX_NAME"].ToString()));
|
||||||
|
string colList = "";
|
||||||
|
for (int i = 0; i < idxCols.Rows.Count; i++)
|
||||||
|
colList += (i > 0 ? ", " : "") + $"\"{idxCols.Rows[i]["COLUMN_NAME"]}\"";
|
||||||
|
string unique = ir["UNIQUENESS"].ToString() == "UNIQUE" ? "UNIQUE " : "";
|
||||||
|
sb.AppendLine($"CREATE {unique}INDEX \"{ir["INDEX_NAME"]}\" ON \"{t}\" ({colList});\n/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EscapeQuote(string s)
|
||||||
|
=> s?.Replace("'", "''") ?? "";
|
||||||
|
|
||||||
|
/// <summary>获取BOS表配置 - 调用 hmig_get_table_bos_31 存储过程获取 r_table,再查 tabletext</summary>
|
||||||
|
public static string GetTableBos(string tableName)
|
||||||
|
{
|
||||||
|
var db = SqlSugarHelper.DbOrig;
|
||||||
|
string t = tableName.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
// 1. 调用存储过程,获取 r_id 和 r_table
|
||||||
|
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 };
|
||||||
|
db.Ado.ExecuteCommand(
|
||||||
|
"BEGIN hmig_get_table_bos_31(:p_name, :r_id, :r_table); END;",
|
||||||
|
new SugarParameter("p_name", t),
|
||||||
|
pRId,
|
||||||
|
pRTable);
|
||||||
|
|
||||||
|
// 解析 OUT 参数
|
||||||
|
string rTable = pRTable.Value?.ToString() ?? "";
|
||||||
|
string rId = pRId.Value?.ToString() ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(rTable) || string.IsNullOrWhiteSpace(rId)) return "";
|
||||||
|
|
||||||
|
// 2. 根据 r_table 确定字段名,用 r_id 查对应记录的文本
|
||||||
|
string colName = rTable.ToUpperInvariant() == "T_TABLETEXT"
|
||||||
|
? "tabletext"
|
||||||
|
: "table_text";
|
||||||
|
var textResult = db.Ado.GetDataTable(
|
||||||
|
$"SELECT {colName} FROM {rTable} WHERE id = :rid",
|
||||||
|
new SugarParameter("rid", rId));
|
||||||
|
return textResult.Rows.Count > 0 ? textResult.Rows[0][colName]?.ToString() ?? "" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>获取所有存储过程和函数列表 - 查 USER_OBJECTS,返回纯名称(不带前缀),不依赖存储过程</summary>
|
||||||
|
public static string GetAllPrc()
|
||||||
|
{
|
||||||
|
var db = SqlSugarHelper.DbOrig;
|
||||||
|
var result = db.Ado.GetDataTable(
|
||||||
|
"SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_TYPE IN ('PROCEDURE','FUNCTION') AND STATUS='VALID' ORDER BY OBJECT_NAME");
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (DataRow row in result.Rows)
|
||||||
|
sb.AppendLine(row["OBJECT_NAME"].ToString());
|
||||||
|
return sb.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>获取单个存储过程/函数源码 - 查 USER_SOURCE,拼接完整 CREATE OR REPLACE 语句</summary>
|
||||||
|
public static string GetPrcSql(string prcName)
|
||||||
|
{
|
||||||
|
var db = SqlSugarHelper.DbOrig;
|
||||||
|
string name = prcName.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
// 1. 查类型(PROCEDURE 或 FUNCTION)
|
||||||
|
var typeResult = db.Ado.GetDataTable(
|
||||||
|
"SELECT OBJECT_TYPE FROM USER_OBJECTS WHERE OBJECT_NAME = :n AND OBJECT_TYPE IN ('PROCEDURE','FUNCTION', 'TRIGGER')",
|
||||||
|
new SugarParameter("n", name));
|
||||||
|
if (typeResult.Rows.Count == 0) return "";
|
||||||
|
string objType = typeResult.Rows[0]["OBJECT_TYPE"].ToString(); // PROCEDURE 或 FUNCTION
|
||||||
|
|
||||||
|
// 2. 查源码
|
||||||
|
var result = db.Ado.GetDataTable(
|
||||||
|
"SELECT TEXT FROM USER_SOURCE WHERE NAME = :n ORDER BY LINE",
|
||||||
|
new SugarParameter("n", name));
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (DataRow row in result.Rows)
|
||||||
|
sb.Append(row["TEXT"]);
|
||||||
|
|
||||||
|
// 3. 拼接完整 CREATE OR REPLACE 语句
|
||||||
|
string code = sb.ToString();
|
||||||
|
if (code.StartsWith(objType))
|
||||||
|
code = "CREATE OR REPLACE " + code;
|
||||||
|
|
||||||
|
// 4. 添加末尾的 /(Oracle PL/SQL 块结束符)
|
||||||
|
code = code.TrimEnd();
|
||||||
|
if (!code.EndsWith("/"))
|
||||||
|
code += "\n/";
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>获取表数据(INSERT语句)- 直接 SELECT + C# 拼接,不依赖存储过程</summary>
|
||||||
|
public static string GetTableData(string tableName)
|
||||||
|
{
|
||||||
|
var db = SqlSugarHelper.DbOrig;
|
||||||
|
string safeName = tableName.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
// 1. 检查行数
|
||||||
|
var count = db.Ado.GetInt($"SELECT COUNT(*) FROM {safeName}");
|
||||||
|
if (count == 0)
|
||||||
|
return $"-- 表 {safeName} 无数据";
|
||||||
|
if (count > 200)
|
||||||
|
return $"-- 错误: 表 {safeName} 数据行数({count})超过200行限制";
|
||||||
|
|
||||||
|
// 2. 取列定义(列名 + 数据类型)
|
||||||
|
var cols = db.Ado.GetDataTable(@"
|
||||||
|
SELECT COLUMN_NAME, DATA_TYPE
|
||||||
|
FROM USER_TAB_COLUMNS
|
||||||
|
WHERE TABLE_NAME = :tbl
|
||||||
|
ORDER BY COLUMN_ID",
|
||||||
|
new SugarParameter("tbl", safeName));
|
||||||
|
|
||||||
|
// 3. 查所有数据
|
||||||
|
var rows = db.Ado.GetDataTable($"SELECT * FROM {safeName}");
|
||||||
|
|
||||||
|
// 4. C# 端拼接 INSERT
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.AppendLine($"-- 表 {safeName} 共 {count} 行数据");
|
||||||
|
|
||||||
|
for (int ri = 0; ri < rows.Rows.Count; ri++)
|
||||||
|
{
|
||||||
|
var dr = rows.Rows[ri];
|
||||||
|
sb.Append("INSERT INTO " + safeName + " (");
|
||||||
|
|
||||||
|
// 列名
|
||||||
|
for (int ci = 0; ci < cols.Rows.Count; ci++)
|
||||||
|
sb.Append(ci > 0 ? ", " + cols.Rows[ci]["COLUMN_NAME"] : cols.Rows[ci]["COLUMN_NAME"].ToString());
|
||||||
|
sb.Append(") VALUES (");
|
||||||
|
|
||||||
|
// 值(按列名取,保证和列名对应)
|
||||||
|
for (int ci = 0; ci < cols.Rows.Count; ci++)
|
||||||
|
{
|
||||||
|
if (ci > 0) sb.Append(", ");
|
||||||
|
var colName = cols.Rows[ci]["COLUMN_NAME"].ToString();
|
||||||
|
var dataType = cols.Rows[ci]["DATA_TYPE"].ToString();
|
||||||
|
var val = dr[colName];
|
||||||
|
if (val == null || val == DBNull.Value)
|
||||||
|
sb.Append("NULL");
|
||||||
|
else if (dataType == "DATE" || dataType == "TIMESTAMP")
|
||||||
|
sb.Append($"TO_DATE('{val:yyyy-MM-dd HH:mm:ss}', 'YYYY-MM-DD HH24:MI:SS')");
|
||||||
|
else
|
||||||
|
sb.Append("'" + val.ToString()
|
||||||
|
.Replace("'", "''")
|
||||||
|
.Replace("\r", "").Replace("\n", " ") + "'");
|
||||||
|
}
|
||||||
|
sb.AppendLine(");");
|
||||||
|
}
|
||||||
|
sb.Append("COMMIT;");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly string[] PlsqlKeywords = new[]
|
||||||
|
{
|
||||||
|
"PROCEDURE", "FUNCTION", "PACKAGE", "TRIGGER", "TYPE", "VIEW"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>从 CREATE 语句中提取对象名(用于查 USER_ERRORS)</summary>
|
||||||
|
private static string? GetObjectNameFromCreate(string sql)
|
||||||
|
{
|
||||||
|
string upper = sql.ToUpperInvariant();
|
||||||
|
int createIdx = upper.IndexOf("CREATE ");
|
||||||
|
if (createIdx < 0) return null;
|
||||||
|
|
||||||
|
// 找到 PROCEDURE / FUNCTION / PACKAGE 等关键字的位置
|
||||||
|
int bestIdx = -1, bestLen = 0;
|
||||||
|
string? keyword = null;
|
||||||
|
foreach (var kw in PlsqlKeywords)
|
||||||
|
{
|
||||||
|
int idx = upper.IndexOf(kw, createIdx);
|
||||||
|
if (idx >= 0 && (bestIdx < 0 || idx < bestIdx))
|
||||||
|
{
|
||||||
|
bestIdx = idx;
|
||||||
|
bestLen = kw.Length;
|
||||||
|
keyword = kw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestIdx < 0) return null;
|
||||||
|
|
||||||
|
// 关键字后面的第一个非空 token 就是对象名(支持 schema.name 和 "引号名")
|
||||||
|
int pos = bestIdx + bestLen;
|
||||||
|
while (pos < sql.Length && char.IsWhiteSpace(sql[pos])) pos++;
|
||||||
|
if (pos >= sql.Length) return null;
|
||||||
|
|
||||||
|
StringBuilder sb = new();
|
||||||
|
// 处理带引号的标识符
|
||||||
|
if (sql[pos] == '"')
|
||||||
|
{
|
||||||
|
pos++; // 跳过 opening quote
|
||||||
|
while (pos < sql.Length && sql[pos] != '"') sb.Append(sql[pos++]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (pos < sql.Length && !char.IsWhiteSpace(sql[pos]) && sql[pos] != '(' && sql[pos] != '.')
|
||||||
|
sb.Append(sql[pos++]);
|
||||||
|
// 处理 schema.name(再往前一个 token)
|
||||||
|
// 实际上上面已经读到 . 为止,需要重来
|
||||||
|
}
|
||||||
|
// 简化:直接取 "keyword 之后的第一个完整 token(直到空格或括号)"
|
||||||
|
string token = GetNextToken(sql, bestIdx + bestLen);
|
||||||
|
if (string.IsNullOrEmpty(token)) return null;
|
||||||
|
|
||||||
|
// token 可能是 schema.name,只取 name 部分
|
||||||
|
if (token.Contains('.'))
|
||||||
|
token = token.Substring(token.LastIndexOf('.') + 1);
|
||||||
|
// 去掉引号
|
||||||
|
token = token.Trim('"');
|
||||||
|
return token.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetNextToken(string sql, int start)
|
||||||
|
{
|
||||||
|
while (start < sql.Length && char.IsWhiteSpace(sql[start])) start++;
|
||||||
|
if (start >= sql.Length) return "";
|
||||||
|
StringBuilder sb = new();
|
||||||
|
// 引号标识符
|
||||||
|
if (sql[start] == '"')
|
||||||
|
{
|
||||||
|
start++; // 跳过 "
|
||||||
|
while (start < sql.Length && sql[start] != '"') sb.Append(sql[start++]);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
while (start < sql.Length && !char.IsWhiteSpace(sql[start]) && sql[start] != '(')
|
||||||
|
sb.Append(sql[start++]);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>检查刚执行的 CREATE 语句是否有编译错误,有则抛异常</summary>
|
||||||
|
private static void CheckCompilationErrors(string sql)
|
||||||
|
{
|
||||||
|
string? objName = GetObjectNameFromCreate(sql);
|
||||||
|
if (objName == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string query = @"SELECT LINE, POSITION, TEXT FROM USER_ERRORS WHERE NAME = :name ORDER BY LINE, POSITION";
|
||||||
|
var dt = SqlSugarHelper.DbDest.Ado.GetDataTable(query, new { name = objName });
|
||||||
|
if (dt.Rows.Count == 0) return;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"对象 {objName} 编译错误:");
|
||||||
|
foreach (System.Data.DataRow row in dt.Rows)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" Line {row["LINE"]}, Pos {row["POSITION"]}: {row["TEXT"]}");
|
||||||
|
}
|
||||||
|
throw new Exception(sb.ToString());
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (!(ex is Exception && ex.Message.Contains("编译错误")))
|
||||||
|
{
|
||||||
|
// 查询 USER_ERRORS 失败(如表不存在),忽略,不阻断写入流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>在目标库执行建表语句</summary>
|
||||||
|
public static void ExecuteCreate(string sql)
|
||||||
|
{
|
||||||
|
foreach (var stmt in SplitStatements(sql))
|
||||||
|
{
|
||||||
|
// 彻底 Trim 去除首尾空白和换行
|
||||||
|
string clean = stmt.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(clean)) continue;
|
||||||
|
|
||||||
|
// 写日志便于排查 ORA 错误(写执行前的清理后语句)
|
||||||
|
string logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "execute_log.txt");
|
||||||
|
File.AppendAllText(logPath, $"=== {DateTime.Now:HH:mm:ss} [{clean.Length} chars] ===\n{clean}\n\n");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SqlSugarHelper.DbDest.Ado.ExecuteCommand(clean);
|
||||||
|
CheckCompilationErrors(clean);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 追加错误信息到日志
|
||||||
|
File.AppendAllText(logPath, $"!!! ERROR: {ex.Message}\n");
|
||||||
|
throw; // 重新抛出,让调用方捕获
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
// Oracle PL/SQL 块用 \n/ 分隔
|
||||||
|
var parts = sql.Split(new[] { "\n/" }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var result = new List<string>();
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
// Trim 掉首尾空白,保留语句主体
|
||||||
|
var trimmed = part.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(trimmed))
|
||||||
|
result.Add(trimmed);
|
||||||
|
}
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using henlo_migration.Enums;
|
||||||
|
|
||||||
|
namespace henlo_migration.Services
|
||||||
|
{
|
||||||
|
/// <summary>Tab 配置接口,描述一个 Tab 与数据库和文件的交互方式</summary>
|
||||||
|
public interface ITabConfig
|
||||||
|
{
|
||||||
|
TabsEnum Tab { get; }
|
||||||
|
WorkTypeEnum WorkType { get; }
|
||||||
|
/// <summary>索引文件目录(如 data/from/)</summary>
|
||||||
|
string IndexFolderPath { get; }
|
||||||
|
/// <summary>内容文件目录(如 data/)</summary>
|
||||||
|
string ContentFolderPath { get; }
|
||||||
|
string FileName { get; }
|
||||||
|
CodeExt CodeExt { get; }
|
||||||
|
Func<string, string>? ReadDbHandler { get; }
|
||||||
|
Action<string, string>? CodeClickHandler { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using henlo_migration.Enums;
|
||||||
|
|
||||||
|
namespace henlo_migration.Services
|
||||||
|
{
|
||||||
|
/// <summary>Tab 配置实现,每个 Tab 一个实例</summary>
|
||||||
|
public class TabConfig : ITabConfig
|
||||||
|
{
|
||||||
|
public TabsEnum Tab { get; }
|
||||||
|
public WorkTypeEnum WorkType { get; } = WorkTypeEnum.读;
|
||||||
|
/// <summary>索引文件目录(如 data/from/)</summary>
|
||||||
|
public string IndexFolderPath { get; }
|
||||||
|
/// <summary>内容文件目录(如 data/)</summary>
|
||||||
|
public string ContentFolderPath { get; }
|
||||||
|
public string FileName { get; }
|
||||||
|
public CodeExt CodeExt { get; }
|
||||||
|
public Func<string, string>? ReadDbHandler { get; }
|
||||||
|
public Action<string, string>? CodeClickHandler { get; }
|
||||||
|
|
||||||
|
public TabConfig(
|
||||||
|
TabsEnum tab,
|
||||||
|
string indexFolderPath,
|
||||||
|
string contentFolderPath,
|
||||||
|
string fileName,
|
||||||
|
CodeExt codeExt,
|
||||||
|
Func<string, string>? readDbHandler = null,
|
||||||
|
Action<string, string>? codeClickHandler = null)
|
||||||
|
{
|
||||||
|
Tab = tab;
|
||||||
|
IndexFolderPath = indexFolderPath;
|
||||||
|
ContentFolderPath = contentFolderPath;
|
||||||
|
FileName = fileName;
|
||||||
|
CodeExt = codeExt;
|
||||||
|
ReadDbHandler = readDbHandler;
|
||||||
|
CodeClickHandler = codeClickHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using henlo_migration.Common;
|
||||||
|
using henlo_migration.Enums;
|
||||||
|
using henlo_migration.Models;
|
||||||
|
|
||||||
|
namespace henlo_migration.Services
|
||||||
|
{
|
||||||
|
/// <summary>管理单个Tab的数据列表和操作</summary>
|
||||||
|
public class TabServices
|
||||||
|
{
|
||||||
|
private readonly ITabConfig _cfg;
|
||||||
|
|
||||||
|
public ObservableCollection<TableRowModel> List { get; } = new();
|
||||||
|
|
||||||
|
public event Action<string, string>? RowClickEvent;
|
||||||
|
|
||||||
|
public TabServices(ITabConfig cfg)
|
||||||
|
{
|
||||||
|
_cfg = cfg;
|
||||||
|
LoadFromFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCodeExt() => _cfg.CodeExt.ToString();
|
||||||
|
/// <summary>内容文件目录(读取后生成的文件存放位置)</summary>
|
||||||
|
public string GetContentFolderPath() => _cfg.ContentFolderPath;
|
||||||
|
|
||||||
|
public void LoadFromFile()
|
||||||
|
{
|
||||||
|
// 读索引文件
|
||||||
|
string filePath = Path.Combine(_cfg.IndexFolderPath, _cfg.FileName);
|
||||||
|
var items = CommonHelper.ReadText(filePath);
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
item.Tab = _cfg.Tab;
|
||||||
|
// 内容文件存到 data/ 目录
|
||||||
|
string itemFileName = $"{item.Name}_{_cfg.CodeExt}.txt";
|
||||||
|
string itemFilePath = Path.Combine(_cfg.ContentFolderPath, itemFileName);
|
||||||
|
item.RStatus = File.Exists(itemFilePath) ? StatusEnum.已存在 : StatusEnum.不存在;
|
||||||
|
List.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveToFile()
|
||||||
|
{
|
||||||
|
string filePath = Path.Combine(_cfg.IndexFolderPath, _cfg.FileName);
|
||||||
|
var lines = new string[List.Count];
|
||||||
|
for (int i = 0; i < List.Count; i++)
|
||||||
|
lines[i] = List[i].Name;
|
||||||
|
File.WriteAllLines(filePath, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRow(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name)) return;
|
||||||
|
if (List.Count == 0 || !List.Any(x => x.Name == name))
|
||||||
|
{
|
||||||
|
List.Add(new TableRowModel { Name = name, Tab = _cfg.Tab });
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReadRow(TableRowModel row)
|
||||||
|
{
|
||||||
|
if (!SqlSugarHelper.CheckOrigConnect())
|
||||||
|
{
|
||||||
|
HandyControl.Controls.Growl.Warning("源数据库未连接,请先连接");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.RStatus = StatusEnum.读取中;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string code = await Task.Run(() =>
|
||||||
|
_cfg.ReadDbHandler != null ? _cfg.ReadDbHandler(row.Name) : "");
|
||||||
|
|
||||||
|
// 内容文件写入 data/ 目录
|
||||||
|
string fileName = $"{row.Name}_{_cfg.CodeExt}.txt";
|
||||||
|
string filePath = Path.Combine(_cfg.ContentFolderPath, fileName);
|
||||||
|
|
||||||
|
CommonHelper.WriteToFile(filePath, code);
|
||||||
|
row.RStatus = StatusEnum.已读取;
|
||||||
|
OnRowClick(filePath, code);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
row.RStatus = StatusEnum.读取失败;
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
Views.ErrorDialog.ShowError($"读取 [{row.Name}] 失败:\n\n{ex.Message}\n\n{ex.StackTrace}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteRow(TableRowModel row)
|
||||||
|
{
|
||||||
|
if (!SqlSugarHelper.CheckDescConnect())
|
||||||
|
{
|
||||||
|
HandyControl.Controls.Growl.Warning("目标数据库未连接,请先连接");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.WStatus = StatusEnum.写入中;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 从 data/ 目录读取内容文件
|
||||||
|
string fileName = $"{row.Name}_{_cfg.CodeExt}.txt";
|
||||||
|
string filePath = Path.Combine(_cfg.ContentFolderPath, fileName);
|
||||||
|
string code = File.ReadAllText(filePath);
|
||||||
|
|
||||||
|
await Task.Run(() => DbServices.ExecuteCreate(code));
|
||||||
|
row.WStatus = StatusEnum.已写入;
|
||||||
|
HandyControl.Controls.Growl.Success($"写入成功:{row.Name}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
row.WStatus = StatusEnum.写入失败;
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
Views.ErrorDialog.ShowError($"写入 [{row.Name}] 失败:\n\n{ex.Message}\n\n{ex.StackTrace}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteRow(TableRowModel row)
|
||||||
|
{
|
||||||
|
// 删除内容文件(data/ 目录)
|
||||||
|
string itemFileName = $"{row.Name}_{_cfg.CodeExt}.txt";
|
||||||
|
string itemFilePath = Path.Combine(_cfg.ContentFolderPath, itemFileName);
|
||||||
|
if (File.Exists(itemFilePath))
|
||||||
|
File.Delete(itemFilePath);
|
||||||
|
List.Remove(row);
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveUp(TableRowModel row)
|
||||||
|
{
|
||||||
|
int idx = List.IndexOf(row);
|
||||||
|
if (idx > 0)
|
||||||
|
{
|
||||||
|
List.Move(idx, idx - 1);
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveDown(TableRowModel row)
|
||||||
|
{
|
||||||
|
int idx = List.IndexOf(row);
|
||||||
|
if (idx < List.Count - 1)
|
||||||
|
{
|
||||||
|
List.Move(idx, idx + 1);
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveToTop(TableRowModel row)
|
||||||
|
{
|
||||||
|
int idx = List.IndexOf(row);
|
||||||
|
if (idx > 0)
|
||||||
|
{
|
||||||
|
List.Move(idx, 0);
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnRowClick(string filePath, string code)
|
||||||
|
{
|
||||||
|
RowClickEvent?.Invoke(filePath, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using henlo_migration.Common;
|
||||||
|
using henlo_migration.Enums;
|
||||||
|
using henlo_migration.Models;
|
||||||
|
using henlo_migration.Services;
|
||||||
|
|
||||||
|
namespace henlo_migration.ViewModels
|
||||||
|
{
|
||||||
|
public class MainViewModel : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
// 四个 Tab 的数据
|
||||||
|
public TabServices TableTab { get; }
|
||||||
|
public TabServices BosTab { get; }
|
||||||
|
public TabServices PrcTab { get; }
|
||||||
|
public TabServices DataTab { get; }
|
||||||
|
|
||||||
|
// 当前激活的 TabServices
|
||||||
|
private TabServices? _currentTab;
|
||||||
|
private TabServices? CurrentTab
|
||||||
|
{
|
||||||
|
get => _currentTab;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_currentTab = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(TablesData));
|
||||||
|
OnPropertyChanged(nameof(BosTablesData));
|
||||||
|
OnPropertyChanged(nameof(StoredProcsData));
|
||||||
|
OnPropertyChanged(nameof(CurrentDataData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab 切换
|
||||||
|
private int _selectedTabIndex;
|
||||||
|
public int SelectedTabIndex
|
||||||
|
{
|
||||||
|
get => _selectedTabIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_selectedTabIndex = value;
|
||||||
|
CurrentTab = value switch
|
||||||
|
{
|
||||||
|
0 => TableTab,
|
||||||
|
1 => BosTab,
|
||||||
|
2 => PrcTab,
|
||||||
|
3 => DataTab,
|
||||||
|
_ => TableTab
|
||||||
|
};
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(TablesData));
|
||||||
|
OnPropertyChanged(nameof(BosTablesData));
|
||||||
|
OnPropertyChanged(nameof(StoredProcsData));
|
||||||
|
OnPropertyChanged(nameof(CurrentDataData));
|
||||||
|
OnPropertyChanged(nameof(CurrentTabName));
|
||||||
|
OnPropertyChanged(nameof(CurrentTabCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 四个Tab数据源别名(供DataGrid绑定)
|
||||||
|
public ObservableCollection<TableRowModel> TablesData => TableTab.List;
|
||||||
|
public ObservableCollection<TableRowModel> BosTablesData => BosTab.List;
|
||||||
|
public ObservableCollection<TableRowModel> StoredProcsData => PrcTab.List;
|
||||||
|
public ObservableCollection<TableRowModel> CurrentDataData => DataTab.List;
|
||||||
|
|
||||||
|
public string CurrentTabName => SelectedTabIndex switch
|
||||||
|
{
|
||||||
|
0 => "表配置",
|
||||||
|
1 => "BOS配置",
|
||||||
|
2 => "存储过程",
|
||||||
|
3 => "表数据",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
public string CurrentTabCount => CurrentTab?.List.Count.ToString() ?? "0";
|
||||||
|
|
||||||
|
// 选中的行
|
||||||
|
private TableRowModel? _selectedRow;
|
||||||
|
public TableRowModel? SelectedRow
|
||||||
|
{
|
||||||
|
get => _selectedRow;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_selectedRow = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
// 选中行时自动加载已有文件内容
|
||||||
|
LoadSelectedRowContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadSelectedRowContent()
|
||||||
|
{
|
||||||
|
if (_selectedRow == null || CurrentTab == null) return;
|
||||||
|
|
||||||
|
// 构建文件路径
|
||||||
|
string fileName = $"{_selectedRow.Name}_{CurrentTab.GetCodeExt()}.txt";
|
||||||
|
string filePath = Path.Combine(CurrentTab.GetContentFolderPath(), fileName);
|
||||||
|
|
||||||
|
// 始终设置 CurrentFileName,这样保存按钮可用
|
||||||
|
CurrentFileName = filePath;
|
||||||
|
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string content = File.ReadAllText(filePath);
|
||||||
|
CurrentFileContent = content;
|
||||||
|
SqlCode = content;
|
||||||
|
}
|
||||||
|
catch { CurrentFileContent = ""; SqlCode = ""; }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 文件不存在,清空内容但保留文件路径
|
||||||
|
CurrentFileContent = "";
|
||||||
|
SqlCode = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQL 编辑区
|
||||||
|
private string _sqlCode = "";
|
||||||
|
public string SqlCode
|
||||||
|
{
|
||||||
|
get => _sqlCode;
|
||||||
|
set { _sqlCode = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中行详情面板
|
||||||
|
private string _currentFileName = "";
|
||||||
|
public string CurrentFileName
|
||||||
|
{
|
||||||
|
get => _currentFileName;
|
||||||
|
set { _currentFileName = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasCurrentFile)); (SaveCurrentContentCommand as RelayCommand)?.RaiseCanExecuteChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _currentFileContent = "";
|
||||||
|
public string CurrentFileContent
|
||||||
|
{
|
||||||
|
get => _currentFileContent;
|
||||||
|
set { _currentFileContent = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasCurrentFile)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasCurrentFile => !string.IsNullOrEmpty(CurrentFileName);
|
||||||
|
|
||||||
|
// 保存当前文件内容命令
|
||||||
|
public ICommand SaveCurrentContentCommand { get; }
|
||||||
|
|
||||||
|
// 输入框
|
||||||
|
private string _inputText = "";
|
||||||
|
public string InputText
|
||||||
|
{
|
||||||
|
get => _inputText;
|
||||||
|
set { _inputText = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据库连接状态
|
||||||
|
private bool _origConnected;
|
||||||
|
public bool OrigConnected
|
||||||
|
{
|
||||||
|
get => _origConnected;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_origConnected = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
// 触发命令可用性更新
|
||||||
|
(ConnectOrigCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
(DisconnectOrigCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _destConnected;
|
||||||
|
public bool DestConnected
|
||||||
|
{
|
||||||
|
get => _destConnected;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_destConnected = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
(ConnectDestCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
(DisconnectDestCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接中状态
|
||||||
|
private bool _isConnecting;
|
||||||
|
public bool IsConnecting
|
||||||
|
{
|
||||||
|
get => _isConnecting;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isConnecting = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(CanConnect));
|
||||||
|
(ConnectOrigCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
(ConnectDestCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanConnect => !IsConnecting;
|
||||||
|
|
||||||
|
// 连接配置属性(密码改为明文显示,方便用户确认,保存时才加密写入)
|
||||||
|
private string _origIp = "";
|
||||||
|
public string OrigIp { get => _origIp; set { _origIp = value; OnPropertyChanged(); OnPropertyChanged(nameof(OrigConnDisplay)); } }
|
||||||
|
private string _origExam = "";
|
||||||
|
public string OrigExam { get => _origExam; set { _origExam = value; OnPropertyChanged(); OnPropertyChanged(nameof(OrigConnDisplay)); } }
|
||||||
|
private string _origUser = "";
|
||||||
|
public string OrigUser { get => _origUser; set { _origUser = value; OnPropertyChanged(); OnPropertyChanged(nameof(OrigConnDisplay)); } }
|
||||||
|
private string _origPwd = "";
|
||||||
|
public string OrigPwd { get => _origPwd; set { _origPwd = value; OnPropertyChanged(); } }
|
||||||
|
|
||||||
|
private string _destIp = "";
|
||||||
|
public string DestIp { get => _destIp; set { _destIp = value; OnPropertyChanged(); OnPropertyChanged(nameof(DestConnDisplay)); } }
|
||||||
|
private string _destExam = "";
|
||||||
|
public string DestExam { get => _destExam; set { _destExam = value; OnPropertyChanged(); OnPropertyChanged(nameof(DestConnDisplay)); } }
|
||||||
|
private string _destUser = "";
|
||||||
|
public string DestUser { get => _destUser; set { _destUser = value; OnPropertyChanged(); OnPropertyChanged(nameof(DestConnDisplay)); } }
|
||||||
|
private string _destPwd = "";
|
||||||
|
public string DestPwd { get => _destPwd; set { _destPwd = value; OnPropertyChanged(); } }
|
||||||
|
|
||||||
|
public string OrigConnDisplay => string.IsNullOrEmpty(OrigIp) ? "未配置" : $"{OrigIp}/{OrigExam}/{OrigUser}";
|
||||||
|
public string DestConnDisplay => string.IsNullOrEmpty(DestIp) ? "未配置" : $"{DestIp}/{DestExam}/{DestUser}";
|
||||||
|
|
||||||
|
// 密码遮罩显示(用于 TextBox 绑定)
|
||||||
|
public string OrigPwdDisplay => new string('●', OrigPwd?.Length ?? 0);
|
||||||
|
public string DestPwdDisplay => new string('●', DestPwd?.Length ?? 0);
|
||||||
|
|
||||||
|
// 命令
|
||||||
|
public ICommand ConnectOrigCommand { get; }
|
||||||
|
public ICommand ConnectDestCommand { get; }
|
||||||
|
public ICommand SaveOrigCommand { get; }
|
||||||
|
public ICommand SaveDestCommand { get; }
|
||||||
|
public ICommand DisconnectOrigCommand { get; }
|
||||||
|
public ICommand DisconnectDestCommand { get; }
|
||||||
|
public ICommand AddRowCommand { get; }
|
||||||
|
public ICommand OpenDataFolderCommand { get; }
|
||||||
|
public ICommand PreProcessCommand { get; }
|
||||||
|
public ICommand BatchReadCommand { get; }
|
||||||
|
public ICommand BatchWriteCommand { get; }
|
||||||
|
|
||||||
|
// 行操作命令
|
||||||
|
public ICommand ReadCommand { get; }
|
||||||
|
public ICommand WriteCommand { get; }
|
||||||
|
public ICommand DeleteCommand { get; }
|
||||||
|
public ICommand MoveUpCommand { get; }
|
||||||
|
public ICommand MoveDownCommand { get; }
|
||||||
|
public ICommand MoveTopCommand { get; }
|
||||||
|
|
||||||
|
public MainViewModel()
|
||||||
|
{
|
||||||
|
// 从配置加载连接信息
|
||||||
|
OrigIp = SysConfig.GetOrigIp();
|
||||||
|
OrigExam = SysConfig.GetOrigExam();
|
||||||
|
OrigUser = SysConfig.GetOrigUser();
|
||||||
|
OrigPwd = SysConfig.GetOrigPwd();
|
||||||
|
DestIp = SysConfig.GetDestIp();
|
||||||
|
DestExam = SysConfig.GetDestExam();
|
||||||
|
DestUser = SysConfig.GetDestUser();
|
||||||
|
DestPwd = SysConfig.GetDestPwd();
|
||||||
|
|
||||||
|
// 初始化四个 TabServices(索引文件在 from/,内容文件在 data/)
|
||||||
|
TableTab = new TabServices(new TabConfig(TabsEnum.表, CommonHelper.DataFromPath, CommonHelper.DataPath, "table.txt", CodeExt.creat,
|
||||||
|
DbServices.GetTableSql, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
|
BosTab = new TabServices(new TabConfig(TabsEnum.BOS表配置, CommonHelper.DataFromPath, CommonHelper.DataPath, "bos.txt", CodeExt.bos,
|
||||||
|
DbServices.GetTableBos, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
|
PrcTab = new TabServices(new TabConfig(TabsEnum.存储过程, CommonHelper.DataFromPath, CommonHelper.DataPath, "prc.txt", CodeExt.prc,
|
||||||
|
name => DbServices.GetPrcSql(name), (p, c) => SqlCode = c));
|
||||||
|
|
||||||
|
DataTab = new TabServices(new TabConfig(TabsEnum.表数据, CommonHelper.DataFromPath, CommonHelper.DataPath, "data.txt", CodeExt.data,
|
||||||
|
DbServices.GetTableData, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
|
// Tab行点击 -> 显示SQL
|
||||||
|
foreach (var tab in new[] { TableTab, BosTab, PrcTab, DataTab })
|
||||||
|
{
|
||||||
|
tab.RowClickEvent += (path, code) =>
|
||||||
|
{
|
||||||
|
SqlCode = code;
|
||||||
|
CurrentFileName = path;
|
||||||
|
CurrentFileContent = code;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令初始化
|
||||||
|
ConnectOrigCommand = new RelayCommand(async () => await ConnectOrigAsync(), () => !IsConnecting && !OrigConnected);
|
||||||
|
ConnectDestCommand = new RelayCommand(async () => await ConnectDestAsync(), () => !IsConnecting && !DestConnected);
|
||||||
|
SaveOrigCommand = new RelayCommand(SaveOrig);
|
||||||
|
SaveDestCommand = new RelayCommand(SaveDest);
|
||||||
|
DisconnectOrigCommand = new RelayCommand(DisconnectOrig, () => OrigConnected);
|
||||||
|
DisconnectDestCommand = new RelayCommand(DisconnectDest, () => DestConnected);
|
||||||
|
AddRowCommand = new RelayCommand(AddRow, () => CurrentTab != null && !string.IsNullOrWhiteSpace(InputText));
|
||||||
|
OpenDataFolderCommand = new RelayCommand(OpenDataFolder);
|
||||||
|
PreProcessCommand = new RelayCommand(PreProcess);
|
||||||
|
BatchReadCommand = new RelayCommand(async () => await BatchReadAsync());
|
||||||
|
BatchWriteCommand = new RelayCommand(async () => await BatchWriteAsync());
|
||||||
|
|
||||||
|
ReadCommand = new RelayCommand<TableRowModel>(async row => await ReadRowAsync(row));
|
||||||
|
WriteCommand = new RelayCommand<TableRowModel>(async row => await WriteRowAsync(row));
|
||||||
|
DeleteCommand = new RelayCommand<TableRowModel>(DeleteRow);
|
||||||
|
MoveUpCommand = new RelayCommand<TableRowModel>(MoveUp);
|
||||||
|
MoveDownCommand = new RelayCommand<TableRowModel>(MoveDown);
|
||||||
|
MoveTopCommand = new RelayCommand<TableRowModel>(MoveTop);
|
||||||
|
SaveCurrentContentCommand = new RelayCommand(SaveCurrentContent, () => HasCurrentFile);
|
||||||
|
|
||||||
|
// 默认选中第一个Tab
|
||||||
|
CurrentTab = TableTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConnectOrigAsync()
|
||||||
|
{
|
||||||
|
IsConnecting = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SysConfig.SetOrig(OrigIp, OrigExam, OrigUser, OrigPwd);
|
||||||
|
SqlSugarHelper.Connect();
|
||||||
|
OrigConnected = SqlSugarHelper.CheckOrigConnect();
|
||||||
|
if (!OrigConnected) HandyControl.Controls.Growl.Warning("源数据库连接失败,请检查配置");
|
||||||
|
else HandyControl.Controls.Growl.Success("源数据库连接成功");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
OrigConnected = false;
|
||||||
|
HandyControl.Controls.Growl.Warning("源数据库连接失败");
|
||||||
|
}
|
||||||
|
finally { IsConnecting = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConnectDestAsync()
|
||||||
|
{
|
||||||
|
IsConnecting = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SysConfig.SetDest(DestIp, DestExam, DestUser, DestPwd);
|
||||||
|
SqlSugarHelper.Connect();
|
||||||
|
DestConnected = SqlSugarHelper.CheckDescConnect();
|
||||||
|
if (!DestConnected) HandyControl.Controls.Growl.Warning("目标数据库连接失败,请检查配置");
|
||||||
|
else HandyControl.Controls.Growl.Success("目标数据库连接成功");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DestConnected = false;
|
||||||
|
HandyControl.Controls.Growl.Warning("目标数据库连接失败");
|
||||||
|
}
|
||||||
|
finally { IsConnecting = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置(只写 INI,不断开/重连)
|
||||||
|
private void SaveOrig() { SysConfig.SetOrig(OrigIp, OrigExam, OrigUser, OrigPwd); HandyControl.Controls.Growl.Success("源库配置已保存"); }
|
||||||
|
private void SaveDest() { SysConfig.SetDest(DestIp, DestExam, DestUser, DestPwd); HandyControl.Controls.Growl.Success("目标库配置已保存"); }
|
||||||
|
|
||||||
|
// 断开连接(只改状态,不真正关闭 SqlSugarScope,保持实例可用)
|
||||||
|
private void DisconnectOrig()
|
||||||
|
{
|
||||||
|
OrigConnected = false;
|
||||||
|
HandyControl.Controls.Growl.Info("已断开源数据库");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisconnectDest()
|
||||||
|
{
|
||||||
|
DestConnected = false;
|
||||||
|
HandyControl.Controls.Growl.Info("已断开目标数据库");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRow()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(InputText))
|
||||||
|
{
|
||||||
|
CurrentTab?.AddRow(InputText.Trim().ToLower());
|
||||||
|
InputText = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveCurrentContent()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(CurrentFileName)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(CurrentFileName, CurrentFileContent);
|
||||||
|
HandyControl.Controls.Growl.Success("保存成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.Growl.Error($"保存失败:{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenDataFolder()
|
||||||
|
{
|
||||||
|
Process.Start("explorer.exe", CommonHelper.DataPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PreProcess()
|
||||||
|
{
|
||||||
|
try { DbServices.PreProcess(); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BatchReadAsync()
|
||||||
|
{
|
||||||
|
if (CurrentTab == null) return;
|
||||||
|
foreach (var row in CurrentTab.List)
|
||||||
|
await CurrentTab.ReadRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>批量写入断点:记录上次失败行的索引,下次从此处继续</summary>
|
||||||
|
private int _breakpointIndex = -1;
|
||||||
|
public int BreakpointIndex
|
||||||
|
{
|
||||||
|
get => _breakpointIndex;
|
||||||
|
set { _breakpointIndex = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasBreakpoint)); }
|
||||||
|
}
|
||||||
|
public bool HasBreakpoint => _breakpointIndex >= 0;
|
||||||
|
|
||||||
|
private async Task BatchWriteAsync()
|
||||||
|
{
|
||||||
|
if (CurrentTab == null) return;
|
||||||
|
int startIdx = _breakpointIndex >= 0 ? _breakpointIndex : 0;
|
||||||
|
bool hasNewFailure = false;
|
||||||
|
for (int i = startIdx; i < CurrentTab.List.Count; i++)
|
||||||
|
{
|
||||||
|
var row = CurrentTab.List[i];
|
||||||
|
await CurrentTab.WriteRow(row);
|
||||||
|
if (row.WStatus == StatusEnum.写入失败)
|
||||||
|
{
|
||||||
|
_breakpointIndex = i;
|
||||||
|
OnPropertyChanged(nameof(BreakpointIndex));
|
||||||
|
OnPropertyChanged(nameof(HasBreakpoint));
|
||||||
|
hasNewFailure = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 全部成功(或从断点后全部成功)→ 重置断点
|
||||||
|
if (!hasNewFailure && _breakpointIndex >= 0)
|
||||||
|
{
|
||||||
|
_breakpointIndex = -1;
|
||||||
|
OnPropertyChanged(nameof(BreakpointIndex));
|
||||||
|
OnPropertyChanged(nameof(HasBreakpoint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReadRowAsync(TableRowModel? row)
|
||||||
|
{
|
||||||
|
if (row == null || CurrentTab == null) return;
|
||||||
|
await CurrentTab.ReadRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WriteRowAsync(TableRowModel? row)
|
||||||
|
{
|
||||||
|
if (row == null || CurrentTab == null) return;
|
||||||
|
await CurrentTab.WriteRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteRow(TableRowModel? row)
|
||||||
|
{
|
||||||
|
CurrentTab?.DeleteRow(row!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveUp(TableRowModel? row)
|
||||||
|
{
|
||||||
|
CurrentTab?.MoveUp(row!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveDown(TableRowModel? row)
|
||||||
|
{
|
||||||
|
CurrentTab?.MoveDown(row!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveTop(TableRowModel? row)
|
||||||
|
{
|
||||||
|
CurrentTab?.MoveToTop(row!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||||
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单命令(无参数)
|
||||||
|
public class RelayCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly Action _act;
|
||||||
|
private readonly Func<bool>? _can;
|
||||||
|
public RelayCommand(Action act, Func<bool>? can = null) { _act = act; _can = can; }
|
||||||
|
public bool CanExecute(object? p) => _can?.Invoke() ?? true;
|
||||||
|
public void Execute(object? p) => _act();
|
||||||
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单命令(带参数)
|
||||||
|
public class RelayCommand<T> : ICommand
|
||||||
|
{
|
||||||
|
private readonly Action<T?> _act;
|
||||||
|
public RelayCommand(Action<T?> act) { _act = act; }
|
||||||
|
public bool CanExecute(object? p) => true;
|
||||||
|
public void Execute(object? p) => _act((T?)p);
|
||||||
|
#pragma warning disable CS0067
|
||||||
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
#pragma warning restore CS0067
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<hc:Window x:Class="henlo_migration.Views.ConnectionDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
|
Title="修改数据库连接"
|
||||||
|
Height="560" Width="500"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ShowInTaskbar="False"
|
||||||
|
Background="#f6f8fa">
|
||||||
|
|
||||||
|
<Grid Margin="24">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 源数据库 -->
|
||||||
|
<StackPanel Grid.Row="0" Margin="0,0,0,20">
|
||||||
|
<TextBlock Text="源数据库 (Oracle)" FontSize="14" FontWeight="SemiBold"
|
||||||
|
Foreground="#24292e" Margin="0,0,0,10"/>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="80"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="IP / 主机名" VerticalAlignment="Center" Foreground="#57606a"/>
|
||||||
|
<hc:TextBox Grid.Row="0" Grid.Column="1" x:Name="OrigIp" Height="32" VerticalContentAlignment="Center"
|
||||||
|
Margin="0,0,0,4" hc:InfoElement.Placeholder="例如: 192.168.1.100"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="服务名 (SID)" VerticalAlignment="Center" Foreground="#57606a"/>
|
||||||
|
<hc:TextBox Grid.Row="1" Grid.Column="1" x:Name="OrigExam" Height="32" VerticalContentAlignment="Center"
|
||||||
|
Margin="0,0,0,4" hc:InfoElement.Placeholder="例如: orcl"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="用户名" VerticalAlignment="Center" Foreground="#57606a"/>
|
||||||
|
<hc:TextBox Grid.Row="2" Grid.Column="1" x:Name="OrigUser" Height="32" VerticalContentAlignment="Center"
|
||||||
|
Margin="0,0,0,4" hc:InfoElement.Placeholder="例如: system"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="密码" VerticalAlignment="Center" Foreground="#57606a"/>
|
||||||
|
<hc:PasswordBox Grid.Row="3" Grid.Column="1" x:Name="OrigPwd" Height="32" VerticalContentAlignment="Center"
|
||||||
|
ShowEyeButton="True" hc:InfoElement.Placeholder="密码"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 目标数据库 -->
|
||||||
|
<StackPanel Grid.Row="1" Margin="0,0,0,20">
|
||||||
|
<TextBlock Text="目标数据库 (Oracle)" FontSize="14" FontWeight="SemiBold"
|
||||||
|
Foreground="#24292e" Margin="0,0,0,10"/>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="80"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="IP / 主机名" VerticalAlignment="Center" Foreground="#57606a"/>
|
||||||
|
<hc:TextBox Grid.Row="0" Grid.Column="1" x:Name="DestIp" Height="32" VerticalContentAlignment="Center"
|
||||||
|
Margin="0,0,0,4" hc:InfoElement.Placeholder="例如: 192.168.1.100"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="服务名 (SID)" VerticalAlignment="Center" Foreground="#57606a"/>
|
||||||
|
<hc:TextBox Grid.Row="1" Grid.Column="1" x:Name="DestExam" Height="32" VerticalContentAlignment="Center"
|
||||||
|
Margin="0,0,0,4" hc:InfoElement.Placeholder="例如: orcl"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="用户名" VerticalAlignment="Center" Foreground="#57606a"/>
|
||||||
|
<hc:TextBox Grid.Row="2" Grid.Column="1" x:Name="DestUser" Height="32" VerticalContentAlignment="Center"
|
||||||
|
Margin="0,0,0,4" hc:InfoElement.Placeholder="例如: system"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="密码" VerticalAlignment="Center" Foreground="#57606a"/>
|
||||||
|
<hc:PasswordBox Grid.Row="3" Grid.Column="1" x:Name="DestPwd" Height="32" VerticalContentAlignment="Center"
|
||||||
|
ShowEyeButton="True" hc:InfoElement.Placeholder="密码"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 测试连接 -->
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,0,0,16">
|
||||||
|
<Button Content="测试连接" x:Name="BtnTest" Style="{StaticResource ButtonInfo}"
|
||||||
|
Height="32" Padding="16,0" Margin="0,0,10,0" Click="BtnTest_Click"/>
|
||||||
|
<TextBlock x:Name="TestResult" VerticalAlignment="Center" FontSize="12"
|
||||||
|
Foreground="#57606a"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 按钮 -->
|
||||||
|
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="取消" Style="{StaticResource ButtonDefault}"
|
||||||
|
Height="36" Padding="24,0" Margin="0,0,10,0"
|
||||||
|
Click="BtnCancel_Click"/>
|
||||||
|
<Button Content="保存" Style="{StaticResource ButtonPrimary}"
|
||||||
|
Height="36" Padding="24,0"
|
||||||
|
Click="BtnSave_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</hc:Window>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using IniParser;
|
||||||
|
using IniParser.Model;
|
||||||
|
|
||||||
|
namespace henlo_migration.Views
|
||||||
|
{
|
||||||
|
public partial class ConnectionDialog
|
||||||
|
{
|
||||||
|
private readonly string _configPath;
|
||||||
|
private readonly IniData _ini;
|
||||||
|
|
||||||
|
public bool Saved { get; private set; }
|
||||||
|
|
||||||
|
public ConnectionDialog(string ownerPath)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_configPath = Path.Combine(Path.GetDirectoryName(ownerPath)!, "config.ini");
|
||||||
|
|
||||||
|
var parser = new FileIniDataParser();
|
||||||
|
_ini = File.Exists(_configPath) ? parser.ReadFile(_configPath) : new IniData();
|
||||||
|
|
||||||
|
LoadCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadCurrent()
|
||||||
|
{
|
||||||
|
OrigIp.Text = _ini["oracle"]?["orig_ip"] ?? "";
|
||||||
|
OrigExam.Text = _ini["oracle"]?["orig_exam"] ?? "";
|
||||||
|
OrigUser.Text = _ini["oracle"]?["orig_user"] ?? "";
|
||||||
|
OrigPwd.Password = _ini["oracle"]?["orig_password"] ?? "";
|
||||||
|
DestIp.Text = _ini["oracle"]?["desc_ip"] ?? "";
|
||||||
|
DestExam.Text = _ini["oracle"]?["desc_exam"] ?? "";
|
||||||
|
DestUser.Text = _ini["oracle"]?["desc_user"] ?? "";
|
||||||
|
DestPwd.Password = _ini["oracle"]?["desc_password"] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnSave_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_ini.Sections.ContainsSection("oracle"))
|
||||||
|
_ini.Sections.AddSection("oracle");
|
||||||
|
|
||||||
|
var sec = _ini["oracle"];
|
||||||
|
sec["orig_ip"] = OrigIp.Text.Trim();
|
||||||
|
sec["orig_exam"] = OrigExam.Text.Trim();
|
||||||
|
sec["orig_user"] = OrigUser.Text.Trim();
|
||||||
|
sec["orig_password"] = OrigPwd.Password;
|
||||||
|
sec["desc_ip"] = DestIp.Text.Trim();
|
||||||
|
sec["desc_exam"] = DestExam.Text.Trim();
|
||||||
|
sec["desc_user"] = DestUser.Text.Trim();
|
||||||
|
sec["desc_password"] = DestPwd.Password;
|
||||||
|
|
||||||
|
var parser = new FileIniDataParser();
|
||||||
|
parser.WriteFile(_configPath, _ini);
|
||||||
|
|
||||||
|
Saved = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnCancel_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void BtnTest_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
BtnTest.IsEnabled = false;
|
||||||
|
TestResult.Text = "正在测试...";
|
||||||
|
TestResult.Foreground = System.Windows.Media.Brushes.Gray;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var origConn = $"Data Source={OrigIp.Text.Trim()}/{OrigExam.Text.Trim()};User ID={OrigUser.Text.Trim()};Password={OrigPwd.Password}";
|
||||||
|
var destConn = $"Data Source={DestIp.Text.Trim()}/{DestExam.Text.Trim()};User ID={DestUser.Text.Trim()};Password={DestPwd.Password}";
|
||||||
|
|
||||||
|
var (origOk, destOk) = await System.Threading.Tasks.Task.Run(() =>
|
||||||
|
{
|
||||||
|
bool o = false, d = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var odb = new SqlSugar.SqlSugarScope(
|
||||||
|
new System.Collections.Generic.List<SqlSugar.ConnectionConfig>
|
||||||
|
{
|
||||||
|
new SqlSugar.ConnectionConfig { ConfigId = "o", ConnectionString = origConn, DbType = SqlSugar.DbType.Oracle, IsAutoCloseConnection = true }
|
||||||
|
});
|
||||||
|
o = odb.Ado.IsValidConnection();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var ddb = new SqlSugar.SqlSugarScope(
|
||||||
|
new System.Collections.Generic.List<SqlSugar.ConnectionConfig>
|
||||||
|
{
|
||||||
|
new SqlSugar.ConnectionConfig { ConfigId = "d", ConnectionString = destConn, DbType = SqlSugar.DbType.Oracle, IsAutoCloseConnection = true }
|
||||||
|
});
|
||||||
|
d = ddb.Ado.IsValidConnection();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
return (o, d);
|
||||||
|
});
|
||||||
|
|
||||||
|
TestResult.Text = $"源库: {(origOk ? "✓ 成功" : "✗ 失败")} 目标库: {(destOk ? "✓ 成功" : "✗ 失败")}";
|
||||||
|
TestResult.Foreground = origOk && destOk
|
||||||
|
? System.Windows.Media.Brushes.DarkGreen
|
||||||
|
: System.Windows.Media.Brushes.DarkOrange;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TestResult.Text = "测试失败: " + ex.Message;
|
||||||
|
TestResult.Foreground = System.Windows.Media.Brushes.DarkRed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
BtnTest.IsEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<Window x:Class="henlo_migration.Views.ErrorDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
|
Title="错误详情"
|
||||||
|
Height="400" Width="600"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ResizeMode="CanResize">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Text="操作失败,详细信息如下:" FontSize="14" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<TextBox Grid.Row="1" x:Name="ErrorTextBox"
|
||||||
|
IsReadOnly="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
FontFamily="Consolas" FontSize="12"
|
||||||
|
Padding="10"/>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0">
|
||||||
|
<Button Content="复制" Width="80" Height="32" Margin="0,0,10,0"
|
||||||
|
Style="{StaticResource ButtonPrimary}"
|
||||||
|
Click="CopyButton_Click"/>
|
||||||
|
<Button Content="关闭" Width="80" Height="32"
|
||||||
|
Style="{StaticResource ButtonDefault}"
|
||||||
|
IsCancel="True"
|
||||||
|
Click="CloseButton_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace henlo_migration.Views
|
||||||
|
{
|
||||||
|
public partial class ErrorDialog : Window
|
||||||
|
{
|
||||||
|
public string ErrorMessage
|
||||||
|
{
|
||||||
|
get => ErrorTextBox.Text;
|
||||||
|
set => ErrorTextBox.Text = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Clipboard.SetText(ErrorTextBox.Text);
|
||||||
|
HandyControl.Controls.Growl.Success("已复制到剪贴板");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示错误对话框(支持复制)
|
||||||
|
/// </summary>
|
||||||
|
public static void ShowError(string message, Window? owner = null)
|
||||||
|
{
|
||||||
|
var dlg = new ErrorDialog
|
||||||
|
{
|
||||||
|
ErrorMessage = message,
|
||||||
|
Owner = owner ?? Application.Current.MainWindow
|
||||||
|
};
|
||||||
|
dlg.ShowDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
<hc:Window x:Class="henlo_migration.Views.HomeWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
|
xmlns:vm="clr-namespace:henlo_migration.ViewModels"
|
||||||
|
xmlns:b="clr-namespace:henlo_migration.Behaviors"
|
||||||
|
Title="恒诺迁移工具"
|
||||||
|
Height="850" Width="1427"
|
||||||
|
MinHeight="600" MinWidth="1000"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Closing="Window_Closing">
|
||||||
|
|
||||||
|
<hc:Window.Resources>
|
||||||
|
<!-- 选中行时名称列前景色变白,未选中恢复默认蓝 -->
|
||||||
|
<Style x:Key="CopyableNameWhenSelected" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#0969da"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Padding" Value="8,0,0,0"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=DataGridRow}}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- 状态徽章颜色:未读取=灰、读取中=蓝、已读取=绿、读取失败=红 -->
|
||||||
|
<Style x:Key="ReadStatusBadge" TargetType="Border">
|
||||||
|
<Setter Property="CornerRadius" Value="3"/>
|
||||||
|
<Setter Property="Padding" Value="6,0"/>
|
||||||
|
<Setter Property="Height" Value="24"/>
|
||||||
|
<Setter Property="MinHeight" Value="24"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||||
|
<Setter Property="Background" Value="#6c757d"/>
|
||||||
|
</Style>
|
||||||
|
</hc:Window.Resources>
|
||||||
|
|
||||||
|
<Grid DataContext="{StaticResource MainVM}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="16*"/>
|
||||||
|
<ColumnDefinition Width="274*"/>
|
||||||
|
<ColumnDefinition Width="1137*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="55"/>
|
||||||
|
<RowDefinition/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<!-- 顶部:输入框 + 批量操作按钮 -->
|
||||||
|
<Border BorderBrush="#d0d7de" BorderThickness="0,1,0,0" Background="#f6f8fa" Padding="12,8" Grid.ColumnSpan="3">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="300"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 输入框 -->
|
||||||
|
<hc:TextBox Grid.Column="0"
|
||||||
|
hc:InfoElement.Placeholder="输入表名或存储过程名,按回车添加..."
|
||||||
|
Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Height="32" VerticalContentAlignment="Center">
|
||||||
|
<hc:TextBox.InputBindings>
|
||||||
|
<KeyBinding Key="Return" Command="{Binding AddRowCommand}"/>
|
||||||
|
</hc:TextBox.InputBindings>
|
||||||
|
</hc:TextBox>
|
||||||
|
|
||||||
|
<!-- 批量操作按钮(输入框右边,有间隔) -->
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Left">
|
||||||
|
<Button Content="批量读取" Width="80" Height="32" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource ButtonInfo}"
|
||||||
|
Command="{Binding BatchReadCommand}"/>
|
||||||
|
<Button Width="80" Height="32" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource ButtonSuccess}"
|
||||||
|
Command="{Binding BatchWriteCommand}">
|
||||||
|
<Button.Content>
|
||||||
|
<TextBlock>
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="批量写入"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding HasBreakpoint}" Value="True">
|
||||||
|
<Setter Property="Text" Value="继续写入"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Button.Content>
|
||||||
|
</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"
|
||||||
|
Style="{StaticResource ButtonDefault}"
|
||||||
|
Command="{Binding OpenDataFolderCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 主内容:左侧连接配置 + 中间Tab + 右侧详情面板 -->
|
||||||
|
<Grid Grid.Row="1" Margin="12,12,12,5" Cursor="Hand" Grid.ColumnSpan="3">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="0"/>
|
||||||
|
<ColumnDefinition Width="280"/>
|
||||||
|
<ColumnDefinition Width="*" MinWidth="700"/>
|
||||||
|
<ColumnDefinition Width="8"/>
|
||||||
|
<ColumnDefinition Width="500"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 左:连接配置 -->
|
||||||
|
<Border Grid.Column="0" BorderBrush="#d0d7de" BorderThickness="1" CornerRadius="4" Margin="0,0,8,0" Grid.ColumnSpan="2">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="38"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Border Grid.Row="0" Background="#f6f8fa" CornerRadius="4,4,0,0"
|
||||||
|
BorderBrush="#d0d7de" BorderThickness="0,0,0,1">
|
||||||
|
<TextBlock Text="连接配置" FontSize="13" FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center" Margin="12,0" Foreground="#24292e"/>
|
||||||
|
</Border>
|
||||||
|
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="12">
|
||||||
|
<StackPanel>
|
||||||
|
<!-- 源数据库 -->
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="源数据库 (Oracle)" FontSize="12" FontWeight="SemiBold"
|
||||||
|
Foreground="#24292e" Margin="0,8,0,8"/>
|
||||||
|
<Ellipse Width="8" Height="8" Margin="8,0,6,0">
|
||||||
|
<Ellipse.Style>
|
||||||
|
<Style TargetType="Ellipse">
|
||||||
|
<Setter Property="Fill" Value="#f85149"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding OrigConnected}" Value="True">
|
||||||
|
<Setter Property="Fill" Value="#3fb950"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Ellipse.Style>
|
||||||
|
</Ellipse>
|
||||||
|
<TextBlock FontSize="12" VerticalAlignment="Center">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="未连接"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding OrigConnected}" Value="True">
|
||||||
|
<Setter Property="Text" Value="已连接"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="IP / 主机名" FontSize="11" Foreground="#57606a" Margin="0,0,0,2"/>
|
||||||
|
<hc:TextBox Height="30" Margin="0,0,0,6"
|
||||||
|
hc:InfoElement.Placeholder="192.168.1.100"
|
||||||
|
Text="{Binding OrigIp, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<TextBlock Text="服务名 (SID)" FontSize="11" Foreground="#57606a" Margin="0,0,0,2"/>
|
||||||
|
<hc:TextBox Height="30" Margin="0,0,0,6"
|
||||||
|
hc:InfoElement.Placeholder="ORCL"
|
||||||
|
Text="{Binding OrigExam, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<TextBlock Text="用户名" FontSize="11" Foreground="#57606a" Margin="0,0,0,2"/>
|
||||||
|
<hc:TextBox Height="30" Margin="0,0,0,6"
|
||||||
|
hc:InfoElement.Placeholder="system"
|
||||||
|
Text="{Binding OrigUser, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<TextBlock Text="密码" FontSize="11" Foreground="#57606a" Margin="0,0,0,2"/>
|
||||||
|
<hc:TextBox Height="30" Margin="0,0,0,6"
|
||||||
|
hc:InfoElement.Placeholder="输入密码"
|
||||||
|
Text="{Binding OrigPwd, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<!-- 源库按钮 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||||
|
<Button Content="断开" MinWidth="70"
|
||||||
|
Visibility="{Binding OrigConnected, Converter={StaticResource Boolean2VisibilityConverter}}"
|
||||||
|
Style="{StaticResource ButtonDanger}"
|
||||||
|
Command="{Binding DisconnectOrigCommand}"/>
|
||||||
|
<Button MinWidth="70" Margin="0,0,5,0"
|
||||||
|
Style="{StaticResource ButtonPrimary}"
|
||||||
|
Command="{Binding ConnectOrigCommand}">
|
||||||
|
<Button.Content>
|
||||||
|
<TextBlock>
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="连接"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsConnecting}" Value="True">
|
||||||
|
<Setter Property="Text" Value="连接中..."/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Button.Content>
|
||||||
|
</Button>
|
||||||
|
<Button Content="保存" MinWidth="70"
|
||||||
|
Style="{StaticResource ButtonSuccess}"
|
||||||
|
Command="{Binding SaveOrigCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 分隔线 -->
|
||||||
|
<Border Height="1" Background="#d0d7de" Margin="0,12"/>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="目标数据库 (Oracle)" FontSize="12" FontWeight="SemiBold"
|
||||||
|
Foreground="#24292e" Margin="0,8,0,8"/>
|
||||||
|
<Ellipse Width="8" Height="8" Margin="8,0,6,0">
|
||||||
|
<Ellipse.Style>
|
||||||
|
<Style TargetType="Ellipse">
|
||||||
|
<Setter Property="Fill" Value="#f85149"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding DestConnected}" Value="True">
|
||||||
|
<Setter Property="Fill" Value="#3fb950"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Ellipse.Style>
|
||||||
|
</Ellipse>
|
||||||
|
<TextBlock FontSize="12" VerticalAlignment="Center">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="未连接"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding DestConnected}" Value="True">
|
||||||
|
<Setter Property="Text" Value="已连接"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="IP / 主机名" FontSize="11" Foreground="#57606a" Margin="0,0,0,2"/>
|
||||||
|
<hc:TextBox Height="30" Margin="0,0,0,6"
|
||||||
|
hc:InfoElement.Placeholder="192.168.1.101"
|
||||||
|
Text="{Binding DestIp, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<TextBlock Text="服务名 (SID)" FontSize="11" Foreground="#57606a" Margin="0,0,0,2"/>
|
||||||
|
<hc:TextBox Height="30" Margin="0,0,0,6"
|
||||||
|
hc:InfoElement.Placeholder="ORCL"
|
||||||
|
Text="{Binding DestExam, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<TextBlock Text="用户名" FontSize="11" Foreground="#57606a" Margin="0,0,0,2"/>
|
||||||
|
<hc:TextBox Height="30" Margin="0,0,0,6"
|
||||||
|
hc:InfoElement.Placeholder="system"
|
||||||
|
Text="{Binding DestUser, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<TextBlock Text="密码" FontSize="11" Foreground="#57606a" Margin="0,0,0,2"/>
|
||||||
|
<hc:TextBox Height="30" Margin="0,0,0,6"
|
||||||
|
hc:InfoElement.Placeholder="输入密码"
|
||||||
|
Text="{Binding DestPwd, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<!-- 目标库按钮 -->
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||||
|
<Button Content="断开" MinWidth="70"
|
||||||
|
Visibility="{Binding DestConnected, Converter={StaticResource Boolean2VisibilityConverter}}"
|
||||||
|
Style="{StaticResource ButtonDanger}"
|
||||||
|
Command="{Binding DisconnectDestCommand}"/>
|
||||||
|
<Button MinWidth="70" Margin="0,0,5,0"
|
||||||
|
Style="{StaticResource ButtonSuccess}"
|
||||||
|
Command="{Binding ConnectDestCommand}">
|
||||||
|
<Button.Content>
|
||||||
|
<TextBlock>
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="连接"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsConnecting}" Value="True">
|
||||||
|
<Setter Property="Text" Value="连接中..."/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Button.Content>
|
||||||
|
</Button>
|
||||||
|
<Button Content="保存" MinWidth="70"
|
||||||
|
Style="{StaticResource ButtonSuccess}"
|
||||||
|
Command="{Binding SaveDestCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 中:TabControl -->
|
||||||
|
<TabControl Grid.Column="2" SelectedIndex="{Binding SelectedTabIndex}">
|
||||||
|
|
||||||
|
<!-- 表配置 -->
|
||||||
|
<TabItem Header="表配置">
|
||||||
|
<DataGrid x:Name="TablesGrid" ItemsSource="{Binding TablesData}"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- BOS配置 -->
|
||||||
|
<TabItem Header="BOS配置">
|
||||||
|
<DataGrid x:Name="BosGrid" ItemsSource="{Binding BosTablesData}"
|
||||||
|
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="存储过程">
|
||||||
|
<DataGrid x:Name="PrcGrid" ItemsSource="{Binding StoredProcsData}"
|
||||||
|
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="200">
|
||||||
|
<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="表数据">
|
||||||
|
<DataGrid x:Name="DataGrid4" ItemsSource="{Binding CurrentDataData}"
|
||||||
|
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>
|
||||||
|
</TabControl>
|
||||||
|
|
||||||
|
<!-- 分隔条 -->
|
||||||
|
<GridSplitter Grid.Column="3" Width="8" HorizontalAlignment="Stretch"
|
||||||
|
Background="Transparent" Cursor="SizeWE"/>
|
||||||
|
|
||||||
|
<!-- 右:选中行详情面板 -->
|
||||||
|
<Border Grid.Column="4" BorderBrush="#d0d7de" BorderThickness="1" CornerRadius="4" Margin="0,0,85,0">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="38"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<!-- 标题栏 -->
|
||||||
|
<Border Grid.Row="0" Background="#f6f8fa" CornerRadius="4,4,0,0"
|
||||||
|
BorderBrush="#d0d7de" BorderThickness="0,0,0,1">
|
||||||
|
<Grid Margin="12,0">
|
||||||
|
<TextBlock Text="代码详情" FontSize="13" FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center" Foreground="#24292e"/>
|
||||||
|
<TextBlock Text="{Binding CurrentFileName}" FontSize="11" Foreground="#57606a"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Right"
|
||||||
|
TextTrimming="CharacterEllipsis" MaxWidth="300"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<!-- 文本编辑区 -->
|
||||||
|
<TextBox Grid.Row="1"
|
||||||
|
Text="{Binding CurrentFileContent, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
AcceptsReturn="True" AcceptsTab="True"
|
||||||
|
TextWrapping="NoWrap"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
FontFamily="Consolas" FontSize="13"
|
||||||
|
BorderThickness="0" Padding="12"
|
||||||
|
hc:InfoElement.Placeholder="点击表格「读取」按钮获取代码..."/>
|
||||||
|
<!-- 保存按钮 -->
|
||||||
|
<Border Grid.Row="2" Background="#f6f8fa" CornerRadius="0,0,4,4"
|
||||||
|
BorderBrush="#d0d7de" BorderThickness="0,1,0,0"
|
||||||
|
Padding="12,8" Width="413" HorizontalAlignment="Center">
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="检查更新" Width="80" Height="32" Margin="0,0,12,0"
|
||||||
|
Click="CheckUpdate_Click" VerticalAlignment="Center"/>
|
||||||
|
<Button Content="保存" Width="80" Height="32"
|
||||||
|
Style="{StaticResource ButtonPrimary}"
|
||||||
|
Command="{Binding SaveCurrentContentCommand}"
|
||||||
|
IsEnabled="{Binding HasCurrentFile}" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</hc:Window>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using henlo_migration.Common;
|
||||||
|
|
||||||
|
namespace henlo_migration.Views
|
||||||
|
{
|
||||||
|
public partial class HomeWindow : HandyControl.Controls.Window
|
||||||
|
{
|
||||||
|
public HomeWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
// 窗口标题显示版本号
|
||||||
|
string version = SysConfig.GetVersion();
|
||||||
|
if (!string.IsNullOrEmpty(version))
|
||||||
|
Title = $"恒诺迁移工具 v{version}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭窗口时断开连接并终止程序
|
||||||
|
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||||
|
{
|
||||||
|
try { SqlSugarHelper.Dbs?.Dispose(); } catch { }
|
||||||
|
System.Environment.Exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void CheckUpdate_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
await ((App)Application.Current).CheckUpdateManualAsync(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,54 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<RootNamespace>henlo_migration</RootNamespace>
|
||||||
|
<AssemblyName>恒诺迁移工具</AssemblyName>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
|
<ApplicationIcon>Resources\favicon.ico</ApplicationIcon>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<Version>1.0.16</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="HandyControl" Version="3.5.1" />
|
||||||
|
<PackageReference Include="SqlSugar" Version="5.1.4.157" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.5.0" />
|
||||||
|
<PackageReference Include="ini-parser" Version="2.5.2" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
|
||||||
|
<PackageReference Include="Velopack" Version="0.0.1298" />
|
||||||
|
</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">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="pre\*.fnc">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="pre\*.prc">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Resource Include="Resources\favicon.ico" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 18
|
||||||
|
VisualStudioVersion = 18.4.11626.88 stable
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "henlo_migration_wpf10", "henlo_migration_wpf10.csproj", "{57155DB9-E3BA-4517-23FF-D15A898A4FBA}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{57155DB9-E3BA-4517-23FF-D15A898A4FBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{57155DB9-E3BA-4517-23FF-D15A898A4FBA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{57155DB9-E3BA-4517-23FF-D15A898A4FBA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{57155DB9-E3BA-4517-23FF-D15A898A4FBA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {07691456-001F-4548-8FEC-9796F7980920}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
REM Velopack pack script (portable mode, no installer)
|
||||||
|
REM Reads <Version> from csproj automatically
|
||||||
|
|
||||||
|
set "PROJECT_DIR=%~dp0"
|
||||||
|
set "PUBLISH_DIR=%PROJECT_DIR%publish"
|
||||||
|
set "RELEASE_DIR=%PROJECT_DIR%releases"
|
||||||
|
set "CSPROJ=%PROJECT_DIR%henlo_migration_wpf10.csproj"
|
||||||
|
|
||||||
|
REM Read <Version> from csproj via PowerShell (handles UTF-8)
|
||||||
|
for /f "usebackq delims=" %%a in (`powershell -NoProfile -Command "[regex]::Match(([System.IO.File]::ReadAllText('%CSPROJ%', [System.Text.Encoding]::UTF8)), '<Version>(.+?)</Version>').Groups[1].Value"`) do set "VERSION=%%a"
|
||||||
|
|
||||||
|
if "!VERSION!"=="" (
|
||||||
|
echo ERROR: Cannot read version from csproj
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Henlo Migration Tool - Velopack Pack
|
||||||
|
echo Version: !VERSION!
|
||||||
|
echo ========================================
|
||||||
|
|
||||||
|
echo [1/3] Cleanup publish dir...
|
||||||
|
if exist "!PUBLISH_DIR!" rmdir /s /q "!PUBLISH_DIR!"
|
||||||
|
|
||||||
|
echo [2/3] dotnet publish...
|
||||||
|
dotnet publish "%CSPROJ%" -c Release -o "!PUBLISH_DIR!" --self-contained false -r win-x64
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: dotnet publish failed!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2.5/3] Create data directories in publish (user runtime data)...
|
||||||
|
if not exist "!PUBLISH_DIR!\data" mkdir "!PUBLISH_DIR!\data"
|
||||||
|
echo. 2>nul > "!PUBLISH_DIR!\data\.gitkeep"
|
||||||
|
if not exist "!PUBLISH_DIR!\data\from" mkdir "!PUBLISH_DIR!\data\from"
|
||||||
|
echo. 2>nul > "!PUBLISH_DIR!\data\from\.gitkeep"
|
||||||
|
if not exist "!PUBLISH_DIR!\pre" mkdir "!PUBLISH_DIR!\pre"
|
||||||
|
echo. 2>nul > "!PUBLISH_DIR!\pre\.gitkeep"
|
||||||
|
|
||||||
|
echo [3/3] vpk pack (--noInst)...
|
||||||
|
vpk pack -u "henlo_migration" -v "!VERSION!" -p "!PUBLISH_DIR!" -e "恒诺迁移工具.exe" -o "!RELEASE_DIR!" --noInst
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: vpk pack failed!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Pack complete!
|
||||||
|
echo Output:
|
||||||
|
echo henlo_migration-win-Portable.zip
|
||||||
|
echo henlo_migration-!VERSION!-full.nupkg
|
||||||
|
echo RELEASES
|
||||||
|
echo Server deploy to:
|
||||||
|
echo http://zjhenlo.henlo.net:8878/henlo_migration/auto_update/
|
||||||
|
echo ========================================
|
||||||
|
|
||||||
|
endlocal
|
||||||
|
pause
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION get_insert_sql(v_table_name IN VARCHAR2,
|
||||||
|
p_id IN NUMBER) RETURN VARCHAR2 AS
|
||||||
|
v_columns VARCHAR2(4000);
|
||||||
|
v_values VARCHAR2(4000);
|
||||||
|
v_value VARCHAR2(100);
|
||||||
|
v_sql VARCHAR2(4000);
|
||||||
|
BEGIN
|
||||||
|
-- 获取表的列名
|
||||||
|
FOR v IN (SELECT column_name, data_type
|
||||||
|
FROM user_tab_columns
|
||||||
|
WHERE table_name = upper(v_table_name)
|
||||||
|
ORDER BY column_id) LOOP
|
||||||
|
-- 构建值部分(示例,实际根据需求填充)
|
||||||
|
CASE
|
||||||
|
WHEN v.column_name IN ('ID') THEN
|
||||||
|
v_values := v_values || ''',''||' || v.column_name || '||';
|
||||||
|
WHEN v.data_type IN ('DATE') THEN
|
||||||
|
v_values := v_values || ''',''''@@''';
|
||||||
|
ELSE
|
||||||
|
v_values := v_values || ''',''''''||' || v.column_name || '||''''';
|
||||||
|
END CASE;
|
||||||
|
v_columns := v_columns || ',' || v.column_name;
|
||||||
|
END LOOP;
|
||||||
|
v_columns := substr(v_columns, 2);
|
||||||
|
v_values := substr(v_values, 6) || '''''';
|
||||||
|
EXECUTE IMMEDIATE 'select ' || v_values || ' from ' || v_table_name || ' where id=:1'
|
||||||
|
INTO v_values
|
||||||
|
USING p_id;
|
||||||
|
--v_values := REPLACE(v_values, '''', '''''');
|
||||||
|
v_sql := 'INSERT INTO ' || v_table_name || '(' || v_columns || ') values(' || v_values || ');';
|
||||||
|
RETURN REPLACE(v_sql, '''@@''', 'sysdate');
|
||||||
|
END;
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
-- Oracle 11g 兼容的存储过程:生成表的 INSERT 语句
|
||||||
|
-- 使用 DBMS_XMLGEN 实现,支持任意表结构
|
||||||
|
-- 参数:p_table_name 表名,p_result 输出 INSERT 语句(CLOB)
|
||||||
|
-- 限制:行数超过 200 行报错
|
||||||
|
|
||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_insert_sql(
|
||||||
|
p_table_name IN VARCHAR2,
|
||||||
|
p_result OUT CLOB
|
||||||
|
) AS
|
||||||
|
v_count NUMBER;
|
||||||
|
v_table_upper VARCHAR2(128);
|
||||||
|
v_ctx NUMBER;
|
||||||
|
v_xml CLOB;
|
||||||
|
v_sql VARCHAR2(32767);
|
||||||
|
v_col_names DBMS_SQL.VARCHAR2A;
|
||||||
|
v_col_count NUMBER;
|
||||||
|
|
||||||
|
-- 解析 XML
|
||||||
|
v_row_xml XMLTYPE;
|
||||||
|
v_col_val VARCHAR2(4000);
|
||||||
|
v_insert_line VARCHAR2(32767);
|
||||||
|
v_cols_part VARCHAR2(32767);
|
||||||
|
v_vals_part VARCHAR2(32767);
|
||||||
|
v_col_idx NUMBER;
|
||||||
|
v_row_idx NUMBER;
|
||||||
|
v_rows XMLTYPE;
|
||||||
|
v_row_node XMLTYPE;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
v_table_upper := UPPER(TRIM(p_table_name));
|
||||||
|
DBMS_LOB.createtemporary(p_result, TRUE);
|
||||||
|
|
||||||
|
-- 检查表是否存在
|
||||||
|
SELECT COUNT(*) INTO v_count
|
||||||
|
FROM user_tables
|
||||||
|
WHERE table_name = v_table_upper;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 ' || v_table_upper || ' 不存在');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 检查行数限制
|
||||||
|
v_sql := 'SELECT COUNT(*) FROM ' || v_table_upper;
|
||||||
|
EXECUTE IMMEDIATE v_sql INTO v_count;
|
||||||
|
|
||||||
|
IF v_count > 200 THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 ' || v_table_upper || ' 数据行数(' || v_count || ')超过200行限制');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '-- 表 ' || v_table_upper || ' 无数据');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 头部
|
||||||
|
DBMS_LOB.append(p_result, '-- 表 ' || v_table_upper || ' 共 ' || v_count || ' 行' || CHR(10));
|
||||||
|
|
||||||
|
-- 获取列名列表
|
||||||
|
SELECT column_name
|
||||||
|
BULK COLLECT INTO v_col_names
|
||||||
|
FROM user_tab_columns
|
||||||
|
WHERE table_name = v_table_upper
|
||||||
|
ORDER BY column_id;
|
||||||
|
|
||||||
|
v_col_count := v_col_names.COUNT;
|
||||||
|
|
||||||
|
-- 构建列名部分
|
||||||
|
v_cols_part := '';
|
||||||
|
FOR v_col_idx IN 1..v_col_count LOOP
|
||||||
|
IF v_col_idx > 1 THEN v_cols_part := v_cols_part || ', '; END IF;
|
||||||
|
v_cols_part := v_cols_part || v_col_names(v_col_idx);
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- 用 DBMS_XMLGEN 获取数据为 XML
|
||||||
|
v_sql := 'SELECT * FROM ' || v_table_upper;
|
||||||
|
v_ctx := DBMS_XMLGEN.NEWCONTEXT(v_sql);
|
||||||
|
DBMS_XMLGEN.SETROWSETTAG(v_ctx, 'ROWSET');
|
||||||
|
DBMS_XMLGEN.SETROWTAG(v_ctx, 'ROW');
|
||||||
|
v_xml := DBMS_XMLGEN.GETXML(v_ctx);
|
||||||
|
DBMS_XMLGEN.CLOSECONTEXT(v_ctx);
|
||||||
|
|
||||||
|
-- 解析 XML 生成 INSERT 语句
|
||||||
|
v_rows := XMLTYPE(v_xml);
|
||||||
|
|
||||||
|
-- 遍历每个 ROW
|
||||||
|
FOR v_row_idx IN 1..v_count LOOP
|
||||||
|
v_row_node := v_rows.extract('/ROWSET/ROW[' || v_row_idx || ']');
|
||||||
|
|
||||||
|
IF v_row_node IS NOT NULL THEN
|
||||||
|
v_vals_part := '';
|
||||||
|
|
||||||
|
FOR v_col_idx IN 1..v_col_count LOOP
|
||||||
|
IF v_col_idx > 1 THEN v_vals_part := v_vals_part || ', '; END IF;
|
||||||
|
|
||||||
|
-- 提取列值
|
||||||
|
BEGIN
|
||||||
|
v_col_val := v_row_node.extract('//ROW/' || v_col_names(v_col_idx) || '/text()').getStringVal();
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
v_col_val := NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 处理 NULL 和字符串转义
|
||||||
|
IF v_col_val IS NULL THEN
|
||||||
|
v_vals_part := v_vals_part || 'NULL';
|
||||||
|
ELSE
|
||||||
|
-- 转义单引号
|
||||||
|
v_col_val := REPLACE(v_col_val, '''', '''''');
|
||||||
|
v_vals_part := v_vals_part || '''' || v_col_val || '''';
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- 生成 INSERT 语句
|
||||||
|
v_insert_line := 'INSERT INTO ' || v_table_upper || ' (' || v_cols_part || ') VALUES (' || v_vals_part || ');' || CHR(10);
|
||||||
|
DBMS_LOB.append(p_result, v_insert_line);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
DBMS_LOB.append(p_result, 'COMMIT;' || CHR(10));
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: ' || SQLERRM);
|
||||||
|
END;
|
||||||
|
/
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_prc_all(p_name IN VARCHAR2,
|
||||||
|
r_str OUT CLOB) AS
|
||||||
|
/*
|
||||||
|
»ñÈ¡±íºÍ±íÊý¾Ý£¬·µ»Ø
|
||||||
|
*/
|
||||||
|
BEGIN
|
||||||
|
SELECT rtrim(xmlagg(xmlelement(e, text, '').extract('//text()') ORDER BY line)
|
||||||
|
.getclobval(), ',')
|
||||||
|
INTO r_str
|
||||||
|
FROM (SELECT text, line
|
||||||
|
FROM user_source
|
||||||
|
WHERE NAME = upper(p_name));
|
||||||
|
r_str := REPLACE(r_str, '&apos;', '''');
|
||||||
|
r_str := REPLACE(r_str, '<', '<');
|
||||||
|
r_str := REPLACE(r_str, '>', '>');
|
||||||
|
r_str := REPLACE(r_str, '&gt;', '>');
|
||||||
|
r_str := REPLACE(r_str, '&lt;', '<');
|
||||||
|
r_str := REPLACE(r_str, '<>', '!=');
|
||||||
|
/* v_str := REPLACE(v_str, '<=', '<=');
|
||||||
|
v_str := REPLACE(v_str, '>=', '>=');*/
|
||||||
|
r_str := REPLACE(r_str, ''', '''');
|
||||||
|
r_str := REPLACE(r_str, '"', '"');
|
||||||
|
r_str := REPLACE(r_str, '<', '<');
|
||||||
|
r_str := REPLACE(r_str, 'gt;', '>');
|
||||||
|
r_str := 'CREATE OR REPLACE ' || r_str;
|
||||||
|
END;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_table_bos(p_name IN VARCHAR2,
|
||||||
|
r_id OUT CLOB) AS
|
||||||
|
/*
|
||||||
|
获取表和表数据,返回
|
||||||
|
*/
|
||||||
|
v_id NUMBER;
|
||||||
|
v_code NUMBER(10);
|
||||||
|
v_msg VARCHAR2(1000);
|
||||||
|
TYPE v_type IS RECORD(
|
||||||
|
tsql VARCHAR2(4000));
|
||||||
|
TYPE mybrray1 IS TABLE OF v_type;
|
||||||
|
v_sql_list mybrray1;
|
||||||
|
v_sql CLOB;
|
||||||
|
BEGIN
|
||||||
|
SELECT id
|
||||||
|
INTO r_id
|
||||||
|
FROM ad_table t
|
||||||
|
WHERE t.name = upper(p_name);
|
||||||
|
-- 获取bos表结构
|
||||||
|
ad_table_text(893, '<id>' || r_id || '</id>', v_code, v_msg);
|
||||||
|
--r_table_sql := empty_clob();
|
||||||
|
/*SELECT tabletext
|
||||||
|
INTO v_sql
|
||||||
|
FROM t_tabletext t
|
||||||
|
WHERE t.id = v_id;
|
||||||
|
--r_table_sql := v_sql;
|
||||||
|
dbms_lob.createtemporary(r_table_sql, TRUE);
|
||||||
|
dbms_lob.append(r_table_sql, v_sql);*/
|
||||||
|
/*-- 获取表数据结构
|
||||||
|
v_sql := 'select get_insert_sql(''' || p_name || ''',id) tsql from ' || p_name;
|
||||||
|
EXECUTE IMMEDIATE v_sql BULK COLLECT
|
||||||
|
INTO v_sql_list;
|
||||||
|
v_table_data := '';
|
||||||
|
FOR i IN 1 .. v_sql_list.count LOOP
|
||||||
|
v_table_data := v_table_data || v_sql_list(i).tsql;
|
||||||
|
--dbms_output.put_line(v_sql_list(i).tsql);
|
||||||
|
END LOOP;*/
|
||||||
|
-- RAISE_APPLICATION_ERROR(-20201, V_MESSAGE);
|
||||||
|
END;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_table_bos_31(p_name IN VARCHAR2,
|
||||||
|
r_id OUT number,
|
||||||
|
r_table out varchar2) AS
|
||||||
|
v_code NUMBER;
|
||||||
|
v_msg VARCHAR2(4000);
|
||||||
|
v_sql VARCHAR2(4000);
|
||||||
|
v_proc_name VARCHAR2(100);
|
||||||
|
v_result CLOB;
|
||||||
|
v_temp_clob CLOB;
|
||||||
|
-- 兼容bos31的版本
|
||||||
|
BEGIN
|
||||||
|
-- 初始化返回结果LL;
|
||||||
|
-- 查询表id
|
||||||
|
begin
|
||||||
|
SELECT id INTO r_id FROM ad_table t WHERE t.name = upper(p_name);
|
||||||
|
exception
|
||||||
|
when no_data_found then
|
||||||
|
raise_application_error(-20201, '表' || p_name || '不存在');
|
||||||
|
end;
|
||||||
|
r_table := 't_tabletext';
|
||||||
|
|
||||||
|
-- 根据p_name参数决定调用哪个存储过程ext' THEN
|
||||||
|
v_proc_name := 'AD_TABLE_TEXT';
|
||||||
|
|
||||||
|
-- 检查存储过程是否存在
|
||||||
|
BEGIN
|
||||||
|
EXECUTE IMMEDIATE 'BEGIN ' || v_proc_name || '(:1, :2, :3, :4); END;'
|
||||||
|
USING 893, '<id>' || r_id || '</id>', OUT v_code, OUT v_msg;
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
-- 如果ad_table_text不存在或执行失败,尝试ad_table_sql_exp
|
||||||
|
IF SQLCODE = -06550 OR SQLCODE = -04043 THEN
|
||||||
|
-- ORA-06550: PLS-00201, ORA-04043: object does not exist
|
||||||
|
BEGIN
|
||||||
|
v_proc_name := 'AD_TABLE_SQL_EXP';
|
||||||
|
EXECUTE IMMEDIATE 'BEGIN ' || v_proc_name ||
|
||||||
|
'(:1, :2, :3, :4); END;'
|
||||||
|
USING 893, '<id>' || r_id || '</id>', OUT v_code, OUT v_msg;
|
||||||
|
r_table := 'ad_tablesql';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
raise_application_error(-20201,
|
||||||
|
'查找bos表配置失败,hmig_get_table_bos_31');
|
||||||
|
END;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
commit;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
raise_application_error(-20201,
|
||||||
|
'查找bos表配置失败,hmig_get_table_bos_31' || SQLERRM);
|
||||||
|
END;
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_table_sql(p_name IN VARCHAR2,
|
||||||
|
r_id OUT VARCHAR2,
|
||||||
|
r_table OUT CLOB) AS
|
||||||
|
v_spacename VARCHAR2(100);
|
||||||
|
v_ddl CLOB;
|
||||||
|
v_table_upper VARCHAR2(128);
|
||||||
|
|
||||||
|
-- 安全拼接:直接用 || 操作符,Oracle 会自动处理 CLOB 扩展
|
||||||
|
PROCEDURE append_line(p_text IN VARCHAR2) IS
|
||||||
|
BEGIN
|
||||||
|
IF p_text IS NOT NULL THEN
|
||||||
|
r_table := r_table || p_text;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
v_table_upper := UPPER(TRIM(p_name));
|
||||||
|
r_id := v_table_upper;
|
||||||
|
|
||||||
|
-- 初始化为空 CLOB(用空字符串赋值,避免 NULL LOB locator 问题)
|
||||||
|
r_table := EMPTY_CLOB();
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- 1. 关闭不需要的 DDL 附加属性
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,
|
||||||
|
'STORAGE',
|
||||||
|
FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,
|
||||||
|
'TABLESPACE',
|
||||||
|
FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,
|
||||||
|
'SEGMENT_ATTRIBUTES',
|
||||||
|
FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,
|
||||||
|
'CONSTRAINTS',
|
||||||
|
FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,
|
||||||
|
'REF_CONSTRAINTS',
|
||||||
|
FALSE);
|
||||||
|
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,
|
||||||
|
'CONSTRAINTS_AS_ALTER',
|
||||||
|
TRUE);
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- 2. 建表语句
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
SELECT DBMS_METADATA.GET_DDL('TABLE', v_table_upper)
|
||||||
|
INTO v_ddl
|
||||||
|
FROM DUAL;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
SELECT tablespace_name
|
||||||
|
INTO v_spacename
|
||||||
|
FROM user_tables
|
||||||
|
WHERE table_name = v_table_upper;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN NO_DATA_FOUND THEN
|
||||||
|
v_spacename := NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF v_spacename IS NOT NULL THEN
|
||||||
|
v_ddl := REPLACE(v_ddl, '"' || v_spacename || '".', '');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
r_table := r_table || v_ddl || CHR(10) || ';' || CHR(10) || CHR(10);
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- 3. 表注释
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
FOR rec IN (SELECT comments
|
||||||
|
FROM user_tab_comments
|
||||||
|
WHERE table_name = v_table_upper
|
||||||
|
AND comments IS NOT NULL) LOOP
|
||||||
|
append_line('COMMENT ON TABLE ' || v_table_upper || ' IS ''' ||
|
||||||
|
REPLACE(rec.comments, '''', '''''') || ''';' || CHR(10));
|
||||||
|
END LOOP;
|
||||||
|
append_line(CHR(10));
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- 4. 字段注释
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
FOR rec IN (SELECT column_name, comments
|
||||||
|
FROM user_col_comments
|
||||||
|
WHERE table_name = v_table_upper
|
||||||
|
AND comments IS NOT NULL
|
||||||
|
ORDER BY column_name) LOOP
|
||||||
|
append_line('COMMENT ON COLUMN ' || v_table_upper || '.' ||
|
||||||
|
rec.column_name || ' IS ''' ||
|
||||||
|
REPLACE(rec.comments, '''', '''''') || ''';' || CHR(10));
|
||||||
|
END LOOP;
|
||||||
|
append_line(CHR(10));
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- 5. 主键 & 唯一键
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
FOR rec IN (SELECT c.constraint_name,
|
||||||
|
c.constraint_type,
|
||||||
|
LISTAGG(cc.column_name, ', ') WITHIN GROUP(ORDER BY cc.position) AS cols
|
||||||
|
FROM user_constraints c
|
||||||
|
JOIN user_cons_columns cc
|
||||||
|
ON cc.constraint_name = c.constraint_name
|
||||||
|
WHERE c.table_name = v_table_upper
|
||||||
|
AND c.constraint_type IN ('P', 'U')
|
||||||
|
GROUP BY c.constraint_name, c.constraint_type
|
||||||
|
ORDER BY c.constraint_type, c.constraint_name) LOOP
|
||||||
|
IF rec.constraint_type = 'P' THEN
|
||||||
|
append_line('ALTER TABLE ' || v_table_upper || ' ADD CONSTRAINT ' ||
|
||||||
|
rec.constraint_name || ' PRIMARY KEY (' || rec.cols || ');' ||
|
||||||
|
CHR(10));
|
||||||
|
ELSE
|
||||||
|
append_line('ALTER TABLE ' || v_table_upper || ' ADD CONSTRAINT ' ||
|
||||||
|
rec.constraint_name || ' UNIQUE (' || rec.cols || ');' ||
|
||||||
|
CHR(10));
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
append_line(CHR(10));
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- 6. 外键
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
FOR rec IN (SELECT c.constraint_name,
|
||||||
|
c.r_constraint_name,
|
||||||
|
c.delete_rule,
|
||||||
|
LISTAGG(cc.column_name, ', ') WITHIN GROUP(ORDER BY cc.position) AS cols
|
||||||
|
FROM user_constraints c
|
||||||
|
JOIN user_cons_columns cc
|
||||||
|
ON cc.constraint_name = c.constraint_name
|
||||||
|
WHERE c.table_name = v_table_upper
|
||||||
|
AND c.constraint_type = 'R'
|
||||||
|
GROUP BY c.constraint_name,
|
||||||
|
c.r_constraint_name,
|
||||||
|
c.delete_rule
|
||||||
|
ORDER BY c.constraint_name) LOOP
|
||||||
|
DECLARE
|
||||||
|
v_ref_table VARCHAR2(128);
|
||||||
|
v_ref_cols VARCHAR2(4000);
|
||||||
|
BEGIN
|
||||||
|
SELECT rc.table_name,
|
||||||
|
LISTAGG(rcc.column_name, ', ') WITHIN GROUP(ORDER BY rcc.position)
|
||||||
|
INTO v_ref_table, v_ref_cols
|
||||||
|
FROM user_constraints rc
|
||||||
|
JOIN user_cons_columns rcc
|
||||||
|
ON rcc.constraint_name = rc.constraint_name
|
||||||
|
WHERE rc.constraint_name = rec.r_constraint_name
|
||||||
|
GROUP BY rc.table_name;
|
||||||
|
|
||||||
|
append_line('ALTER TABLE ' || v_table_upper || ' ADD CONSTRAINT ' ||
|
||||||
|
rec.constraint_name || ' FOREIGN KEY (' || rec.cols || ')' ||
|
||||||
|
' REFERENCES ' || v_ref_table || ' (' || v_ref_cols || ')' || CASE
|
||||||
|
rec.delete_rule WHEN 'CASCADE' THEN ' ON DELETE CASCADE' WHEN
|
||||||
|
'SET NULL' THEN ' ON DELETE SET NULL' ELSE ''
|
||||||
|
END || ';' || CHR(10));
|
||||||
|
EXCEPTION
|
||||||
|
WHEN NO_DATA_FOUND THEN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
END LOOP;
|
||||||
|
append_line(CHR(10));
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
-- 7. 普通索引(排除主键/唯一键隐式创建的索引)
|
||||||
|
-- ----------------------------------------------------------------
|
||||||
|
FOR rec IN (SELECT i.index_name,
|
||||||
|
i.uniqueness,
|
||||||
|
LISTAGG(ic.column_name, ', ') WITHIN GROUP(ORDER BY ic.column_position) AS cols
|
||||||
|
FROM user_indexes i
|
||||||
|
JOIN user_ind_columns ic
|
||||||
|
ON ic.index_name = i.index_name
|
||||||
|
WHERE i.table_name = v_table_upper
|
||||||
|
AND i.index_type IN ('NORMAL', 'FUNCTION-BASED NORMAL')
|
||||||
|
AND NOT EXISTS
|
||||||
|
(SELECT 1
|
||||||
|
FROM user_constraints c
|
||||||
|
WHERE c.constraint_name = i.index_name
|
||||||
|
AND c.table_name = v_table_upper
|
||||||
|
AND c.constraint_type IN ('P', 'U'))
|
||||||
|
GROUP BY i.index_name, i.uniqueness
|
||||||
|
ORDER BY i.index_name) LOOP
|
||||||
|
IF rec.uniqueness = 'UNIQUE' THEN
|
||||||
|
append_line('CREATE UNIQUE INDEX ' || rec.index_name || ' ON ' ||
|
||||||
|
v_table_upper || ' (' || rec.cols || ');' || CHR(10));
|
||||||
|
ELSE
|
||||||
|
append_line('CREATE INDEX ' || rec.index_name || ' ON ' ||
|
||||||
|
v_table_upper || ' (' || rec.cols || ');' || CHR(10));
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN NO_DATA_FOUND THEN
|
||||||
|
r_table := r_table || '-- Table not found: ' || v_table_upper ||
|
||||||
|
CHR(10);
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
r_table := r_table || '-- Error: ' || SQLERRM || CHR(10);
|
||||||
|
END hmig_get_table_sql;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
-- Oracle 11g 兼容的存储过程:生成表的 INSERT 语句
|
||||||
|
-- 参数:p_table_name 表名,p_result 输出 INSERT 语句(CLOB)
|
||||||
|
-- 限制:行数超过 200 行报错
|
||||||
|
|
||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_insert_sql(
|
||||||
|
p_table_name IN VARCHAR2,
|
||||||
|
p_result OUT CLOB
|
||||||
|
) AS
|
||||||
|
v_count NUMBER;
|
||||||
|
v_sql VARCHAR2(32767);
|
||||||
|
v_insert_sql CLOB;
|
||||||
|
v_table_upper VARCHAR2(128);
|
||||||
|
v_col_count NUMBER;
|
||||||
|
v_col_names DBMS_SQL.VARCHAR2_TABLE;
|
||||||
|
v_col_types DBMS_SQL.NUMBER_TABLE;
|
||||||
|
v_col_values DBMS_SQL.VARCHAR2_TABLE;
|
||||||
|
v_row_sql VARCHAR2(32767);
|
||||||
|
v_val_sql VARCHAR2(32767);
|
||||||
|
v_cursor_id INTEGER;
|
||||||
|
v_dummy INTEGER;
|
||||||
|
v_col_val VARCHAR2(4000);
|
||||||
|
v_date_val DATE;
|
||||||
|
v_col_idx NUMBER;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
v_table_upper := UPPER(TRIM(p_table_name));
|
||||||
|
|
||||||
|
-- 初始化 CLOB
|
||||||
|
DBMS_LOB.createtemporary(p_result, TRUE);
|
||||||
|
DBMS_LOB.createtemporary(v_insert_sql, TRUE);
|
||||||
|
|
||||||
|
-- 检查表是否存在
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO v_count
|
||||||
|
FROM user_tables
|
||||||
|
WHERE table_name = v_table_upper;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 ' || v_table_upper || ' 不存在');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 检查行数限制(200行)
|
||||||
|
v_sql := 'SELECT COUNT(*) FROM ' || v_table_upper;
|
||||||
|
EXECUTE IMMEDIATE v_sql INTO v_count;
|
||||||
|
|
||||||
|
IF v_count > 200 THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 ' || v_table_upper || ' 数据行数(' || v_count || ')超过200行限制');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '-- 表 ' || v_table_upper || ' 无数据');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 头部注释
|
||||||
|
DBMS_LOB.append(v_insert_sql, '-- 表 ' || v_table_upper || ' 共 ' || v_count || ' 行数据' || CHR(10));
|
||||||
|
|
||||||
|
-- 使用 DBMS_SQL 动态获取列信息
|
||||||
|
v_cursor_id := DBMS_SQL.OPEN_CURSOR;
|
||||||
|
|
||||||
|
-- 解析查询
|
||||||
|
DBMS_SQL.PARSE(v_cursor_id, 'SELECT * FROM ' || v_table_upper, DBMS_SQL.NATIVE);
|
||||||
|
|
||||||
|
-- 获取列数
|
||||||
|
v_col_count := DBMS_SQL.DESCRIBE_COLUMNS(v_cursor_id, v_col_count, v_col_names, ... );
|
||||||
|
|
||||||
|
-- ... DBMS_SQL 方案也较复杂,换更简单的方案
|
||||||
|
DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
|
||||||
|
|
||||||
|
p_result := v_insert_sql;
|
||||||
|
DBMS_LOB.freetemporary(v_insert_sql);
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: ' || SQLERRM);
|
||||||
|
IF DBMS_SQL.IS_OPEN(v_cursor_id) THEN DBMS_SQL.CLOSE_CURSOR(v_cursor_id); END IF;
|
||||||
|
DBMS_LOB.freetemporary(v_insert_sql);
|
||||||
|
END;
|
||||||
|
/
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
-- Oracle 11g 兼容的存储过程:生成表的 INSERT 语句
|
||||||
|
-- 使用 DBMS_XMLGEN 实现,支持任意表结构
|
||||||
|
-- 参数:p_table_name 表名,p_result 输出 INSERT 语句(CLOB)
|
||||||
|
-- 限制:行数超过 200 行报错
|
||||||
|
|
||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_insert_sql(
|
||||||
|
p_table_name IN VARCHAR2,
|
||||||
|
p_result OUT CLOB
|
||||||
|
) AS
|
||||||
|
v_count NUMBER;
|
||||||
|
v_table_upper VARCHAR2(128);
|
||||||
|
v_ctx NUMBER;
|
||||||
|
v_xml CLOB;
|
||||||
|
v_sql VARCHAR2(32767);
|
||||||
|
v_col_names DBMS_SQL.VARCHAR2A;
|
||||||
|
v_col_count NUMBER;
|
||||||
|
|
||||||
|
-- 解析 XML
|
||||||
|
v_row_xml XMLTYPE;
|
||||||
|
v_col_val VARCHAR2(4000);
|
||||||
|
v_insert_line VARCHAR2(32767);
|
||||||
|
v_cols_part VARCHAR2(32767);
|
||||||
|
v_vals_part VARCHAR2(32767);
|
||||||
|
v_col_idx NUMBER;
|
||||||
|
v_row_idx NUMBER;
|
||||||
|
v_rows XMLTYPE;
|
||||||
|
v_row_node XMLTYPE;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
v_table_upper := UPPER(TRIM(p_table_name));
|
||||||
|
DBMS_LOB.createtemporary(p_result, TRUE);
|
||||||
|
|
||||||
|
-- 检查表是否存在
|
||||||
|
SELECT COUNT(*) INTO v_count
|
||||||
|
FROM user_tables
|
||||||
|
WHERE table_name = v_table_upper;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 ' || v_table_upper || ' 不存在');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 检查行数限制
|
||||||
|
v_sql := 'SELECT COUNT(*) FROM ' || v_table_upper;
|
||||||
|
EXECUTE IMMEDIATE v_sql INTO v_count;
|
||||||
|
|
||||||
|
IF v_count > 200 THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 ' || v_table_upper || ' 数据行数(' || v_count || ')超过200行限制');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '-- 表 ' || v_table_upper || ' 无数据');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 头部
|
||||||
|
DBMS_LOB.append(p_result, '-- 表 ' || v_table_upper || ' 共 ' || v_count || ' 行' || CHR(10));
|
||||||
|
|
||||||
|
-- 获取列名列表
|
||||||
|
SELECT column_name
|
||||||
|
BULK COLLECT INTO v_col_names
|
||||||
|
FROM user_tab_columns
|
||||||
|
WHERE table_name = v_table_upper
|
||||||
|
ORDER BY column_id;
|
||||||
|
|
||||||
|
v_col_count := v_col_names.COUNT;
|
||||||
|
|
||||||
|
-- 构建列名部分
|
||||||
|
v_cols_part := '';
|
||||||
|
FOR v_col_idx IN 1..v_col_count LOOP
|
||||||
|
IF v_col_idx > 1 THEN v_cols_part := v_cols_part || ', '; END IF;
|
||||||
|
v_cols_part := v_cols_part || v_col_names(v_col_idx);
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- 用 DBMS_XMLGEN 获取数据为 XML
|
||||||
|
v_sql := 'SELECT * FROM ' || v_table_upper;
|
||||||
|
v_ctx := DBMS_XMLGEN.NEWCONTEXT(v_sql);
|
||||||
|
DBMS_XMLGEN.SETROWSETTAG(v_ctx, 'ROWSET');
|
||||||
|
DBMS_XMLGEN.SETROWTAG(v_ctx, 'ROW');
|
||||||
|
v_xml := DBMS_XMLGEN.GETXML(v_ctx);
|
||||||
|
DBMS_XMLGEN.CLOSECONTEXT(v_ctx);
|
||||||
|
|
||||||
|
-- 解析 XML 生成 INSERT 语句
|
||||||
|
v_rows := XMLTYPE(v_xml);
|
||||||
|
|
||||||
|
-- 遍历每个 ROW
|
||||||
|
FOR v_row_idx IN 1..v_count LOOP
|
||||||
|
v_row_node := v_rows.extract('/ROWSET/ROW[' || v_row_idx || ']');
|
||||||
|
|
||||||
|
IF v_row_node IS NOT NULL THEN
|
||||||
|
v_vals_part := '';
|
||||||
|
|
||||||
|
FOR v_col_idx IN 1..v_col_count LOOP
|
||||||
|
IF v_col_idx > 1 THEN v_vals_part := v_vals_part || ', '; END IF;
|
||||||
|
|
||||||
|
-- 提取列值
|
||||||
|
BEGIN
|
||||||
|
v_col_val := v_row_node.extract('//ROW/' || v_col_names(v_col_idx) || '/text()').getStringVal();
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
v_col_val := NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 处理 NULL 和字符串转义
|
||||||
|
IF v_col_val IS NULL THEN
|
||||||
|
v_vals_part := v_vals_part || 'NULL';
|
||||||
|
ELSE
|
||||||
|
-- 转义单引号
|
||||||
|
v_col_val := REPLACE(v_col_val, '''', '''''');
|
||||||
|
v_vals_part := v_vals_part || '''' || v_col_val || '''';
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- 生成 INSERT 语句
|
||||||
|
v_insert_line := 'INSERT INTO ' || v_table_upper || ' (' || v_cols_part || ') VALUES (' || v_vals_part || ');' || CHR(10);
|
||||||
|
DBMS_LOB.append(p_result, v_insert_line);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
DBMS_LOB.append(p_result, 'COMMIT;' || CHR(10));
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: ' || SQLERRM);
|
||||||
|
END;
|
||||||
|
/
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- Oracle 11g 兼容的存储过程:生成表的 INSERT 语句
|
||||||
|
--
|
||||||
|
-- 方案说明:
|
||||||
|
-- Oracle 11g PL/SQL 无法动态处理未知表结构的行数据
|
||||||
|
-- 以下是三种可行的实现方案
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 方案1: DBMS_XMLGEN + XMLTYPE(通用版,推荐)
|
||||||
|
-- 优点:支持任意表结构
|
||||||
|
-- 缺点:XML 解析可能有性能问题,CLOB 较大时需注意内存
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_insert_sql(
|
||||||
|
p_table_name IN VARCHAR2,
|
||||||
|
p_result OUT CLOB
|
||||||
|
) AS
|
||||||
|
v_count NUMBER;
|
||||||
|
v_table_upper VARCHAR2(128);
|
||||||
|
v_ctx NUMBER;
|
||||||
|
v_xml CLOB;
|
||||||
|
v_sql VARCHAR2(32767);
|
||||||
|
v_col_names DBMS_SQL.VARCHAR2A;
|
||||||
|
v_col_types DBMS_SQL.VARCHAR2A;
|
||||||
|
v_col_count NUMBER;
|
||||||
|
v_insert_line VARCHAR2(32767);
|
||||||
|
v_cols_part VARCHAR2(32767);
|
||||||
|
v_vals_part VARCHAR2(32767);
|
||||||
|
v_col_idx NUMBER;
|
||||||
|
v_row_idx NUMBER;
|
||||||
|
v_rows XMLTYPE;
|
||||||
|
v_row_node XMLTYPE;
|
||||||
|
v_col_val VARCHAR2(4000);
|
||||||
|
v_col_name VARCHAR2(128);
|
||||||
|
BEGIN
|
||||||
|
v_table_upper := UPPER(TRIM(p_table_name));
|
||||||
|
DBMS_LOB.createtemporary(p_result, TRUE);
|
||||||
|
|
||||||
|
-- 检查表是否存在
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO v_count FROM user_tables WHERE table_name = v_table_upper;
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 [' || v_table_upper || '] 不存在');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 检查行数限制(200行)
|
||||||
|
v_sql := 'SELECT COUNT(*) FROM ' || v_table_upper;
|
||||||
|
EXECUTE IMMEDIATE v_sql INTO v_count;
|
||||||
|
|
||||||
|
IF v_count > 200 THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 [' || v_table_upper || '] 数据行数(' || v_count || ')超过200行限制');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '-- 表 [' || v_table_upper || '] 无数据');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 头部注释
|
||||||
|
DBMS_LOB.append(p_result, '-- 表 ' || v_table_upper || ' 共 ' || v_count || ' 行数据' || CHR(10));
|
||||||
|
|
||||||
|
-- 获取列信息
|
||||||
|
SELECT column_name, data_type
|
||||||
|
BULK COLLECT INTO v_col_names, v_col_types
|
||||||
|
FROM user_tab_columns
|
||||||
|
WHERE table_name = v_table_upper
|
||||||
|
ORDER BY column_id;
|
||||||
|
v_col_count := v_col_names.COUNT;
|
||||||
|
|
||||||
|
-- 构建列名部分
|
||||||
|
v_cols_part := '';
|
||||||
|
FOR v_col_idx IN 1..v_col_count LOOP
|
||||||
|
IF v_col_idx > 1 THEN v_cols_part := v_cols_part || ', '; END IF;
|
||||||
|
v_cols_part := v_cols_part || v_col_names(v_col_idx);
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- 用 DBMS_XMLGEN 获取数据
|
||||||
|
v_sql := 'SELECT * FROM ' || v_table_upper;
|
||||||
|
v_ctx := DBMS_XMLGEN.NEWCONTEXT(v_sql);
|
||||||
|
v_xml := DBMS_XMLGEN.GETXML(v_ctx);
|
||||||
|
DBMS_XMLGEN.CLOSECONTEXT(v_ctx);
|
||||||
|
|
||||||
|
-- 解析 XML
|
||||||
|
v_rows := XMLTYPE(v_xml);
|
||||||
|
|
||||||
|
FOR v_row_idx IN 1..v_count LOOP
|
||||||
|
BEGIN
|
||||||
|
v_row_node := v_rows.extract('/ROWSET/ROW[' || v_row_idx || ']');
|
||||||
|
IF v_row_node IS NULL THEN CONTINUE; END IF;
|
||||||
|
|
||||||
|
v_vals_part := '';
|
||||||
|
FOR v_col_idx IN 1..v_col_count LOOP
|
||||||
|
IF v_col_idx > 1 THEN v_vals_part := v_vals_part || ', '; END IF;
|
||||||
|
|
||||||
|
v_col_name := v_col_names(v_col_idx);
|
||||||
|
BEGIN
|
||||||
|
v_col_val := v_row_node.extract('//ROW/' || v_col_name || '/text()').getStringVal();
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN v_col_val := NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF v_col_val IS NULL THEN
|
||||||
|
v_vals_part := v_vals_part || 'NULL';
|
||||||
|
ELSE
|
||||||
|
v_col_val := REPLACE(v_col_val, '''', '''''');
|
||||||
|
v_vals_part := v_vals_part || '''' || v_col_val || '''';
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
v_insert_line := 'INSERT INTO ' || v_table_upper || ' (' || v_cols_part || ') VALUES (' || v_vals_part || ');' || CHR(10);
|
||||||
|
DBMS_LOB.append(p_result, v_insert_line);
|
||||||
|
END;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
DBMS_LOB.append(p_result, CHR(10) || 'COMMIT;');
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: ' || SQLERRM);
|
||||||
|
END;
|
||||||
|
/
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 方案2: 函数版本(返回 CLOB)
|
||||||
|
-- 用法: SELECT hmig_get_insert_sql_f('表名') FROM DUAL;
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION hmig_get_insert_sql_f(
|
||||||
|
p_table_name IN VARCHAR2
|
||||||
|
) RETURN CLOB AS
|
||||||
|
v_result CLOB;
|
||||||
|
BEGIN
|
||||||
|
hmig_get_insert_sql(p_table_name, v_result);
|
||||||
|
RETURN v_result;
|
||||||
|
END;
|
||||||
|
/
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 方案3: 带行数限制参数的版本
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE PROCEDURE hmig_get_insert_sql_ex(
|
||||||
|
p_table_name IN VARCHAR2,
|
||||||
|
p_max_rows IN NUMBER DEFAULT 200,
|
||||||
|
p_result OUT CLOB
|
||||||
|
) AS
|
||||||
|
v_count NUMBER;
|
||||||
|
v_table_upper VARCHAR2(128);
|
||||||
|
v_ctx NUMBER;
|
||||||
|
v_xml CLOB;
|
||||||
|
v_sql VARCHAR2(32767);
|
||||||
|
v_col_names DBMS_SQL.VARCHAR2A;
|
||||||
|
v_col_types DBMS_SQL.VARCHAR2A;
|
||||||
|
v_col_count NUMBER;
|
||||||
|
v_insert_line VARCHAR2(32767);
|
||||||
|
v_cols_part VARCHAR2(32767);
|
||||||
|
v_vals_part VARCHAR2(32767);
|
||||||
|
v_col_idx NUMBER;
|
||||||
|
v_row_idx NUMBER;
|
||||||
|
v_rows XMLTYPE;
|
||||||
|
v_row_node XMLTYPE;
|
||||||
|
v_col_val VARCHAR2(4000);
|
||||||
|
v_col_name VARCHAR2(128);
|
||||||
|
BEGIN
|
||||||
|
v_table_upper := UPPER(TRIM(p_table_name));
|
||||||
|
DBMS_LOB.createtemporary(p_result, TRUE);
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO v_count FROM user_tables WHERE table_name = v_table_upper;
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 [' || v_table_upper || '] 不存在');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
|
||||||
|
v_sql := 'SELECT COUNT(*) FROM ' || v_table_upper;
|
||||||
|
EXECUTE IMMEDIATE v_sql INTO v_count;
|
||||||
|
|
||||||
|
IF v_count > p_max_rows THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: 表 [' || v_table_upper || '] 数据行数(' || v_count || ')超过' || p_max_rows || '行限制');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(p_result, '-- 表 [' || v_table_upper || '] 无数据');
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DBMS_LOB.append(p_result, '-- 表 ' || v_table_upper || ' 共 ' || v_count || ' 行数据' || CHR(10));
|
||||||
|
|
||||||
|
SELECT column_name, data_type
|
||||||
|
BULK COLLECT INTO v_col_names, v_col_types
|
||||||
|
FROM user_tab_columns
|
||||||
|
WHERE table_name = v_table_upper
|
||||||
|
ORDER BY column_id;
|
||||||
|
v_col_count := v_col_names.COUNT;
|
||||||
|
|
||||||
|
v_cols_part := '';
|
||||||
|
FOR v_col_idx IN 1..v_col_count LOOP
|
||||||
|
IF v_col_idx > 1 THEN v_cols_part := v_cols_part || ', '; END IF;
|
||||||
|
v_cols_part := v_cols_part || v_col_names(v_col_idx);
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
v_sql := 'SELECT * FROM ' || v_table_upper;
|
||||||
|
v_ctx := DBMS_XMLGEN.NEWCONTEXT(v_sql);
|
||||||
|
v_xml := DBMS_XMLGEN.GETXML(v_ctx);
|
||||||
|
DBMS_XMLGEN.CLOSECONTEXT(v_ctx);
|
||||||
|
|
||||||
|
v_rows := XMLTYPE(v_xml);
|
||||||
|
|
||||||
|
FOR v_row_idx IN 1..v_count LOOP
|
||||||
|
BEGIN
|
||||||
|
v_row_node := v_rows.extract('/ROWSET/ROW[' || v_row_idx || ']');
|
||||||
|
IF v_row_node IS NULL THEN CONTINUE; END IF;
|
||||||
|
|
||||||
|
v_vals_part := '';
|
||||||
|
FOR v_col_idx IN 1..v_col_count LOOP
|
||||||
|
IF v_col_idx > 1 THEN v_vals_part := v_vals_part || ', '; END IF;
|
||||||
|
|
||||||
|
v_col_name := v_col_names(v_col_idx);
|
||||||
|
BEGIN
|
||||||
|
v_col_val := v_row_node.extract('//ROW/' || v_col_name || '/text()').getStringVal();
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN v_col_val := NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF v_col_val IS NULL THEN
|
||||||
|
v_vals_part := v_vals_part || 'NULL';
|
||||||
|
ELSE
|
||||||
|
v_col_val := REPLACE(v_col_val, '''', '''''');
|
||||||
|
v_vals_part := v_vals_part || '''' || v_col_val || '''';
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
v_insert_line := 'INSERT INTO ' || v_table_upper || ' (' || v_cols_part || ') VALUES (' || v_vals_part || ');' || CHR(10);
|
||||||
|
DBMS_LOB.append(p_result, v_insert_line);
|
||||||
|
END;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
DBMS_LOB.append(p_result, CHR(10) || 'COMMIT;');
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
DBMS_LOB.append(p_result, '错误: ' || SQLERRM);
|
||||||
|
END;
|
||||||
|
/
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 使用示例
|
||||||
|
-- ============================================
|
||||||
|
/*
|
||||||
|
-- 调用存储过程
|
||||||
|
DECLARE
|
||||||
|
v_result CLOB;
|
||||||
|
BEGIN
|
||||||
|
hmig_get_insert_sql('表名', v_result);
|
||||||
|
DBMS_OUTPUT.PUT_LINE(v_result);
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 调用函数
|
||||||
|
SELECT hmig_get_insert_sql_f('表名') FROM DUAL;
|
||||||
|
|
||||||
|
-- 带自定义行数限制
|
||||||
|
DECLARE
|
||||||
|
v_result CLOB;
|
||||||
|
BEGIN
|
||||||
|
hmig_get_insert_sql_ex('表名', 500, v_result); -- 允许最多500行
|
||||||
|
DBMS_OUTPUT.PUT_LINE(v_result);
|
||||||
|
END;
|
||||||
|
*/
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
-- Oracle 11g 兼容的函数:生成表的 INSERT 语句
|
||||||
|
-- 参数:p_table_name 表名
|
||||||
|
-- 返回:INSERT 语句(CLOB),行数超过200报错
|
||||||
|
-- 注意:此函数需要已知表结构,通用版需要用 DBMS_SQL
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION hmig_get_insert_sql_f(
|
||||||
|
p_table_name IN VARCHAR2
|
||||||
|
) RETURN CLOB AS
|
||||||
|
v_result CLOB;
|
||||||
|
v_count NUMBER;
|
||||||
|
v_table_upper VARCHAR2(128);
|
||||||
|
|
||||||
|
-- 通用游标
|
||||||
|
TYPE t_ref_cursor IS REF CURSOR;
|
||||||
|
c_data t_ref_cursor;
|
||||||
|
v_sql VARCHAR2(32767);
|
||||||
|
|
||||||
|
-- 列信息
|
||||||
|
TYPE t_col_rec IS RECORD (
|
||||||
|
column_name VARCHAR2(128),
|
||||||
|
data_type VARCHAR2(128)
|
||||||
|
);
|
||||||
|
TYPE t_col_list IS TABLE OF t_col_rec INDEX BY BINARY_INTEGER;
|
||||||
|
v_cols t_col_list;
|
||||||
|
v_col_count NUMBER;
|
||||||
|
|
||||||
|
-- 行数据
|
||||||
|
v_row_sql VARCHAR2(32767);
|
||||||
|
v_col_sql VARCHAR2(32767);
|
||||||
|
v_val_sql VARCHAR2(32767);
|
||||||
|
v_col_val VARCHAR2(4000);
|
||||||
|
v_date_val DATE;
|
||||||
|
v_num_val NUMBER;
|
||||||
|
i NUMBER;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
v_table_upper := UPPER(TRIM(p_table_name));
|
||||||
|
DBMS_LOB.createtemporary(v_result, TRUE);
|
||||||
|
|
||||||
|
-- 检查表
|
||||||
|
SELECT COUNT(*) INTO v_count
|
||||||
|
FROM user_tables WHERE table_name = v_table_upper;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(v_result, '错误: 表 ' || v_table_upper || ' 不存在');
|
||||||
|
RETURN v_result;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 检查行数
|
||||||
|
v_sql := 'SELECT COUNT(*) FROM ' || v_table_upper;
|
||||||
|
EXECUTE IMMEDIATE v_sql INTO v_count;
|
||||||
|
|
||||||
|
IF v_count > 200 THEN
|
||||||
|
DBMS_LOB.append(v_result, '错误: 表 ' || v_table_upper || ' 数据行数(' || v_count || ')超过200行限制');
|
||||||
|
RETURN v_result;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_count = 0 THEN
|
||||||
|
DBMS_LOB.append(v_result, '-- 表 ' || v_table_upper || ' 无数据');
|
||||||
|
RETURN v_result;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- 添加头部
|
||||||
|
DBMS_LOB.append(v_result, '-- 表 ' || v_table_upper || ' 共 ' || v_count || ' 行' || CHR(10));
|
||||||
|
|
||||||
|
-- 获取列信息
|
||||||
|
SELECT column_name, data_type
|
||||||
|
BULK COLLECT INTO v_cols
|
||||||
|
FROM user_tab_columns
|
||||||
|
WHERE table_name = v_table_upper
|
||||||
|
ORDER BY column_id;
|
||||||
|
|
||||||
|
v_col_count := v_cols.COUNT;
|
||||||
|
|
||||||
|
-- 构建列名列表
|
||||||
|
v_col_sql := '';
|
||||||
|
FOR i IN 1..v_col_count LOOP
|
||||||
|
IF i > 1 THEN v_col_sql := v_col_sql || ', '; END IF;
|
||||||
|
v_col_sql := v_col_sql || v_cols(i).column_name;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- 用 PL/SQL 无法直接 FETCH 未知结构的游标
|
||||||
|
-- 这里用变通方案:动态 SQL + XML
|
||||||
|
-- 实际上 Oracle 11g 的 DBMS_XMLGEN 可以做到
|
||||||
|
|
||||||
|
FOR r_row IN (SELECT rowid as rid FROM v_table_upper) LOOP
|
||||||
|
-- 需要用 DBMS_SQL 或已知表结构才能逐行处理
|
||||||
|
-- Oracle 11g 不支持动态记录类型
|
||||||
|
NULL;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- 由于 Oracle 11g PL/SQL 限制,这里提供替代方案
|
||||||
|
-- 方案A: 用已知列名调用(下方注释掉的代码)
|
||||||
|
-- 方案B: 用 Java 存储过程(更灵活)
|
||||||
|
-- 方案C: 用 DBMS_XMLGEN + XSLT 转换
|
||||||
|
|
||||||
|
DBMS_LOB.append(v_result, '-- 注意:Oracle 11g PL/SQL 无法动态获取未知结构的行数据' || CHR(10));
|
||||||
|
DBMS_LOB.append(v_result, '-- 建议使用 DBMS_XMLGEN 或 Java 存储过程实现通用方案' || CHR(10));
|
||||||
|
|
||||||
|
RETURN v_result;
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
DBMS_LOB.append(v_result, '错误: ' || SQLERRM);
|
||||||
|
RETURN v_result;
|
||||||
|
END;
|
||||||
|
/
|
||||||
Reference in New Issue
Block a user