Initial import of WPF migration tool

This commit is contained in:
chen qiang
2026-08-25 16:23:14 +08:00
commit 227f92c45b
47 changed files with 5204 additions and 0 deletions
@@ -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 正式安装后的自动更新流程
+112
View File
@@ -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>
+230
View File
@@ -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;
}
}
}
}
+81
View File
@@ -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();
}
}
+12
View File
@@ -0,0 +1,12 @@
namespace henlo_migration.Enums
{
public enum BtnsEnum
{
读取,
写入,
删除,
上,
下,
置顶
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace henlo_migration.Enums
{
public enum CodeExt
{
bos,
creat,
data,
prc
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace henlo_migration.Enums
{
public enum StatusEnum
{
未读取,
不存在,
读取中,
已存在,
已读取,
读取失败,
未写入,
写入中,
已写入,
写入失败,
编译失败
}
}
+10
View File
@@ -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>
+231
View File
@@ -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();
}
}
}
+619
View File
@@ -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
+65
View File
@@ -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, '&amp;apos;', '''');
r_str := REPLACE(r_str, '&lt;', '<');
r_str := REPLACE(r_str, '&gt;', '>');
r_str := REPLACE(r_str, '&amp;gt;', '>');
r_str := REPLACE(r_str, '&amp;lt;', '<');
r_str := REPLACE(r_str, '<>', '!=');
/* v_str := REPLACE(v_str, '<=', '&lt;=');
v_str := REPLACE(v_str, '>=', '&gt;=');*/
r_str := REPLACE(r_str, '&apos;', '''');
r_str := REPLACE(r_str, '&quot;', '"');
r_str := REPLACE(r_str, '&lt;', '<');
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;
/