|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Data;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Data.SqlClient;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Windows.Threading;
|
|
using NLog;
|
|
using System.Reflection;
|
|
using System.Xml.Linq;
|
|
using System.Diagnostics;
|
|
using IOPath = System.IO.Path; // 添加別名以避免命名空間衝突
|
|
using IOFile = System.IO.File; // 添加別名以避免可能的命名空間衝突
|
|
using com.mirle.ibg3k0.sc.App;
|
|
using com.mirle.ibg3k0.sc.Common;
|
|
|
|
namespace BCWPF
|
|
{
|
|
/// <summary>
|
|
/// 用於顯示可還原資料庫資訊的類
|
|
/// </summary>
|
|
public class RestorableDatabaseInfo
|
|
{
|
|
/// <summary>
|
|
/// 資料庫名稱
|
|
/// </summary>
|
|
public string DatabaseName { get; set; }
|
|
|
|
/// <summary>
|
|
/// 資料庫版本
|
|
/// </summary>
|
|
public string Version { get; set; }
|
|
|
|
/// <summary>
|
|
/// 資料庫建立時間
|
|
/// </summary>
|
|
public DateTime CreationTime { get; set; }
|
|
|
|
/// <summary>
|
|
/// 顯示在清單中的文字
|
|
/// </summary>
|
|
public override string ToString()
|
|
{
|
|
return $"{DatabaseName} (版本: {Version}, 建立時間: {CreationTime:yyyy-MM-dd HH:mm:ss})";
|
|
}
|
|
}
|
|
|
|
public partial class frm_DbSetting : Window
|
|
{
|
|
/// <summary>
|
|
/// 檢查資料庫連線狀態並等待直到可以連線
|
|
/// </summary>
|
|
/// <param name="connectionString">資料庫連線字串</param>
|
|
/// <param name="databaseName">要檢查的資料庫名稱</param>
|
|
/// <param name="maxRetries">最大重試次數</param>
|
|
/// <param name="retryDelayMs">每次重試間隔(毫秒)</param>
|
|
/// <param name="timeoutMs">總超時時間(毫秒),0表示無限制</param>
|
|
/// <returns>成功返回true,失敗返回false</returns>
|
|
private async Task<bool> WaitForDatabaseConnection(
|
|
string connectionString,
|
|
string databaseName,
|
|
int maxRetries = 20,
|
|
int retryDelayMs = 1000,
|
|
int timeoutMs = 0)
|
|
{
|
|
var startTime = DateTime.Now;
|
|
int retryCount = 0;
|
|
|
|
while (retryCount < maxRetries)
|
|
{
|
|
try
|
|
{
|
|
using (var connection = new SqlConnection(connectionString))
|
|
{
|
|
await connection.OpenAsync();
|
|
|
|
// 如果指定了特定資料庫,檢查該資料庫是否可用
|
|
if (!string.IsNullOrEmpty(databaseName))
|
|
{
|
|
using (var command = new SqlCommand(
|
|
"SELECT COUNT(*) FROM sys.databases WHERE name = @dbName",
|
|
connection))
|
|
{
|
|
command.Parameters.AddWithValue("@dbName", databaseName);
|
|
int count = Convert.ToInt32(await command.ExecuteScalarAsync());
|
|
if (count == 0)
|
|
{
|
|
throw new Exception($"資料庫 {databaseName} 不存在");
|
|
}
|
|
}
|
|
|
|
// 嘗試切換到指定的資料庫
|
|
using (var command = new SqlCommand($"USE [{databaseName}]", connection))
|
|
{
|
|
await command.ExecuteNonQueryAsync();
|
|
}
|
|
}
|
|
|
|
LogMessage($"成功連線到資料庫 {(string.IsNullOrEmpty(databaseName) ? "(master)" : databaseName)}");
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
retryCount++;
|
|
|
|
// 檢查是否超時
|
|
if (timeoutMs > 0 && (DateTime.Now - startTime).TotalMilliseconds > timeoutMs)
|
|
{
|
|
LogMessage($"等待資料庫連線超時: {ex.Message}", true);
|
|
return false;
|
|
}
|
|
|
|
if (retryCount < maxRetries)
|
|
{
|
|
LogMessage($"等待資料庫連線中... (重試 {retryCount}/{maxRetries}): {ex.Message}", true);
|
|
await Task.Delay(retryDelayMs);
|
|
}
|
|
else
|
|
{
|
|
LogMessage($"資料庫連線重試次數已達上限: {ex.Message}", true);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private string _connectionString = string.Empty; // 預設值移至SetDefaultValues方法中
|
|
private string _masterConnectionString = string.Empty; // 預設值移至SetDefaultValues方法中
|
|
private string _configPath;
|
|
private static readonly Logger _logger = LogManager.GetLogger("DbSettings");
|
|
|
|
// 新增一個標記來追蹤是否有操作正在執行
|
|
private bool _isOperationInProgress = false;
|
|
|
|
// 用於追蹤資料庫遷移過程中是否因錯誤而失敗
|
|
// 當遷移過程中發生錯誤時設為 true,用於後續判斷是否需要重試或回滾
|
|
private bool _migrationFailedDueToError = false;
|
|
// 追蹤當前執行的操作名稱,便於日誌記錄
|
|
private string _currentOperation = string.Empty;
|
|
// 新增一個標記,表示是否已從配置檔案載入成功
|
|
private bool _isLoadedFromConfig = false;
|
|
// 新增一個標記,表示界面是否有未保存的變更
|
|
private bool _hasUnsavedChanges = false;
|
|
|
|
/// <summary>
|
|
/// 檢查主視窗是否存在且可見
|
|
/// </summary>
|
|
private bool IsMainWindowActive()
|
|
{
|
|
return Application.Current.Windows.OfType<MainWindow>().Any(w => w.IsVisible);
|
|
}
|
|
|
|
private void PauseTraceDataReportTimer()
|
|
{
|
|
try
|
|
{
|
|
if (IsMainWindowActive())
|
|
{
|
|
// 只有在主視窗存在時才控制資料庫存取
|
|
SCUtility.AllowDBAccess = false;
|
|
LogMessage("已暫停資料庫存取和 Timer");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"暫停資料庫存取和 Timer 時發生錯誤: {ex.Message}", true);
|
|
}
|
|
}
|
|
|
|
private void ResumeTraceDataReportTimer()
|
|
{
|
|
try
|
|
{
|
|
if (IsMainWindowActive())
|
|
{
|
|
// 只有在主視窗存在時才控制資料庫存取
|
|
SCUtility.AllowDBAccess = true;
|
|
LogMessage("已恢復資料庫存取和 Timer");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"恢復資料庫存取和 Timer 時發生錯誤: {ex.Message}", true);
|
|
}
|
|
}
|
|
|
|
public frm_DbSetting()
|
|
{
|
|
InitializeComponent();
|
|
|
|
// 添加鍵盤焦點處理事件
|
|
this.KeyDown += Frm_DbSetting_KeyDown;
|
|
this.PreviewMouseDown += Frm_DbSetting_PreviewMouseDown;
|
|
this.MouseDown += Frm_DbSetting_MouseDown;
|
|
|
|
// 添加窗體點擊事件來處理焦點
|
|
this.MouseDown += Window_MouseDown;
|
|
|
|
|
|
|
|
// 使用低優先級加載初始設定,避免阻塞UI
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
// 設定配置檔案路徑
|
|
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
string configDir = IOPath.Combine(baseDir, "Config");
|
|
|
|
// 如果是 Debug 模式,使用專案目錄下的 Config
|
|
if (System.Diagnostics.Debugger.IsAttached)
|
|
{
|
|
// 取得專案根目錄(向上三層到專案根目錄)
|
|
string projectRoot = IOPath.GetFullPath(IOPath.Combine(baseDir, @"..\..\..\"));
|
|
configDir = IOPath.Combine(projectRoot, "BCWPF", "Config");
|
|
LogMessage($"Debug 模式:使用專案目錄 {configDir}");
|
|
}
|
|
|
|
_configPath = IOPath.Combine(configDir, "SQLServer.cfg.xml");
|
|
LogMessage($"設定檔路徑:{_configPath}");
|
|
|
|
// 初始化日誌區域
|
|
ClearLog();
|
|
LogMessage("系統設定視窗初始化...");
|
|
|
|
// 設定預設連線字串 - 這只是備用
|
|
SetDefaultValues();
|
|
|
|
// 載入資料庫設定
|
|
LoadDatabaseSettings();
|
|
|
|
LogMessage("系統設定視窗初始化完成");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"視窗初始化錯誤: {ex.Message}");
|
|
MessageBox.Show($"視窗初始化發生錯誤: {ex.Message}", "錯誤", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}), DispatcherPriority.Background);
|
|
|
|
// 立即註冊事件,避免等待初始化完成
|
|
RegisterEvents();
|
|
}
|
|
|
|
// 點擊窗體任何區域時嘗試將焦點移到窗體上,以便讓文本框失去焦點
|
|
private void Frm_DbSetting_MouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
Keyboard.ClearFocus(); // 清除鍵盤焦點
|
|
this.Focus(); // 使窗體獲得焦點
|
|
}
|
|
|
|
// 預覽鼠標點擊事件
|
|
private void Frm_DbSetting_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
// 如果點擊的是窗體背景(非控件區域)
|
|
if (e.OriginalSource is Border || e.OriginalSource is Grid || e.OriginalSource is Window)
|
|
{
|
|
Keyboard.ClearFocus(); // 清除當前控件的焦點
|
|
FocusManager.SetFocusedElement(this, this); // 將焦點設置到窗體
|
|
}
|
|
}
|
|
|
|
// 處理按Esc鍵清除焦點
|
|
private void Frm_DbSetting_KeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (e.Key == Key.Escape)
|
|
{
|
|
// 清除焦點
|
|
Keyboard.ClearFocus();
|
|
FocusManager.SetFocusedElement(this, this);
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
// 分離事件註冊邏輯,提高UI回應速度
|
|
private void RegisterEvents()
|
|
{
|
|
try
|
|
{
|
|
// 設定事件處理
|
|
txtDbPort.PreviewTextInput += TxtDbPort_PreviewTextInput;
|
|
txtDbPort.LostFocus += TxtDbPort_LostFocus;
|
|
|
|
// 為IP地址文字框添加事件處理
|
|
txtDbIP.LostFocus += TxtDbIP_LostFocus;
|
|
|
|
// 新增文本變更事件處理以追蹤未保存的變更
|
|
txtDbIP.TextChanged += MarkAsChanged;
|
|
txtDbPort.TextChanged += MarkAsChanged;
|
|
txtDbName.TextChanged += MarkAsChanged;
|
|
txtDbID.TextChanged += MarkAsChanged;
|
|
txtDbPassword.TextChanged += MarkAsChanged;
|
|
txtMinPoolSize.TextChanged += MarkAsChanged;
|
|
txtMaxPoolSize.TextChanged += MarkAsChanged;
|
|
|
|
// 預先設置文本框的Tag屬性,避免第一次失焦驗證時找不到Tag
|
|
txtDbIP.Tag = "localhost";
|
|
txtDbPort.Tag = "1433";
|
|
txtMinPoolSize.Tag = "50";
|
|
txtMaxPoolSize.Tag = "1000";
|
|
|
|
// 移除窗體點擊事件,它過於嚴格導致文本框無法獲得焦點
|
|
// this.PreviewMouseDown += Window_PreviewMouseDown;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"註冊事件時發生錯誤: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 恢復簡單的GotFocus處理方法,但不添加特殊處理邏輯
|
|
private void TextBox_GotFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
// 只做一件事:保存當前值到Tag
|
|
if (sender is TextBox textBox && !textBox.IsReadOnly)
|
|
{
|
|
textBox.Tag = textBox.Text;
|
|
}
|
|
}
|
|
|
|
private void TxtDbIP_GotFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
// 標記為已獲取焦點,避免重複處理
|
|
txtDbIP.Tag = txtDbIP.Text;
|
|
}
|
|
|
|
// IP文字框失去焦點時的驗證處理
|
|
private void TxtDbIP_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
// 使用Dispatcher.BeginInvoke確保UI響應
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
// 保存原始值
|
|
string originalValue = txtDbIP.Tag as string ?? "localhost";
|
|
string ipAddress = txtDbIP.Text.Trim();
|
|
|
|
// 允許使用 localhost
|
|
if (ipAddress.Equals("localhost", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// 更新保存的有效值
|
|
txtDbIP.Tag = ipAddress;
|
|
return;
|
|
}
|
|
|
|
// 驗證IPv4格式
|
|
if (!IsValidIPv4(ipAddress))
|
|
{
|
|
MessageBox.Show("請輸入有效的IPv4地址(如: 192.168.1.1)或 localhost\n\nPlease enter a valid IPv4 address (e.g. 192.168.1.1) or localhost", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
// 恢復為最後一個有效值
|
|
txtDbIP.Text = originalValue;
|
|
txtDbIP.Focus();
|
|
txtDbIP.SelectAll();
|
|
}
|
|
else
|
|
{
|
|
// 更新保存的有效值
|
|
txtDbIP.Tag = ipAddress;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 確保即使驗證出現問題也不會阻塞UI
|
|
_logger.Error($"IP驗證時發生錯誤: {ex.Message}");
|
|
}
|
|
}), DispatcherPriority.Background);
|
|
}
|
|
|
|
// 驗證IPv4地址格式
|
|
private bool IsValidIPv4(string ipAddress)
|
|
{
|
|
// IP地址格式檢查
|
|
if (string.IsNullOrWhiteSpace(ipAddress))
|
|
return false;
|
|
|
|
// 嘗試解析IP地址
|
|
if (System.Net.IPAddress.TryParse(ipAddress, out System.Net.IPAddress address))
|
|
{
|
|
// 確保是IPv4地址
|
|
if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
|
{
|
|
string[] parts = ipAddress.Split('.');
|
|
// 確保有四個部分且每個部分都是0-255的數字
|
|
if (parts.Length == 4)
|
|
{
|
|
foreach (string part in parts)
|
|
{
|
|
if (!int.TryParse(part, out int value) || value < 0 || value > 255)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void TxtDbPort_PreviewTextInput(object sender, TextCompositionEventArgs e)
|
|
{
|
|
// 只允許輸入數字
|
|
if (!char.IsDigit(e.Text, 0))
|
|
{
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
// 檢查輸入後的值是否超過範圍
|
|
string newText = txtDbPort.Text.Insert(txtDbPort.CaretIndex, e.Text);
|
|
if (int.TryParse(newText, out int port))
|
|
{
|
|
if (port > 65535)
|
|
{
|
|
e.Handled = true;
|
|
MessageBox.Show("Port 值不可大於 65535\n\nPort value cannot be greater than 65535", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TxtDbPort_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
// 使用Dispatcher.BeginInvoke確保UI響應
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
// 檢查 Port 值是否在有效範圍內
|
|
if (string.IsNullOrEmpty(txtDbPort.Text))
|
|
{
|
|
txtDbPort.Text = "1433";
|
|
return;
|
|
}
|
|
|
|
if (!int.TryParse(txtDbPort.Text, out int port))
|
|
{
|
|
txtDbPort.Text = "1433";
|
|
MessageBox.Show("Port 必須為數字\n\nPort must be a number", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
if (port < 100)
|
|
{
|
|
txtDbPort.Text = "1433";
|
|
MessageBox.Show("Port 值不可小於 100\n\nPort value cannot be less than 100", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
else if (port > 32765)
|
|
{
|
|
txtDbPort.Text = "1433";
|
|
MessageBox.Show("Port 值不可大於 32765\n\nPort value cannot be greater than 32765", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 確保即使驗證出現問題也不會阻塞UI
|
|
_logger.Error($"Port驗證時發生錯誤: {ex.Message}");
|
|
}
|
|
}), DispatcherPriority.Background);
|
|
}
|
|
|
|
private void TxtMinPoolSize_PreviewTextInput(object sender, TextCompositionEventArgs e)
|
|
{
|
|
// 只允許輸入數字
|
|
if (!char.IsDigit(e.Text, 0))
|
|
{
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
// 檢查輸入後的值是否超過範圍
|
|
string newText = txtMinPoolSize.Text.Insert(txtMinPoolSize.CaretIndex, e.Text);
|
|
if (int.TryParse(newText, out int value))
|
|
{
|
|
if (value > 32767)
|
|
{
|
|
e.Handled = true;
|
|
MessageBox.Show("Connection Pool大小必須在 1-32767 之間\n\nConnection Pool size must be between 1-32767", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TxtMinPoolSize_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
// 使用Dispatcher.BeginInvoke確保UI響應
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
// 檢查 MinPoolSize 值是否在有效範圍內
|
|
if (string.IsNullOrEmpty(txtMinPoolSize.Text))
|
|
{
|
|
txtMinPoolSize.Text = "50";
|
|
return;
|
|
}
|
|
|
|
if (!int.TryParse(txtMinPoolSize.Text, out int value))
|
|
{
|
|
txtMinPoolSize.Text = "50";
|
|
MessageBox.Show("Connection Pool大小必須為數字\n\nConnection Pool size must be a number", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
if (value < 1)
|
|
{
|
|
txtMinPoolSize.Text = "50";
|
|
MessageBox.Show("Connection Pool大小必須在 1-32767 之間\n\nConnection Pool size must be between 1-32767", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
else if (value > 32767)
|
|
{
|
|
txtMinPoolSize.Text = "50";
|
|
MessageBox.Show("Connection Pool大小必須在 1-32767 之間\n\nConnection Pool size must be between 1-32767", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 確保即使驗證出現問題也不會阻塞UI
|
|
_logger.Error($"MinPoolSize驗證時發生錯誤: {ex.Message}");
|
|
}
|
|
}), DispatcherPriority.Background);
|
|
}
|
|
|
|
private void TxtMaxPoolSize_PreviewTextInput(object sender, TextCompositionEventArgs e)
|
|
{
|
|
// 只允許輸入數字
|
|
if (!char.IsDigit(e.Text, 0))
|
|
{
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
// 檢查輸入後的值是否超過範圍
|
|
string newText = txtMaxPoolSize.Text.Insert(txtMaxPoolSize.CaretIndex, e.Text);
|
|
if (int.TryParse(newText, out int value))
|
|
{
|
|
if (value > 32767)
|
|
{
|
|
e.Handled = true;
|
|
MessageBox.Show("Connection Pool大小必須在 1-32767 之間\n\nConnection Pool size must be between 1-32767", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TxtMaxPoolSize_LostFocus(object sender, RoutedEventArgs e)
|
|
{
|
|
// 使用Dispatcher.BeginInvoke確保UI響應
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
// 檢查 MaxPoolSize 值是否在有效範圍內
|
|
if (string.IsNullOrEmpty(txtMaxPoolSize.Text))
|
|
{
|
|
txtMaxPoolSize.Text = "1000";
|
|
return;
|
|
}
|
|
|
|
if (!int.TryParse(txtMaxPoolSize.Text, out int value))
|
|
{
|
|
txtMaxPoolSize.Text = "1000";
|
|
MessageBox.Show("Connection Pool大小必須為數字\n\nConnection Pool size must be a number", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
if (value < 1)
|
|
{
|
|
txtMaxPoolSize.Text = "1000";
|
|
MessageBox.Show("Connection Pool大小必須在 1-32767 之間\n\nConnection Pool size must be between 1-32767", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
else if (value > 32767)
|
|
{
|
|
txtMaxPoolSize.Text = "1000";
|
|
MessageBox.Show("Connection Pool大小必須在 1-32767 之間\n\nConnection Pool size must be between 1-32767", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 確保即使驗證出現問題也不會阻塞UI
|
|
_logger.Error($"MaxPoolSize驗證時發生錯誤: {ex.Message}");
|
|
}
|
|
}), DispatcherPriority.Background);
|
|
}
|
|
|
|
private void LoadDatabaseSettings()
|
|
{
|
|
try
|
|
{
|
|
LogMessage($"開始讀取資料庫設定檔:{_configPath}");
|
|
|
|
if (!IOFile.Exists(_configPath))
|
|
{
|
|
LogMessage("設定檔不存在,將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
LogMessage("讀取設定檔...");
|
|
XDocument doc = XDocument.Load(_configPath);
|
|
LogMessage("設定檔讀取成功");
|
|
|
|
// 取得命名空間
|
|
XNamespace ns = doc.Root.GetDefaultNamespace();
|
|
LogMessage($"XML 命名空間:{ns}");
|
|
|
|
// 尋找 session-factory 元素
|
|
var sessionFactory = doc.Root.Element(ns + "session-factory");
|
|
if (sessionFactory == null)
|
|
{
|
|
LogMessage("設定檔中找不到 session-factory 元素,將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
// 尋找 connection.connection_string 屬性
|
|
var propertyElement = sessionFactory.Elements(ns + "property")
|
|
.FirstOrDefault(p => p.Attribute("name")?.Value == "connection.connection_string");
|
|
|
|
if (propertyElement == null)
|
|
{
|
|
LogMessage("設定檔中找不到 connection.connection_string 屬性,將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
string connString = propertyElement.Value.Trim();
|
|
if (string.IsNullOrEmpty(connString))
|
|
{
|
|
LogMessage("連線字串為空,將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
LogMessage("解析連線字串...");
|
|
|
|
try
|
|
{
|
|
var builder = new SqlConnectionStringBuilder(connString);
|
|
|
|
// 解析 DataSource 以取得 IP 和 PORT
|
|
string dataSource = builder.DataSource;
|
|
if (string.IsNullOrEmpty(dataSource))
|
|
{
|
|
LogMessage("資料來源為空,將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
string[] dataSourceParts = dataSource.Split(',');
|
|
|
|
string ip = dataSourceParts[0].Trim();
|
|
string port = "1433"; // 預設值
|
|
|
|
// 如果有指定 PORT,則使用指定的值
|
|
if (dataSourceParts.Length > 1)
|
|
{
|
|
port = dataSourceParts[1].Trim();
|
|
}
|
|
|
|
// 檢查IP是否有效
|
|
if (!ip.Equals("localhost", StringComparison.OrdinalIgnoreCase) && !IsValidIPv4(ip))
|
|
{
|
|
LogMessage($"設定檔中的IP格式無效:{ip},將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
// 檢查連接埠是否有效
|
|
if (!int.TryParse(port, out int portNum) || portNum < 1 || portNum > 65535)
|
|
{
|
|
LogMessage($"設定檔中的連接埠無效:{port},將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
// 檢查資料庫名稱是否有效
|
|
if (string.IsNullOrEmpty(builder.InitialCatalog))
|
|
{
|
|
LogMessage("設定檔中的資料庫名稱為空,將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
// 檢查使用者ID是否有效
|
|
if (string.IsNullOrEmpty(builder.UserID))
|
|
{
|
|
LogMessage("設定檔中的使用者ID為空,將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
// 檢查密碼是否有效
|
|
if (string.IsNullOrEmpty(builder.Password))
|
|
{
|
|
LogMessage("設定檔中的密碼為空,將使用預設值", true);
|
|
_isLoadedFromConfig = false;
|
|
return;
|
|
}
|
|
|
|
// 檢查連線池設定
|
|
if (builder.MinPoolSize < 1 || builder.MinPoolSize > 32767)
|
|
{
|
|
LogMessage($"設定檔中的最小連線池大小無效:{builder.MinPoolSize},將使用預設值", true);
|
|
builder.MinPoolSize = 50;
|
|
}
|
|
|
|
if (builder.MaxPoolSize < builder.MinPoolSize || builder.MaxPoolSize > 32767)
|
|
{
|
|
LogMessage($"設定檔中的最大連線池大小無效:{builder.MaxPoolSize},將使用預設值", true);
|
|
builder.MaxPoolSize = 1000;
|
|
}
|
|
|
|
// 更新 UI 控制項
|
|
txtDbIP.Text = ip;
|
|
txtDbIP.Tag = ip; // 保存有效的IP到Tag屬性
|
|
txtDbPort.Text = port;
|
|
txtDbName.Text = builder.InitialCatalog;
|
|
txtDbID.Text = builder.UserID;
|
|
txtDbPassword.Text = builder.Password;
|
|
txtMinPoolSize.Text = builder.MinPoolSize.ToString();
|
|
txtMaxPoolSize.Text = builder.MaxPoolSize.ToString();
|
|
|
|
LogMessage("連線字串解析完成:");
|
|
LogMessage($"- 資料庫伺服器:{txtDbIP.Text}:{txtDbPort.Text}");
|
|
LogMessage($"- 資料庫名稱:{txtDbName.Text}");
|
|
LogMessage($"- 使用者帳號:{txtDbID.Text}");
|
|
LogMessage($"- Connection Pool設定:Min={txtMinPoolSize.Text}, Max={txtMaxPoolSize.Text}");
|
|
|
|
// 更新記憶體中的連線字串
|
|
_connectionString = builder.ConnectionString;
|
|
|
|
// 更新 master 連線字串
|
|
var masterBuilder = new SqlConnectionStringBuilder(builder.ConnectionString);
|
|
masterBuilder.InitialCatalog = "master";
|
|
_masterConnectionString = masterBuilder.ConnectionString;
|
|
|
|
LogMessage("記憶體中的連線字串已更新");
|
|
_isLoadedFromConfig = true;
|
|
|
|
LogMessage("資料庫設定載入完成");
|
|
_hasUnsavedChanges = false; // 重置未保存變更標記
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"解析連線字串時發生錯誤:{ex.Message}", true);
|
|
LogMessage($"錯誤詳細資訊:{ex.StackTrace}", true);
|
|
LogMessage("將使用預設值");
|
|
_isLoadedFromConfig = false;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"載入資料庫設定時發生錯誤:{ex.Message}", true);
|
|
LogMessage($"錯誤詳細資訊:{ex.StackTrace}", true);
|
|
LogMessage("將使用預設值");
|
|
_isLoadedFromConfig = false;
|
|
MessageBox.Show($"載入資料庫設定時發生錯誤。\n請參閱操作記錄了解詳細錯誤資訊。\n\nAn error occurred while loading the database settings.\nPlease refer to the operation log for detailed error information.", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private void SetDefaultValues()
|
|
{
|
|
// 設定預設UI值
|
|
txtDbIP.Text = "localhost";
|
|
txtDbIP.Tag = "localhost"; // 保存有效的IP到Tag屬性
|
|
txtDbPort.Text = "1433";
|
|
txtDbName.Text = "BC_Dev";
|
|
txtDbID.Text = "sa";
|
|
txtDbPassword.Text = "p@ssw0rd";
|
|
txtMinPoolSize.Text = "50";
|
|
txtMaxPoolSize.Text = "1000";
|
|
|
|
// 設定預設連線字串
|
|
var builder = new SqlConnectionStringBuilder
|
|
{
|
|
DataSource = "localhost,1433",
|
|
InitialCatalog = "BC_Dev",
|
|
UserID = "sa",
|
|
Password = "p@ssw0rd",
|
|
MinPoolSize = 50,
|
|
MaxPoolSize = 1000
|
|
};
|
|
|
|
_connectionString = builder.ConnectionString;
|
|
|
|
// 設定預設 master 連線字串
|
|
var masterBuilder = new SqlConnectionStringBuilder(builder.ConnectionString);
|
|
masterBuilder.InitialCatalog = "master";
|
|
_masterConnectionString = masterBuilder.ConnectionString;
|
|
|
|
LogMessage("已設定預設連線字串值");
|
|
}
|
|
|
|
private void UpdateConnectionStrings()
|
|
{
|
|
try
|
|
{
|
|
// 如果不在 UI 執行緒上,則使用 Dispatcher 來執行
|
|
if (!Dispatcher.CheckAccess())
|
|
{
|
|
Dispatcher.Invoke(() => UpdateConnectionStrings());
|
|
return;
|
|
}
|
|
|
|
// 確保IP和PORT都是有效值
|
|
string ip = txtDbIP.Text.Trim();
|
|
string port = txtDbPort.Text.Trim();
|
|
|
|
if (string.IsNullOrEmpty(ip))
|
|
{
|
|
// 使用上一個有效值或預設值
|
|
ip = txtDbIP.Tag as string ?? "localhost";
|
|
txtDbIP.Text = ip;
|
|
LogMessage("警告:IP地址為空,恢復到上一個有效值", true);
|
|
}
|
|
else if (!ip.Equals("localhost", StringComparison.OrdinalIgnoreCase) && !IsValidIPv4(ip))
|
|
{
|
|
// 如果IP格式無效,恢復到上一個有效值
|
|
ip = txtDbIP.Tag as string ?? "localhost";
|
|
LogMessage("警告:IP地址格式無效,恢復到上一個有效值", true);
|
|
txtDbIP.Text = ip;
|
|
}
|
|
else
|
|
{
|
|
// 更新有效值標記
|
|
txtDbIP.Tag = ip;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(port))
|
|
{
|
|
port = "1433";
|
|
txtDbPort.Text = port;
|
|
LogMessage("警告:設定了預設的Port 1433");
|
|
}
|
|
|
|
// 確保最小池大小有效
|
|
if (!int.TryParse(txtMinPoolSize.Text, out int minPoolSize) || minPoolSize < 1 || minPoolSize > 32767)
|
|
{
|
|
txtMinPoolSize.Text = "50";
|
|
minPoolSize = 50;
|
|
LogMessage("警告:最小池大小無效,設定為預設值 50", true);
|
|
}
|
|
|
|
// 確保最大池大小有效
|
|
if (!int.TryParse(txtMaxPoolSize.Text, out int maxPoolSize) || maxPoolSize < minPoolSize || maxPoolSize > 32767)
|
|
{
|
|
txtMaxPoolSize.Text = "1000";
|
|
maxPoolSize = 1000;
|
|
LogMessage("警告:最大池大小無效,設定為預設值 1000", true);
|
|
}
|
|
|
|
// 建立資料來源字串
|
|
string dataSource = $"{ip},{port}";
|
|
|
|
// 建立完整的連線字串
|
|
var builder = new SqlConnectionStringBuilder
|
|
{
|
|
DataSource = dataSource,
|
|
InitialCatalog = txtDbName.Text.Trim(),
|
|
UserID = txtDbID.Text.Trim(),
|
|
Password = txtDbPassword.Text,
|
|
MinPoolSize = minPoolSize,
|
|
MaxPoolSize = maxPoolSize,
|
|
ApplicationName = "OHCS_BC_Controller_Admin" // 為資料庫設定頁添加特定的應用程式名稱
|
|
};
|
|
|
|
_connectionString = builder.ConnectionString;
|
|
|
|
// 更新 master 連線字串
|
|
var masterBuilder = new SqlConnectionStringBuilder(builder.ConnectionString);
|
|
masterBuilder.InitialCatalog = "master";
|
|
_masterConnectionString = masterBuilder.ConnectionString;
|
|
|
|
LogMessage($"已更新連線字串,資料來源: {dataSource}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"更新連線字串時發生錯誤:{ex.Message}", true);
|
|
LogMessage($"錯誤詳細資訊:{ex.StackTrace}", true);
|
|
|
|
// 發生異常時,如果之前有從配置檔案載入成功,則使用之前的連線字串
|
|
// 否則使用預設值
|
|
if (!_isLoadedFromConfig)
|
|
{
|
|
SetDefaultValues();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool ValidateDatabaseSettings()
|
|
{
|
|
try
|
|
{
|
|
// 檢查必要欄位
|
|
if (string.IsNullOrWhiteSpace(txtDbIP.Text))
|
|
{
|
|
LogMessage("錯誤:資料庫伺服器 IP 不能為空", true);
|
|
MessageBox.Show("資料庫伺服器 IP 不能為空\n\nDatabase server IP cannot be empty", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
|
|
// 驗證IP地址格式
|
|
string ip = txtDbIP.Text.Trim();
|
|
if (!ip.Equals("localhost", StringComparison.OrdinalIgnoreCase) && !IsValidIPv4(ip))
|
|
{
|
|
LogMessage("錯誤:資料庫伺服器 IP 格式無效", true);
|
|
// 恢復到上一個有效值
|
|
string validIP = txtDbIP.Tag as string ?? "localhost";
|
|
txtDbIP.Text = validIP;
|
|
ip = validIP;
|
|
MessageBox.Show("請輸入有效的IPv4地址(如: 192.168.1.1)或 localhost\n\nPlease enter a valid IPv4 address (e.g. 192.168.1.1) or localhost", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
txtDbIP.Focus();
|
|
txtDbIP.SelectAll();
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(txtDbPort.Text))
|
|
{
|
|
LogMessage("錯誤:資料庫伺服器 Port 不能為空", true);
|
|
MessageBox.Show("資料庫伺服器 Port 不能為空\n\nDatabase server Port cannot be empty", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(txtDbID.Text))
|
|
{
|
|
LogMessage("錯誤:資料庫使用者 ID 不能為空", true);
|
|
MessageBox.Show("資料庫使用者 ID 不能為空\n\nDatabase user ID cannot be empty", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(txtDbPassword.Text))
|
|
{
|
|
LogMessage("錯誤:資料庫密碼不能為空", true);
|
|
MessageBox.Show("資料庫密碼不能為空\n\nDatabase password cannot be empty", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
|
|
// 檢查 Min Pool 和 Max Pool
|
|
if (!int.TryParse(txtMinPoolSize.Text, out int minPool) ||
|
|
!int.TryParse(txtMaxPoolSize.Text, out int maxPool))
|
|
{
|
|
LogMessage("錯誤:連線池大小必須為數字", true);
|
|
MessageBox.Show("連線池大小必須為數字\n\nConnection Pool size must be a number", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
|
|
if (minPool > maxPool)
|
|
{
|
|
LogMessage("錯誤:最小連線池大小不能大於最大連線池大小", true);
|
|
MessageBox.Show("最小連線池大小不能大於最大連線池大小\n\nMinimum Connection Pool size cannot be greater than Maximum Connection Pool size", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"驗證資料庫設定時發生錯誤:{ex.Message}", true);
|
|
MessageBox.Show("驗證資料庫設定時發生錯誤。\n請參閱操作記錄了解詳細錯誤資訊。\n\nAn error occurred while validating the database settings.\nPlease refer to the operation log for detailed error information.", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void LogMessage(string message, bool isError = false)
|
|
{
|
|
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
string logMessage = $"[{timestamp}] {(isError ? "ERROR" : "INFO")}: {message}";
|
|
|
|
// 使用 NLog 輸出到檔案
|
|
if (isError)
|
|
{
|
|
_logger.Error(message);
|
|
}
|
|
else
|
|
{
|
|
_logger.Info(message);
|
|
}
|
|
|
|
// 使用 Dispatcher 確保在 UI 執行緒上更新
|
|
// 優先使用 Invoke 強制立即更新,而不是 BeginInvoke (異步更新)
|
|
if (Dispatcher.CheckAccess())
|
|
{
|
|
try
|
|
{
|
|
// 如果已經在UI線程上,直接更新
|
|
// 添加新日誌
|
|
txtLog.AppendText(logMessage + Environment.NewLine);
|
|
|
|
// 確保最新的日誌訊息可見
|
|
txtLog.CaretIndex = txtLog.Text.Length;
|
|
txtLog.ScrollToEnd();
|
|
|
|
// 強制更新 UI
|
|
txtLog.UpdateLayout();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine(logMessage);
|
|
// 防止日誌顯示錯誤導致整個程序崩潰
|
|
_logger.Error(ex, "添加日誌到 UI 時發生錯誤");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
// 異步時使用 Invoke 而非 BeginInvoke,確保UI更新不會延遲
|
|
Dispatcher.Invoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
// 添加新日誌
|
|
txtLog.AppendText(logMessage + Environment.NewLine);
|
|
|
|
// 確保最新的日誌訊息可見
|
|
txtLog.CaretIndex = txtLog.Text.Length;
|
|
txtLog.ScrollToEnd();
|
|
|
|
// 強制更新 UI
|
|
txtLog.UpdateLayout();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine(logMessage);
|
|
// 防止日誌顯示錯誤導致整個程序崩潰
|
|
_logger.Error(ex, "添加日誌到 UI 時發生錯誤");
|
|
}
|
|
}), DispatcherPriority.Send);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine(logMessage);
|
|
// 如果 Invoke 失敗,嘗試使用 BeginInvoke
|
|
_logger.Error(ex, "使用 Invoke 更新日誌失敗,嘗試 BeginInvoke");
|
|
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
// 添加新日誌
|
|
txtLog.AppendText(logMessage + Environment.NewLine);
|
|
|
|
// 確保最新的日誌訊息可見
|
|
txtLog.CaretIndex = txtLog.Text.Length;
|
|
txtLog.ScrollToEnd();
|
|
|
|
// 強制更新 UI
|
|
txtLog.UpdateLayout();
|
|
}
|
|
catch (Exception innerEx)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine(logMessage);
|
|
// 防止日誌顯示錯誤導致整個程序崩潰
|
|
_logger.Error(innerEx, "添加日誌到 UI 時發生錯誤");
|
|
}
|
|
}), DispatcherPriority.Send);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void ClearLog()
|
|
{
|
|
if (Dispatcher.CheckAccess())
|
|
{
|
|
try
|
|
{
|
|
txtLog.Clear();
|
|
// 強制更新 UI
|
|
txtLog.UpdateLayout();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 防止清除日誌時發生錯誤導致整個程序崩潰
|
|
_logger.Error(ex, "清除日誌時發生錯誤");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
// 使用 Invoke 而非 BeginInvoke 確保立即更新 UI
|
|
Dispatcher.Invoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
txtLog.Clear();
|
|
// 強制更新 UI
|
|
txtLog.UpdateLayout();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 防止清除日誌時發生錯誤導致整個程序崩潰
|
|
_logger.Error(ex, "清除日誌時發生錯誤");
|
|
}
|
|
}), DispatcherPriority.Send);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 如果 Invoke 失敗,嘗試使用 BeginInvoke
|
|
_logger.Error(ex, "使用 Invoke 清除日誌失敗,嘗試 BeginInvoke");
|
|
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
txtLog.Clear();
|
|
// 強制更新 UI
|
|
txtLog.UpdateLayout();
|
|
}
|
|
catch (Exception innerEx)
|
|
{
|
|
// 防止清除日誌時發生錯誤導致整個程序崩潰
|
|
_logger.Error(innerEx, "清除日誌時發生錯誤");
|
|
}
|
|
}), DispatcherPriority.Send);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 處理UI事件,防止UI凍結
|
|
/// </summary>
|
|
private void DoEvents()
|
|
{
|
|
try
|
|
{
|
|
// 使用 DispatcherFrame 創建一個新的消息循環
|
|
DispatcherFrame frame = new DispatcherFrame();
|
|
|
|
// 建立一個在消息循環中被執行的動作
|
|
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
|
|
new DispatcherOperationCallback(delegate (object parameter)
|
|
{
|
|
((DispatcherFrame)parameter).Continue = false;
|
|
return null;
|
|
}), frame);
|
|
|
|
// 啟動新的消息循環,處理所有挂起的UI事件
|
|
Dispatcher.PushFrame(frame);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"DoEvents失敗: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void btn_OK_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 檢查是否有未保存的變更
|
|
if (!CheckUnsavedChanges())
|
|
{
|
|
return; // 用戶取消操作
|
|
}
|
|
|
|
Close();
|
|
}
|
|
|
|
private void btn_Cancel_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 檢查是否有未保存的變更
|
|
if (!CheckUnsavedChanges())
|
|
{
|
|
return; // 用戶取消操作
|
|
}
|
|
Close();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 設置所有操作按鈕的可用性
|
|
/// </summary>
|
|
/// <param name="isEnabled">是否啟用按鈕</param>
|
|
/// <param name="operationName">當前正在執行的操作名稱(當禁用按鈕時)</param>
|
|
private void SetOperationButtonsState(bool isEnabled, string operationName = "")
|
|
{
|
|
Action updateAction = () =>
|
|
{
|
|
try
|
|
{
|
|
// 更新操作狀態標記和名稱
|
|
_isOperationInProgress = !isEnabled;
|
|
_currentOperation = isEnabled ? string.Empty : operationName;
|
|
|
|
// 更新所有操作按鈕的啟用狀態
|
|
btn_CreateDatabase.IsEnabled = isEnabled;
|
|
btn_UpdateDatabase.IsEnabled = isEnabled;
|
|
btn_BackupDatabase.IsEnabled = isEnabled;
|
|
btn_RestoreDatabase.IsEnabled = isEnabled;
|
|
btn_ClearBackup.IsEnabled = isEnabled;
|
|
btn_SaveSettings.IsEnabled = isEnabled;
|
|
btn_Close.IsEnabled = isEnabled;
|
|
btn_OpenConfigFile.IsEnabled = isEnabled;
|
|
btn_CheckFieldConsistency.IsEnabled = isEnabled; // 確保檢查按鈕也被禁用
|
|
|
|
// 如果禁用按鈕,顯示操作名稱
|
|
if (!isEnabled && !string.IsNullOrEmpty(operationName))
|
|
{
|
|
LogMessage($"正在執行{operationName}操作...");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"設置按鈕狀態時發生錯誤: {ex.Message}");
|
|
}
|
|
};
|
|
|
|
// 使用Dispatcher確保在UI線程上執行
|
|
if (Dispatcher.CheckAccess())
|
|
{
|
|
// 當前線程是UI線程,直接執行
|
|
updateAction();
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
// 使用 Invoke 而非 BeginInvoke 確保立即更新 UI
|
|
Dispatcher.Invoke(updateAction, DispatcherPriority.Send);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 如果 Invoke 失敗,嘗試使用 BeginInvoke
|
|
_logger.Error($"使用 Invoke 設置按鈕狀態失敗: {ex.Message}");
|
|
Dispatcher.BeginInvoke(updateAction, DispatcherPriority.Send);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 開啟資料庫設定檔案
|
|
/// </summary>
|
|
private void btn_OpenConfigFile_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
// 檢查檔案是否存在
|
|
if (!IOFile.Exists(_configPath))
|
|
{
|
|
MessageBox.Show($"找不到資料庫設定檔案:\n{_configPath}\n\nDatabase configuration file not found:\n{_configPath}", "檔案不存在 / File Not Found", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
LogMessage($"嘗試開啟資料庫設定檔案:{_configPath}");
|
|
|
|
// 使用系統默認程式開啟檔案
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = _configPath,
|
|
UseShellExecute = true
|
|
});
|
|
|
|
LogMessage("已開啟資料庫設定檔案");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"開啟資料庫設定檔案時發生錯誤:{ex.Message}", true);
|
|
|
|
// 嘗試使用記事本開啟
|
|
try
|
|
{
|
|
LogMessage("嘗試使用記事本開啟檔案...");
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = "notepad.exe",
|
|
Arguments = _configPath,
|
|
UseShellExecute = true
|
|
});
|
|
LogMessage("已使用記事本開啟檔案");
|
|
}
|
|
catch (Exception notepadEx)
|
|
{
|
|
LogMessage($"使用記事本開啟檔案失敗:{notepadEx.Message}", true);
|
|
MessageBox.Show($"無法開啟資料庫設定檔案。\n\n路徑:{_configPath}\n\n錯誤:{ex.Message}\n\nUnable to open the database configuration file.\n\nPath: {_configPath}\n\nError: {ex.Message}",
|
|
"開啟檔案失敗 / Failed to Open File", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void btn_CreateDatabase_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 檢查是否有未保存的變更
|
|
if (!CheckUnsavedChanges())
|
|
{
|
|
return; // 用戶取消操作
|
|
}
|
|
|
|
// 禁用所有按鈕
|
|
SetOperationButtonsState(false, "建立資料庫");
|
|
|
|
try
|
|
{
|
|
// 暫停 TraceDataReport Timer
|
|
PauseTraceDataReportTimer();
|
|
|
|
if (!ValidateDatabaseSettings())
|
|
{
|
|
return;
|
|
}
|
|
|
|
UpdateConnectionStrings();
|
|
ClearLog();
|
|
LogMessage("開始建立資料庫程序");
|
|
|
|
// 先確認是否要執行建立資料庫
|
|
LogMessage("詢問用戶是否確定要建立資料庫");
|
|
var confirmResult = MessageBox.Show("確定要建立資料庫嗎?\n\nAre you sure you want to create the database?", "確認操作 / Confirm Operation",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
|
|
|
if (confirmResult != MessageBoxResult.Yes)
|
|
{
|
|
LogMessage("使用者選擇取消建立資料庫操作");
|
|
return;
|
|
}
|
|
|
|
LogMessage("使用者確認要建立資料庫");
|
|
|
|
// 檢查ACJID和APJID表是否有資料(簡單判斷是否未清線狀況下作資料庫操作)
|
|
if (!CheckAccountTables())
|
|
{
|
|
LogMessage("使用者因資料表風險取消操作");
|
|
return;
|
|
}
|
|
|
|
// 檢查資料庫是否已存在
|
|
using (var connection = new SqlConnection(_masterConnectionString))
|
|
{
|
|
connection.Open();
|
|
using (var command = new SqlCommand("SELECT database_id FROM sys.databases WHERE Name = 'BC_Dev'", connection))
|
|
{
|
|
var dbExists = command.ExecuteScalar();
|
|
if (dbExists != null)
|
|
{
|
|
// 資料庫已存在,詢問是否要備份後重建
|
|
LogMessage("發現BC_Dev資料庫已存在,詢問用戶是否要備份後重建");
|
|
var backupResult = MessageBox.Show("資料庫已存在,是否要備份後重新建立?\n\nDatabase already exists. Do you want to backup and recreate it?", "確認操作 / Confirm Operation",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
|
|
|
if (backupResult == MessageBoxResult.Yes)
|
|
{
|
|
LogMessage("使用者選擇備份後重建資料庫");
|
|
// 執行備份
|
|
var result = await Task.Run(() => BackupDatabase());
|
|
if (result.ErrorMessage != null)
|
|
{
|
|
// 備份失敗,詢問是否繼續
|
|
LogMessage($"備份失敗:{result.ErrorMessage},詢問用戶是否直接刪除並重建", true);
|
|
var continueResult = MessageBox.Show($"備份失敗:{result.ErrorMessage}\n是否要直接刪除資料庫並重新建立?\n\nBackup failed: {result.ErrorMessage}\nDo you want to delete the database and recreate it directly?", "備份失敗 / Backup Failed",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
|
|
|
if (continueResult != MessageBoxResult.Yes)
|
|
{
|
|
LogMessage("使用者取消重建資料庫操作");
|
|
return;
|
|
}
|
|
|
|
LogMessage("使用者選擇直接刪除並重建資料庫");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogMessage("使用者選擇不備份,詢問是否直接刪除並重建");
|
|
// 詢問是否直接刪除資料庫
|
|
var deleteResult = MessageBox.Show("是否要直接刪除現有資料庫後重新建立?\n\nDo you want to delete the existing database and recreate it?", "確認操作 / Confirm Operation",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
|
|
|
if (deleteResult != MessageBoxResult.Yes)
|
|
{
|
|
LogMessage("使用者取消刪除並重建資料庫操作");
|
|
return;
|
|
}
|
|
|
|
LogMessage("使用者確認直接刪除並重建資料庫");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 顯示等待消息
|
|
var progressIndicator = new Progress<string>(message =>
|
|
{
|
|
LogMessage(message);
|
|
// 在長時間運行的操作中使用 DoEvents 讓 UI 有機會更新
|
|
DoEvents();
|
|
});
|
|
|
|
// 使用 CancellationTokenSource 來支持任務取消
|
|
var cts = new CancellationTokenSource();
|
|
|
|
try
|
|
{
|
|
// 新建資料庫時需要寫入基本值,並支持進度回報和取消
|
|
await Task.Run(async () => await CreateDatabaseWithProgress(true, progressIndicator, cts.Token), cts.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
LogMessage("建立資料庫操作已取消");
|
|
return;
|
|
}
|
|
|
|
// 執行資料欄位一致性檢查
|
|
LogMessage("開始進行資料欄位一致性檢查...");
|
|
bool hasInconsistentFields = false;
|
|
try
|
|
{
|
|
hasInconsistentFields = await Task.Run(() => CheckFieldConsistency(false));
|
|
|
|
if (hasInconsistentFields)
|
|
{
|
|
MessageBox.Show("資料庫建立成功,但發現欄位定義不一致的情況,詳情請查看日誌報告。\n\nDatabase created successfully, but field definition inconsistencies were found. Please check the log report for details.",
|
|
"建立完成但有警告",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Warning,
|
|
MessageBoxResult.OK,
|
|
MessageBoxOptions.DefaultDesktopOnly);
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show("資料庫建立成功!\n\nDatabase created successfully!",
|
|
"成功 / Success",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Information,
|
|
MessageBoxResult.OK,
|
|
MessageBoxOptions.DefaultDesktopOnly);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"檢查欄位定義失敗: {ex.Message}", true);
|
|
MessageBox.Show("資料庫建立成功!\n\nDatabase created successfully!",
|
|
"成功 / Success",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Information,
|
|
MessageBoxResult.OK,
|
|
MessageBoxOptions.DefaultDesktopOnly);
|
|
}
|
|
|
|
// 通知使用者程式即將關閉
|
|
MessageBox.Show("資料庫建立完成,程式即將關閉。\n請手動重新啟動程式才能生效。\n\nDatabase creation completed. The program will close.\nPlease restart the program manually for the changes to take effect.",
|
|
"系統提示 / System Message",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Information,
|
|
MessageBoxResult.OK,
|
|
MessageBoxOptions.DefaultDesktopOnly);
|
|
|
|
ShutdownApplication();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"建立資料庫失敗: {ex.Message}", true);
|
|
MessageBox.Show($"建立資料庫失敗。\n請參閱操作記錄了解詳細錯誤資訊。\n\nFailed to create database.\nPlease refer to the operation log for detailed error information.", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
finally
|
|
{
|
|
// 無論執行結果如何,確保按鈕狀態都會被恢復
|
|
SetOperationButtonsState(true);
|
|
// 恢復 TraceDataReport Timer
|
|
ResumeTraceDataReportTimer();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 建立資料庫基本方法
|
|
/// </summary>
|
|
private async Task CreateDatabase(bool insertInitialData = true)
|
|
{
|
|
LogMessage("開始建立資料庫");
|
|
|
|
// 連接到master資料庫
|
|
using (SqlConnection conn = new SqlConnection(_masterConnectionString))
|
|
{
|
|
conn.Open();
|
|
LogMessage("已連接到master資料庫");
|
|
|
|
// 檢查資料庫是否存在於 SQL Server
|
|
bool dbExistsInSQLServer = false;
|
|
using (var command = new SqlCommand("SELECT database_id FROM sys.databases WHERE Name = 'BC_Dev'", conn))
|
|
{
|
|
var dbExists = command.ExecuteScalar();
|
|
dbExistsInSQLServer = (dbExists != null);
|
|
}
|
|
|
|
// 如果資料庫存在於 SQL Server,則進行正規的刪除
|
|
if (dbExistsInSQLServer)
|
|
{
|
|
LogMessage("資料庫 BC_Dev 存在於 SQL Server,進行正規刪除");
|
|
try
|
|
{
|
|
using (var command = new SqlCommand(@"
|
|
ALTER DATABASE BC_Dev SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
|
DROP DATABASE BC_Dev;", conn))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("資料庫 BC_Dev 已成功刪除");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"刪除資料庫失敗: {ex.Message}", true);
|
|
throw new Exception($"無法刪除現有的資料庫。請檢查資料庫是否被使用中,或是否有足夠的權限。", ex);
|
|
}
|
|
}
|
|
|
|
// 檢查資料庫檔案是否存在,存在則刪除 (處理實體檔案存在但資料庫不存在於 SQL Server 的情況)
|
|
string defaultDataPath = string.Empty;
|
|
string defaultLogPath = string.Empty;
|
|
|
|
// 取得SQL Server的默認資料目錄
|
|
using (var command = new SqlCommand(@"
|
|
SELECT
|
|
SERVERPROPERTY('InstanceDefaultDataPath') AS DefaultDataPath,
|
|
SERVERPROPERTY('InstanceDefaultLogPath') AS DefaultLogPath", conn))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
if (reader.Read())
|
|
{
|
|
defaultDataPath = reader["DefaultDataPath"] as string ?? string.Empty;
|
|
defaultLogPath = reader["DefaultLogPath"] as string ?? string.Empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 若取得失敗,嘗試從系統目錄獲取
|
|
if (string.IsNullOrEmpty(defaultDataPath) || string.IsNullOrEmpty(defaultLogPath))
|
|
{
|
|
using (var command = new SqlCommand(@"
|
|
SELECT
|
|
physical_name,
|
|
type_desc
|
|
FROM sys.master_files
|
|
WHERE database_id = 1 AND type IN (0, 1)", conn))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
string filePath = reader["physical_name"].ToString();
|
|
string typeDesc = reader["type_desc"].ToString();
|
|
|
|
if (typeDesc == "ROWS" && string.IsNullOrEmpty(defaultDataPath))
|
|
{
|
|
defaultDataPath = IOPath.GetDirectoryName(filePath);
|
|
}
|
|
else if (typeDesc == "LOG" && string.IsNullOrEmpty(defaultLogPath))
|
|
{
|
|
defaultLogPath = IOPath.GetDirectoryName(filePath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果路徑仍然為空,則使用默認的SQL Server安裝路徑
|
|
if (string.IsNullOrEmpty(defaultDataPath))
|
|
{
|
|
// 嘗試使用SQL Server Express的默認路徑
|
|
defaultDataPath = @"C:\Program Files\Microsoft SQL Server\MSSQL15.SQLEXPRESS\MSSQL\DATA\";
|
|
LogMessage("無法取得SQL Server資料目錄,使用默認路徑: " + defaultDataPath);
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(defaultLogPath))
|
|
{
|
|
defaultLogPath = defaultDataPath; // 如果日誌路徑未定義,使用相同的資料路徑
|
|
}
|
|
|
|
// 構造MDF和LDF檔案路徑
|
|
string mdfPath = IOPath.Combine(defaultDataPath, "BC_Dev.mdf");
|
|
string ldfPath = IOPath.Combine(defaultLogPath, "BC_Dev_log.ldf");
|
|
|
|
// 檢查 MDF 檔案和 LDF 檔案是否存在
|
|
bool mdfExists = IOFile.Exists(mdfPath);
|
|
bool ldfExists = IOFile.Exists(ldfPath);
|
|
|
|
if (mdfExists || ldfExists)
|
|
{
|
|
if (!dbExistsInSQLServer)
|
|
{
|
|
LogMessage("發現殘留的資料庫實體檔案,但資料庫不存在於 SQL Server,準備清理殘留檔案");
|
|
}
|
|
else
|
|
{
|
|
LogMessage("檢查資料庫檔案");
|
|
}
|
|
}
|
|
|
|
// 刪除 MDF 檔案(如果存在)
|
|
if (mdfExists)
|
|
{
|
|
try
|
|
{
|
|
LogMessage($"發現資料庫MDF檔案,準備刪除: {mdfPath}");
|
|
IOFile.Delete(mdfPath);
|
|
LogMessage("MDF檔案刪除成功");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"無法刪除MDF檔案: {ex.Message}", true);
|
|
throw new Exception($"無法刪除現有的資料庫檔案({mdfPath})。請檢查檔案是否被使用中,或是否有足夠的權限。", ex);
|
|
}
|
|
}
|
|
|
|
// 刪除 LDF 檔案(如果存在)
|
|
if (ldfExists)
|
|
{
|
|
try
|
|
{
|
|
LogMessage($"發現資料庫LDF檔案,準備刪除: {ldfPath}");
|
|
IOFile.Delete(ldfPath);
|
|
LogMessage("LDF檔案刪除成功");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"無法刪除LDF檔案: {ex.Message}", true);
|
|
throw new Exception($"無法刪除現有的資料庫日誌檔案({ldfPath})。請檢查檔案是否被使用中,或是否有足夠的權限。", ex);
|
|
}
|
|
}
|
|
|
|
// 建立資料庫 (在交易外執行)
|
|
LogMessage("正在建立BC_Dev資料庫...");
|
|
try
|
|
{
|
|
using (var command = new SqlCommand("CREATE DATABASE BC_Dev COLLATE Chinese_Taiwan_Stroke_CI_AS", conn))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("BC_Dev資料庫建立完成");
|
|
|
|
// 檢查資料庫連線狀態
|
|
if (!await WaitForDatabaseConnection(_connectionString, "BC_Dev", 30, 1000, 10000))
|
|
{
|
|
LogMessage("無法連接到新建立的資料庫,建立資料庫失敗", true);
|
|
throw new Exception("無法連接到新建立的資料庫,請檢查SQL Server狀態或權限設定");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"建立資料庫失敗: {ex.Message}", true);
|
|
throw;
|
|
}
|
|
|
|
// 切換到BC_Dev資料庫
|
|
conn.ChangeDatabase("BC_Dev");
|
|
LogMessage("已切換到BC_Dev資料庫");
|
|
|
|
// 開始交易來建立表格和插入資料
|
|
using (var transaction = conn.BeginTransaction())
|
|
{
|
|
try
|
|
{
|
|
// 建立表格
|
|
CreateTables(conn, transaction);
|
|
|
|
// 只有在需要時才插入基本資料
|
|
if (insertInitialData)
|
|
{
|
|
InsertInitialData(conn, transaction);
|
|
}
|
|
|
|
// 提交交易
|
|
transaction.Commit();
|
|
LogMessage("資料庫建立完成");
|
|
|
|
// 計算建立的表格和視圖數量
|
|
int tableCount = 0;
|
|
int viewCount = 0;
|
|
|
|
using (var command = new SqlCommand(@"
|
|
SELECT
|
|
(SELECT COUNT(*) FROM sys.tables WHERE type = 'U') as TableCount,
|
|
(SELECT COUNT(*) FROM sys.views) as ViewCount", conn))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
if (reader.Read())
|
|
{
|
|
tableCount = reader.GetInt32(0);
|
|
viewCount = reader.GetInt32(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
LogMessage($"資料庫建立完成,共建立 {tableCount} 張Table和 {viewCount} 個View");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try
|
|
{
|
|
LogMessage($"發生錯誤,開始回滾交易: {ex.Message}", true);
|
|
transaction.Rollback();
|
|
LogMessage("交易回滾完成");
|
|
throw;
|
|
}
|
|
catch (Exception rollbackEx)
|
|
{
|
|
LogMessage($"回滾過程中發生錯誤: {rollbackEx.Message}", true);
|
|
throw new Exception($"建立資料庫時發生錯誤,且無法完全回滾,請參閱操作日誌", rollbackEx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 帶有進度回報功能的資料庫建立方法
|
|
/// </summary>
|
|
/// <param name="insertInitialData">是否插入初始資料</param>
|
|
/// <param name="progress">進度回報介面</param>
|
|
/// <param name="progress">進度回報介面</param>
|
|
/// <param name="cancellationToken">取消令牌</param>
|
|
private async Task CreateDatabaseWithProgress(bool insertInitialData = true, IProgress<string> progress = null, CancellationToken cancellationToken = default)
|
|
{
|
|
// 在長時間操作中檢查取消狀態和報告進度
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
// 直接調用原有方法
|
|
await CreateDatabase(insertInitialData);
|
|
}
|
|
|
|
//資料庫結構變動時,一定要來更新這裡的SQL語法
|
|
private void CreateTables(SqlConnection conn, SqlTransaction transaction)
|
|
{
|
|
//此段註解請勿移除
|
|
//SQL語法可以先在SSMS等Client工具匯出語句,對於比較不好閱讀的可以貼給AI去轉成一般SQL
|
|
//不要直接貼SSMS含GO的SQL
|
|
//SQL有變動,目前千萬要記得:
|
|
//1.HBM檔同步調整
|
|
//2.App.xaml.cs中的最小相容版本號(_minimumRequiredVersion)一定要更新為此次的版本
|
|
|
|
LogMessage("開始建立表格...");
|
|
string currentSql = string.Empty;
|
|
|
|
try
|
|
{
|
|
#region ABUFFER Table
|
|
// 建立ABUFFER表格
|
|
LogMessage("正在建立ABUFFER表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ABUFFER](
|
|
[BUFF_ID] [char](15) NOT NULL,
|
|
[UNIT_NUM] [int] NOT NULL,
|
|
[EQPT_ID] [char](15) NOT NULL,
|
|
[CAPACITY] [int] NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[BUFF_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ABUFFER表格建立完成");
|
|
#endregion
|
|
|
|
#region ACASSETTE Table
|
|
// 建立ACASSETTE表格
|
|
LogMessage("正在建立ACASSETTE表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ACASSETTE](
|
|
[CST_ID] [char](20) NOT NULL,
|
|
[CST_STAT] [char](1) NOT NULL,
|
|
[SHT_CNT] [int] NOT NULL,
|
|
[PORT_ID] [char](20) NOT NULL,
|
|
[PORT_NUM] [int] NOT NULL,
|
|
[LOT_ID] [char](22) NULL,
|
|
[SLOT_MAP] [char](36) NULL,
|
|
[SLOT_SEL] [char](36) NULL,
|
|
[END_STAT] [char](1) NULL,
|
|
[RPTED_EAP_CSTID] [char](1) NOT NULL,
|
|
[EAP_CHECK_SLOTMAP] [char](1) NOT NULL,
|
|
[EAP_USER_RELOAD] [char](1) NOT NULL,
|
|
[CSTLOGON_TIME] [datetime] NULL,
|
|
[CSTLOGOff_TIME] [datetime] NULL,
|
|
[ABNORMAL_CODE] [char](3) NULL,
|
|
[CSTID_READ_TYPE] [char](1) NULL,
|
|
[CDA_PURGET_STAT] [char](2) NULL,
|
|
[ISLOTSTART] [char](1) NOT NULL,
|
|
[CSTPROCSTART_TIME] [datetime] NULL,
|
|
[CSTPROCEND_TIME] [datetime] NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[CST_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ACASSETTE表格建立完成");
|
|
#endregion
|
|
|
|
#region ACELL Table
|
|
// 建立ACELL表格
|
|
LogMessage("正在建立ACELL表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ACELL](
|
|
[SHT_ID] [char](26) NOT NULL,
|
|
[CELL_ID] [char](20) NULL,
|
|
[CELL_JUDGE] [char](20) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SHT_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ACELL表格建立完成");
|
|
#endregion
|
|
|
|
#region ACJID Table
|
|
// 建立ACJID表格
|
|
LogMessage("正在建立ACJID表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ACJID](
|
|
[CJID] [char](100) NOT NULL,
|
|
[CJ_STATUS] [int] NOT NULL,
|
|
[CJ_AUTOSTART] [char](1) NULL,
|
|
[CJCREATE_TIME] [datetime] NULL,
|
|
[START_TIME] [datetime] NULL,
|
|
[END_TIME] [datetime] NULL,
|
|
[PROCESS_END_CNT] [int] NOT NULL,
|
|
[SCRAP_CNT] [int] NOT NULL,
|
|
[TOTAL_COUNT] [int] NOT NULL,
|
|
[END_RESULT] [char](2) NOT NULL,
|
|
[PJID_01] [char](50) NULL,
|
|
[PJID_02] [char](50) NULL,
|
|
[PJID_03] [char](50) NULL,
|
|
[PJID_04] [char](50) NULL,
|
|
[PJID_05] [char](50) NULL,
|
|
[PJID_06] [char](50) NULL,
|
|
[PJID_07] [char](50) NULL,
|
|
[PJID_08] [char](50) NULL,
|
|
[PJID_09] [char](50) NULL,
|
|
[PJID_10] [char](50) NULL,
|
|
[PJID_11] [char](50) NULL,
|
|
[PJID_12] [char](50) NULL,
|
|
[LDPROCTIME] [datetime] NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[CJID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ACJID表格建立完成");
|
|
#endregion
|
|
|
|
#region ACJPANELIDLST Table
|
|
// 建立ACJPANELIDLST表格、Index
|
|
LogMessage("正在建立ACJPANELIDLST表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ACJPANELIDLST](
|
|
[CJID] [char](100) NOT NULL,
|
|
[PANEL_ID] [char](26) NOT NULL,
|
|
[WORK_NO] [char](5) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[CJID] ASC,
|
|
[PANEL_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ACJPANELIDLST表格建立完成");
|
|
#endregion
|
|
|
|
#region ACRATE Table
|
|
// 建立ACRATE表格、Index
|
|
LogMessage("正在建立ACRATE表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ACRATE](
|
|
[CRATE_ID] [char](20) NOT NULL,
|
|
[MAKER] [char](20) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[CRATE_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ACRATE表格建立完成");
|
|
#endregion
|
|
|
|
#region AECDATAMAP Table
|
|
// 建立AECDATAMAP表格' Index
|
|
LogMessage("正在建立AECDATAMAP表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[AECDATAMAP](
|
|
[ECID] [char](4) NOT NULL,
|
|
[EQPT_REAL_ID] [char](15) NULL,
|
|
[ECNAME] [char](40) NULL,
|
|
[ECMIN] [char](20) NULL,
|
|
[ECMAX] [char](20) NULL,
|
|
[ECV] [char](20) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[ECID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("AECDATAMAP表格建立完成");
|
|
#endregion
|
|
|
|
#region AEQPT Table
|
|
// 建立AEQPT表格、Index
|
|
LogMessage("正在建立AEQPT表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[AEQPT](
|
|
[EQPT_ID] [char](15) NOT NULL,
|
|
[NODE_ID] [char](15) NOT NULL,
|
|
[CIM_MODE] [char](1) NOT NULL,
|
|
[OPER_MODE] [char](1) NOT NULL,
|
|
[INLINE_MODE] [char](1) NOT NULL,
|
|
[EQPT_STAT] [char](1) NOT NULL,
|
|
[EQPT_PROC_STAT] [char](1) NOT NULL,
|
|
[CR_RECIPE] [char](33) NULL,
|
|
[MAX_SHT_CNT] [int] NULL,
|
|
[MIN_SHT_CNT] [int] NULL,
|
|
[ALARM_STAT] [char](1) NULL,
|
|
[WARN_STAT] [char](1) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[EQPT_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("AEQPT表格建立完成");
|
|
#endregion
|
|
|
|
#region AEVENTRPTCOND Table
|
|
// 建立AEVENTRPTCOND表格、Index
|
|
LogMessage("正在建立AEVENTRPTCOND表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[AEVENTRPTCOND](
|
|
[CEID] [char](3) NOT NULL,
|
|
[ENABLE_FLG] [char](1) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[CEID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("AEVENTRPTCOND表格建立完成");
|
|
#endregion
|
|
|
|
#region AFLOW_REL Table
|
|
// 建立AFLOW_REL表格、Index
|
|
LogMessage("正在建立AFLOW_REL表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[AFLOW_REL](
|
|
[UPSTREAM_ID] [char](10) NOT NULL,
|
|
[DOWNSTREAM_ID] [char](10) NOT NULL,
|
|
[FR_ID] [char](10) NOT NULL,
|
|
[REL_TYPE] [char](1) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[UPSTREAM_ID] ASC,
|
|
[DOWNSTREAM_ID] ASC
|
|
),
|
|
UNIQUE NONCLUSTERED
|
|
(
|
|
[FR_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("AFLOW_REL表格建立完成");
|
|
#endregion
|
|
|
|
|
|
|
|
#region AHLOT Table
|
|
// 建立AHLOT表格、Index
|
|
LogMessage("正在建立AHLOT表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[AHLOT](
|
|
[SEQ_NO] [nvarchar](255) NOT NULL,
|
|
[LOT_ID] [char](22) NULL,
|
|
[T_STAMP] [char](19) NULL,
|
|
[CST_OPER_MODE] [char](1) NOT NULL,
|
|
[DUMMY_TYPE] [char](1) NOT NULL,
|
|
[LOT_JUDGE] [char](1) NULL,
|
|
[WORK_ORDER] [char](30) NULL,
|
|
[PROC_STAT] [char](1) NULL,
|
|
[LOT_START_PROC_TIME] [char](16) NULL,
|
|
[LOT_END_PROC_TIME] [char](16) NULL,
|
|
[PROC_SHT_CNT] [int] NULL,
|
|
[ESFLAG] [char](1) NULL,
|
|
[LOTSTATUS] [int] NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SEQ_NO] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("AHLOT表格建立完成");
|
|
#endregion
|
|
|
|
#region ALARM Table
|
|
// 建立ALARM表格
|
|
LogMessage("正在建立ALARM表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ALARM](
|
|
[EQPT_ID] [char](15) NOT NULL,
|
|
[UNIT_NUM] [int] NOT NULL,
|
|
[RPT_DATE_TIME] [char](19) NOT NULL,
|
|
[ALAM_CODE] [char](10) NOT NULL,
|
|
[ALAM_AFFECT_COUNT] [char](3) NOT NULL,
|
|
[ALAM_LVL] [char](1) NOT NULL,
|
|
[ALAM_STAT] [char](1) NOT NULL,
|
|
[ALAM_DESC] [char](80) NULL,
|
|
[CLEAR_DATE_TIME] [char](19) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[EQPT_ID] ASC,
|
|
[UNIT_NUM] ASC,
|
|
[RPT_DATE_TIME] ASC,
|
|
[ALAM_CODE] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ALARM表格建立完成");
|
|
#endregion
|
|
|
|
#region ALARMAFFECTGI Table
|
|
// 建立ALARMAFFECTGI表格
|
|
LogMessage("正在建立ALARMAFFECTGI表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ALARMAFFECTGI](
|
|
[EQPT_ID] [char](15) NOT NULL,
|
|
[RPT_DATE_TIME] [char](19) NOT NULL,
|
|
[GLASS_ID] [char](26) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[EQPT_ID] ASC,
|
|
[RPT_DATE_TIME] ASC,
|
|
[GLASS_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ALARMAFFECTGI表格建立完成");
|
|
#endregion
|
|
|
|
#region ALARMRPTCOND Table
|
|
// 建立ALARMRPTCOND表格
|
|
LogMessage("正在建立ALARMRPTCOND表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ALARMRPTCOND](
|
|
[EQPT_ID] [char](15) NOT NULL,
|
|
[ALAM_CODE] [char](10) NOT NULL,
|
|
[ENABLE_FLG] [char](1) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[EQPT_ID] ASC,
|
|
[ALAM_CODE] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ALARMRPTCOND表格建立完成");
|
|
#endregion
|
|
|
|
#region ALINE Table
|
|
// 建立ALINE表格
|
|
LogMessage("正在建立ALINE表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ALINE](
|
|
[LINE_ID] [char](25) NOT NULL,
|
|
[HOST_MODE] [char](1) NOT NULL,
|
|
[LINE_STAT] [char](1) NOT NULL,
|
|
[RUN_MODE] [char](2) NOT NULL,
|
|
[CUR_PPID] [char](40) NULL,
|
|
[CUR_CJID] [char](100) NULL,
|
|
[CUR_PJID] [char](20) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[LINE_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ALINE表格建立完成");
|
|
#endregion
|
|
|
|
#region ALOT Table
|
|
// 建立ALOT表格
|
|
LogMessage("正在建立ALOT表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ALOT](
|
|
[LOT_ID] [char](22) NOT NULL,
|
|
[CST_OPER_MODE] [char](1) NOT NULL,
|
|
[DUMMY_TYPE] [char](1) NOT NULL,
|
|
[LOT_JUDGE] [char](1) NULL,
|
|
[WORK_ORDER] [char](30) NULL,
|
|
[PROC_STAT] [char](1) NULL,
|
|
[LOT_START_PROC_TIME] [char](16) NULL,
|
|
[LOT_END_PROC_TIME] [char](16) NULL,
|
|
[PROC_SHT_CNT] [int] NULL,
|
|
[ESFLAG] [char](1) NULL,
|
|
[LOTSTATUS] [int] NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[LOT_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ALOT表格建立完成");
|
|
#endregion
|
|
|
|
#region ANODE Table
|
|
// 建立ANODE表格
|
|
LogMessage("正在建立ANODE表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ANODE](
|
|
[NODE_ID] [char](15) NOT NULL,
|
|
[ZONE_ID] [char](30) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[NODE_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ANODE表格建立完成");
|
|
#endregion
|
|
|
|
#region APJID Table
|
|
LogMessage("正在建立APJID表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[APJID](
|
|
[PJID] [char](50) NOT NULL,
|
|
[CSTID] [char](20) NOT NULL,
|
|
[PJ_STATUS] [int] NOT NULL,
|
|
[PJ_PRESTATUS] [int] NULL,
|
|
[AUTOSTART] [char](1) NULL,
|
|
[CJID] [char](100) NULL,
|
|
[RECIPEID] [char](4) NOT NULL,
|
|
[SEQ_NO] [int] NOT NULL,
|
|
[CREATE_TIME] [datetime] NOT NULL,
|
|
[PJIDSTART_TIME] [datetime] NULL,
|
|
[END_TIME] [datetime] NULL,
|
|
[SLOT_SELECT] [char](36) NULL,
|
|
[WORK_QTY] [char](3) NULL,
|
|
[SLOT_SELECT_NUM] [char](98) NULL,
|
|
[EQ_USAGE] [char](1) NULL,
|
|
[CS_SIDE_PROC] [char](1) NULL,
|
|
[RUN_MODE] [char](2) NULL,
|
|
[LOTX_VALUE] [char](12) NULL,
|
|
[LOTY_VALUE] [char](12) NULL,
|
|
[MF] [char](2) NULL,
|
|
[END_RESULT] [char](2) NOT NULL,
|
|
[IS_NORMAL_END] [char](1) NULL,
|
|
[WORKNO_01] [int] NULL,
|
|
[WORKNO_02] [int] NULL,
|
|
[WORKNO_03] [int] NULL,
|
|
[WORKNO_04] [int] NULL,
|
|
[WORKNO_05] [int] NULL,
|
|
[WORKNO_06] [int] NULL,
|
|
[WORKNO_07] [int] NULL,
|
|
[WORKNO_08] [int] NULL,
|
|
[WORKNO_09] [int] NULL,
|
|
[WORKNO_10] [int] NULL,
|
|
[WORKNO_11] [int] NULL,
|
|
[WORKNO_12] [int] NULL,
|
|
[WORKNO_13] [int] NULL,
|
|
[WORKNO_14] [int] NULL,
|
|
[WORKNO_15] [int] NULL,
|
|
[WORKNO_16] [int] NULL,
|
|
[WORKNO_17] [int] NULL,
|
|
[WORKNO_18] [int] NULL,
|
|
[WORKNO_19] [int] NULL,
|
|
[WORKNO_20] [int] NULL,
|
|
[WORKNO_21] [int] NULL,
|
|
[WORKNO_22] [int] NULL,
|
|
[WORKNO_23] [int] NULL,
|
|
[WORKNO_24] [int] NULL,
|
|
[WORKNO_25] [int] NULL,
|
|
[WORKNO_26] [int] NULL,
|
|
[WORKNO_27] [int] NULL,
|
|
[WORKNO_28] [int] NULL,
|
|
[WORKNO_29] [int] NULL,
|
|
[WORKNO_30] [int] NULL,
|
|
[WORKNO_31] [int] NULL,
|
|
[WORKNO_32] [int] NULL,
|
|
[WORKNO_33] [int] NULL,
|
|
[WORKNO_34] [int] NULL,
|
|
[WORKNO_35] [int] NULL,
|
|
[WORKNO_36] [int] NULL,
|
|
[SheetID_01] [char](26) NULL,
|
|
[SheetID_02] [char](26) NULL,
|
|
[SheetID_03] [char](26) NULL,
|
|
[SheetID_04] [char](26) NULL,
|
|
[SheetID_05] [char](26) NULL,
|
|
[SheetID_06] [char](26) NULL,
|
|
[SheetID_07] [char](26) NULL,
|
|
[SheetID_08] [char](26) NULL,
|
|
[SheetID_09] [char](26) NULL,
|
|
[SheetID_10] [char](26) NULL,
|
|
[SheetID_11] [char](26) NULL,
|
|
[SheetID_12] [char](26) NULL,
|
|
[SheetID_13] [char](26) NULL,
|
|
[SheetID_14] [char](26) NULL,
|
|
[SheetID_15] [char](26) NULL,
|
|
[SheetID_16] [char](26) NULL,
|
|
[SheetID_17] [char](26) NULL,
|
|
[SheetID_18] [char](26) NULL,
|
|
[SheetID_19] [char](26) NULL,
|
|
[SheetID_20] [char](26) NULL,
|
|
[SheetID_21] [char](26) NULL,
|
|
[SheetID_22] [char](26) NULL,
|
|
[SheetID_23] [char](26) NULL,
|
|
[SheetID_24] [char](26) NULL,
|
|
[SheetID_25] [char](26) NULL,
|
|
[SheetID_26] [char](26) NULL,
|
|
[SheetID_27] [char](26) NULL,
|
|
[SheetID_28] [char](26) NULL,
|
|
[SheetID_29] [char](26) NULL,
|
|
[SheetID_30] [char](26) NULL,
|
|
[SheetID_31] [char](26) NULL,
|
|
[SheetID_32] [char](26) NULL,
|
|
[SheetID_33] [char](26) NULL,
|
|
[SheetID_34] [char](26) NULL,
|
|
[SheetID_35] [char](26) NULL,
|
|
[SheetID_36] [char](26) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[PJID] ASC,
|
|
[CSTID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("APJID表格建立完成");
|
|
#endregion
|
|
|
|
#region APJPANELIDLST Table
|
|
// 建立APJPANELIDLST表格
|
|
LogMessage("正在建立APJPANELIDLST表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[APJPANELIDLST](
|
|
[PJID] [char](50) NOT NULL,
|
|
[PANEL_ID] [char](26) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[PJID] ASC,
|
|
[PANEL_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("APJPANELIDLST表格建立完成");
|
|
#endregion
|
|
|
|
#region APORT Table
|
|
// 建立APORT表格
|
|
LogMessage("正在建立APORT表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[APORT](
|
|
[PORT_ID] [char](20) NOT NULL,
|
|
[EQPT_ID] [char](15) NOT NULL,
|
|
[UNIT_NUM] [int] NOT NULL,
|
|
[PORT_NUM] [int] NOT NULL,
|
|
[PORT_TYPE] [char](1) NOT NULL,
|
|
[PORT_USE_TYPE] [char](2) NULL,
|
|
[PORT_REAL_TYPE] [char](2) NULL,
|
|
[CAPACITY] [int] NOT NULL,
|
|
[PORT_STAT] [char](1) NOT NULL,
|
|
[PORT_ENABLE] [char](1) NOT NULL,
|
|
[PORT_STATUS] [char](1) NOT NULL,
|
|
[ACCESSMODE] [char](1) NULL,
|
|
[PORT_ACTIONSTATE] [int] NOT NULL,
|
|
[PORT_CMDSTAT] [int] NOT NULL,
|
|
[SERVICE_MODE] [char](1) NULL,
|
|
[CJID] [char](100) NULL,
|
|
[PJID] [char](50) NULL,
|
|
[CJ_STATUS] [int] NULL,
|
|
[PJ_STATUS] [int] NULL,
|
|
[RECIPEID] [char](4) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[PORT_ID] ASC,
|
|
[EQPT_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("APORT表格建立完成");
|
|
#endregion
|
|
|
|
#region ASEQUENCE Table
|
|
// 建立ASEQUENCE表格
|
|
LogMessage("正在建立ASEQUENCE表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ASEQUENCE](
|
|
[SEQ_NAME] [char](12) NOT NULL,
|
|
[NXT_VAL] [bigint] NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SEQ_NAME] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ASEQUENCE表格建立完成");
|
|
#endregion
|
|
|
|
#region ASHEET Table
|
|
// 建立ASHEET表格
|
|
LogMessage("正在建立ASHEET表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ASHEET](
|
|
[SHT_ID] [char](26) NOT NULL,
|
|
[WORK_NO] [char](5) NOT NULL,
|
|
[FINAL_ID] [char](26) NULL,
|
|
[LOT_ID] [char](22) NULL,
|
|
[JOB_NO] [char](5) NOT NULL,
|
|
[PROD_ID] [char](20) NOT NULL,
|
|
[SHT_STAT] [int] NULL,
|
|
[SOURCE_CST_ID] [char](20) NULL,
|
|
[SOURCE_SLOT_NO] [int] NOT NULL,
|
|
[SOURCE_PORT_NO] [int] NOT NULL,
|
|
[TARGET_CST_ID] [char](20) NULL,
|
|
[TARGET_SLOT_NO] [int] NULL,
|
|
[TARGET_PORT_NO] [int] NULL,
|
|
[RECIPE_ID] [char](4) NULL,
|
|
[TAKE_OUT_STAT] [char](1) NULL,
|
|
[NODE_ID] [char](15) NULL,
|
|
[CST_ID] [char](20) NULL,
|
|
[SLOT_NO] [int] NOT NULL,
|
|
[TAKE_OUT_TIME] [char](16) NULL,
|
|
[SCRAP_CODE] [char](5) NULL,
|
|
[REASON_CODE] [char](5) NULL,
|
|
[LOGON_TIME] [datetime] NULL,
|
|
[LOGOff_TIME] [datetime] NULL,
|
|
[SELECTFLAG] [char](1) NULL,
|
|
[BCR_SHT_ID] [char](26) NULL,
|
|
[BCR_RESULT] [char](17) NULL,
|
|
[BCR_MISDISPO] [char](16) NULL,
|
|
[IPQC_SAMPLENO] [int] NULL,
|
|
[IPQC_RESULT] [char](16) NULL,
|
|
[USER_ID] [nvarchar](20) NULL,
|
|
[PROCESS_START_TIME] [char](16) NULL,
|
|
[PJID] [char](50) NULL,
|
|
[CJID] [char](100) NULL,
|
|
[DUMMY_CASE] [int] NULL,
|
|
[DUMMY_FLAG] [int] NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SHT_ID] ASC,
|
|
[WORK_NO] ASC
|
|
),
|
|
UNIQUE NONCLUSTERED
|
|
(
|
|
[WORK_NO] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ASHEET表格建立完成");
|
|
#endregion
|
|
|
|
#region ASHTDCS Table
|
|
// 建立ASHTDCS表格
|
|
LogMessage("正在建立ASHTDCS表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ASHTDCS](
|
|
[SHT_ID] [char](26) NOT NULL,
|
|
[WORK_NO] [char](5) NOT NULL,
|
|
[ST1] [char](15) NULL,
|
|
[ST1_UNIT] [char](15) NULL,
|
|
[ST1_RECIPE] [char](4) NULL,
|
|
[ST1_ONTIME] [datetime] NULL,
|
|
[ST1_OFFTIME] [datetime] NULL,
|
|
[ST1_TIMEOUT] [char](3) NULL,
|
|
[ST2] [char](15) NULL,
|
|
[ST2_UNIT] [char](15) NULL,
|
|
[ST2_RECIPE] [char](4) NULL,
|
|
[ST2_ONTIME] [datetime] NULL,
|
|
[ST2_OFFTIME] [datetime] NULL,
|
|
[ST2_TIMEOUT] [char](3) NULL,
|
|
[ST3] [char](15) NULL,
|
|
[ST3_UNIT] [char](15) NULL,
|
|
[ST3_RECIPE] [char](4) NULL,
|
|
[ST3_ONTIME] [datetime] NULL,
|
|
[ST3_OFFTIME] [datetime] NULL,
|
|
[ST3_TIMEOUT] [char](3) NULL,
|
|
[ST4] [char](15) NULL,
|
|
[ST4_UNIT] [char](15) NULL,
|
|
[ST4_RECIPE] [char](4) NULL,
|
|
[ST4_ONTIME] [datetime] NULL,
|
|
[ST4_OFFTIME] [datetime] NULL,
|
|
[ST4_TIMEOUT] [char](3) NULL,
|
|
[ST5] [char](15) NULL,
|
|
[ST5_UNIT] [char](15) NULL,
|
|
[ST5_RECIPE] [char](4) NULL,
|
|
[ST5_ONTIME] [datetime] NULL,
|
|
[ST5_OFFTIME] [datetime] NULL,
|
|
[ST5_TIMEOUT] [char](3) NULL,
|
|
[ST6] [char](15) NULL,
|
|
[ST6_UNIT] [char](15) NULL,
|
|
[ST6_RECIPE] [char](4) NULL,
|
|
[ST6_ONTIME] [datetime] NULL,
|
|
[ST6_OFFTIME] [datetime] NULL,
|
|
[ST6_TIMEOUT] [char](3) NULL,
|
|
[START_WAIT_TIME] [datetime] NULL,
|
|
[NEXTST] [char](1) NULL,
|
|
[TOTLAST] [char](1) NULL,
|
|
[NEXTEPQT] [char](15) NULL,
|
|
[NEXTUNIT] [char](15) NULL,
|
|
[CURRENTEQ] [char](15) NULL,
|
|
[CURRENTUNIT] [char](15) NULL,
|
|
[CURRENTSTAGE] [char](2) NULL,
|
|
[CURRENTSLOT] [char](2) NULL,
|
|
[ROBOTNO] [char](1) NULL,
|
|
[ONROBOT] [char](1) NULL,
|
|
[TURN_FLAG] [char](1) NULL,
|
|
[SCRAP_FLAG] [char](1) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SHT_ID] ASC,
|
|
[WORK_NO] ASC
|
|
),
|
|
UNIQUE NONCLUSTERED
|
|
(
|
|
[WORK_NO] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ASHTDCS表格建立完成");
|
|
#endregion
|
|
|
|
#region ASHTDESC Table
|
|
LogMessage("正在建立ASHTDESC表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE ASHTDESC (
|
|
SHT_ID CHAR(26) NOT NULL,
|
|
WORK_NO CHAR(5) NOT NULL,
|
|
ARRAY_REPAIR_TYPE CHAR(800) NULL,
|
|
LCVD_REPAIR_TYPE CHAR(800) NULL,
|
|
PANEL_JUDGE CHAR(800) NULL,
|
|
LOT_TOP CHAR(1) NULL,
|
|
LOT_BUTTOM CHAR(1) NULL,
|
|
SHT_THICKNESS FLOAT NULL,
|
|
SHT_SIZE CHAR(1) NULL,
|
|
MASK_ID CHAR(30) NULL,
|
|
MAKER_ID CHAR(20) NULL,
|
|
TURN_GLASS CHAR(1) NULL,
|
|
PROBER_ID CHAR(20) NULL,
|
|
EXP_UNIT_ID CHAR(20) NULL,
|
|
EXP_RECIPE_ID CHAR(30) NULL,
|
|
SHT_ID_TYPE CHAR(1) NULL,
|
|
SMPL_FLAG CHAR(1) NULL,
|
|
REWORK_CNT INT NULL DEFAULT 0,
|
|
DUMMY_USED_CNT INT NULL DEFAULT 0,
|
|
JOB_TYPE INT NULL DEFAULT 0,
|
|
LOT_STARTEND INT NULL,
|
|
LOT_JUDGE CHAR(1) NULL,
|
|
RECIPE_01 CHAR(2) NULL,
|
|
RECIPE_02 CHAR(2) NULL,
|
|
RECIPE_03 CHAR(2) NULL,
|
|
RECIPE_04 CHAR(2) NULL,
|
|
RECIPE_05 CHAR(2) NULL,
|
|
RECIPE_06 CHAR(2) NULL,
|
|
RECIPE_07 CHAR(2) NULL,
|
|
RECIPE_08 CHAR(2) NULL,
|
|
SHT_TYPE CHAR(1) NULL,
|
|
PRIMARY KEY CLUSTERED (SHT_ID, WORK_NO),
|
|
UNIQUE NONCLUSTERED (WORK_NO)
|
|
) ;";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ASHTDESC表格建立完成");
|
|
#endregion
|
|
|
|
#region ASHTPROCDATA Table
|
|
LogMessage("正在建立ASHTPROCDATA表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ASHTPROCDATA](
|
|
[SHT_ID] [char](26) NOT NULL,
|
|
[WORK_NO] [char](5) NOT NULL,
|
|
[EQPT_ID] [char](15) NOT NULL,
|
|
[PROCDATA_NAME] [char](30) NOT NULL,
|
|
[PROCDATA_VALUE] [char](30) NULL,
|
|
[UNIT] [char](15) NULL,
|
|
[NO] [char](4) NULL,
|
|
[EQ_Recipe_ID] [char](2) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SHT_ID] ASC,
|
|
[WORK_NO] ASC,
|
|
[EQPT_ID] ASC,
|
|
[PROCDATA_NAME] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ASHTPROCDATA表格建立完成");
|
|
#endregion
|
|
|
|
#region ASUBFRAME Table
|
|
// 建立ASUBFRAME表格
|
|
LogMessage("正在建立ASUBFRAME表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ASUBFRAME](
|
|
[SUB_NO] [int] NOT NULL,
|
|
[SUBFRAME_ID] [char](26) NOT NULL,
|
|
[Port_No] [int] NOT NULL,
|
|
[SHT_ID] [char](26) NULL,
|
|
[FINAL_ID] [char](26) NULL,
|
|
[LOT_ID] [char](22) NULL,
|
|
[CST_ID] [char](20) NULL,
|
|
[SOURCE_CST_ID] [char](20) NOT NULL,
|
|
[TARGET_CST_ID] [char](20) NULL,
|
|
[SLOT_NO] [int] NOT NULL,
|
|
[BCR_SUBFRAME_ID] [char](26) NULL,
|
|
[BCR_RESULT] [char](17) NULL,
|
|
[IS_USED_STAT] [int] NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SUB_NO] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ASUBFRAME表格建立完成");
|
|
#endregion
|
|
|
|
#region ATRACEITEM Table
|
|
LogMessage("正在建立ATRACEITEM表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ATRACEITEM](
|
|
[TRACE_ID] [char](5) NOT NULL,
|
|
[SVID] [char](6) NOT NULL,
|
|
[SORT_NO] [int] NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[TRACE_ID] ASC,
|
|
[SVID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ATRACEITEM表格建立完成");
|
|
#endregion
|
|
|
|
#region ATRACEITEMVALUE Table
|
|
LogMessage("正在建立ATRACEITEMVALUE表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ATRACEITEMVALUE](
|
|
[ID] [int] IDENTITY(1,1) NOT NULL,
|
|
[TRACE_ID] [char](5) NULL,
|
|
[SVID] [char](6) NULL,
|
|
[SMP_SEQUENCE] [int] NULL,
|
|
[ITEM_SEQUENCE] [int] NULL,
|
|
[SV] [nvarchar](max) NULL,
|
|
[SMP_TIME] [datetime] NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[ID] ASC
|
|
)
|
|
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ATRACEITEMVALUE表格建立完成");
|
|
#endregion
|
|
|
|
#region ATRACESET Table
|
|
LogMessage("正在建立ATRACESET表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[ATRACESET](
|
|
[TRACE_ID] [char](5) NOT NULL,
|
|
[SMP_PERIOD] [char](6) NULL,
|
|
[SMP_PERIOD_SEC] [bigint] NULL,
|
|
[TOTAL_SMP_CNT] [int] NULL,
|
|
[SMP_GRP_CNT] [int] NULL,
|
|
[SMP_CNT] [int] NULL,
|
|
[NX_SMP_TIME] [datetime] NULL,
|
|
[SMP_TIME] [datetime] NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[TRACE_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("ATRACESET表格建立完成");
|
|
#endregion
|
|
|
|
#region AUNIT Table
|
|
// 建立AUNIT表格
|
|
LogMessage("正在建立AUNIT表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[AUNIT](
|
|
[UNIT_ID] [char](15) NOT NULL,
|
|
[UNIT_NUM] [int] NOT NULL,
|
|
[EQPT_ID] [char](15) NOT NULL,
|
|
[UNIT_CATE] [char](1) NOT NULL,
|
|
[EQPT_TYPE] [char](1) NOT NULL,
|
|
[CAPACITY] [int] NOT NULL,
|
|
[UNIT_STAT] [char](1) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[UNIT_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("AUNIT表格建立完成");
|
|
#endregion
|
|
|
|
#region AZONE Table
|
|
// 建立AZONE表格
|
|
LogMessage("正在建立AZONE表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[AZONE](
|
|
[ZONE_ID] [char](30) NOT NULL,
|
|
[LINE_ID] [char](25) NULL,
|
|
[LOT_ID] [char](22) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[ZONE_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("AZONE表格建立完成");
|
|
#endregion
|
|
|
|
#region BCSTAT Table
|
|
// 建立BCSTAT表格
|
|
LogMessage("正在建立BCSTAT表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[BCSTAT](
|
|
[BC_ID] [char](6) NOT NULL,
|
|
[CLOSE_MODE] [char](1) NOT NULL,
|
|
[RUN_TIMESTAMP] [char](16) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[BC_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("BCSTAT表格建立完成");
|
|
#endregion
|
|
|
|
#region EFEMPPID Table
|
|
// 建立EFEMPPID表格
|
|
LogMessage("正在建立EFEMPPID表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[EFEMPPID](
|
|
[PPID] [char](80) NOT NULL,
|
|
[ST1_EQ] [char](15) NULL,
|
|
[ST1_RECIPE] [char](4) NULL,
|
|
[ST1_PREGET] [char](1) NULL,
|
|
[ST1_TIMEOUT] [char](3) NULL,
|
|
[ST2_EQ] [char](15) NULL,
|
|
[ST2_RECIPE] [char](4) NULL,
|
|
[ST2_PREGET] [char](1) NULL,
|
|
[ST2_TIMEOUT] [char](3) NULL,
|
|
[ST3_EQ] [char](15) NULL,
|
|
[ST3_RECIPE] [char](4) NULL,
|
|
[ST3_PREGET] [char](1) NULL,
|
|
[ST3_TIMEOUT] [char](3) NULL,
|
|
[ST4_EQ] [char](15) NULL,
|
|
[ST4_RECIPE] [char](4) NULL,
|
|
[ST4_PREGET] [char](1) NULL,
|
|
[ST4_TIMEOUT] [char](3) NULL,
|
|
[ST5_EQ] [char](15) NULL,
|
|
[ST5_RECIPE] [char](4) NULL,
|
|
[ST5_PREGET] [char](1) NULL,
|
|
[ST5_TIMEOUT] [char](3) NULL,
|
|
[ST6_EQ] [char](15) NULL,
|
|
[ST6_RECIPE] [char](4) NULL,
|
|
[ST6_PREGET] [char](1) NULL,
|
|
[ST6_TIMEOUT] [char](3) NULL,
|
|
[RECIPE_VERSION] [char](10) NULL,
|
|
[LCTIME] [char](20) NULL,
|
|
[LCUSER] [char](20) NULL,
|
|
[Q_TIME] [char](5) NULL,
|
|
[REMARK] [char](40) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[PPID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("EFEMPPID表格建立完成");
|
|
#endregion
|
|
|
|
#region HACASSETTE Table
|
|
// 建立HACASSETTE表格
|
|
LogMessage("正在建立HACASSETTE表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[HACASSETTE](
|
|
[T_STAMP] [char](19) NOT NULL,
|
|
[CST_ID] [char](20) NULL,
|
|
[SHT_CNT] [char](5) NOT NULL,
|
|
[CSTLOGON_TIME] [datetime] NULL,
|
|
[CSTLOGOFF_TIME] [datetime] NULL,
|
|
[CSTPROCSTART_TIME] [datetime] NULL,
|
|
[CSTPROCEND_TIME] [datetime] NULL,
|
|
[END_STAT] [char](1) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[T_STAMP] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("HACASSETTE表格建立完成");
|
|
#endregion
|
|
|
|
#region HACJID Table
|
|
// 建立HACJID表格
|
|
LogMessage("正在建立HACJID表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[HACJID](
|
|
[T_STAMP] [char](19) NOT NULL,
|
|
[CJID] [char](100) NULL,
|
|
[CJ_STATUS] [int] NOT NULL,
|
|
[AUTO_START] [char](1) NOT NULL,
|
|
[CREATE_TIME] [datetime] NOT NULL,
|
|
[START_TIME] [datetime] NULL,
|
|
[END_TIME] [datetime] NULL,
|
|
[PROCESS_END_CNT] [int] NOT NULL,
|
|
[SCRAP_CNT] [int] NOT NULL,
|
|
[TOTAL_COUNT] [int] NOT NULL,
|
|
[END_RESULT] [char](2) NOT NULL,
|
|
[PJID_01] [char](50) NULL,
|
|
[PJID_02] [char](50) NULL,
|
|
[PJID_03] [char](50) NULL,
|
|
[PJID_04] [char](50) NULL,
|
|
[PJID_05] [char](50) NULL,
|
|
[PJID_06] [char](50) NULL,
|
|
[PJID_07] [char](50) NULL,
|
|
[PJID_08] [char](50) NULL,
|
|
[PJID_09] [char](50) NULL,
|
|
[PJID_10] [char](50) NULL,
|
|
[PJID_11] [char](50) NULL,
|
|
[PJID_12] [char](50) NULL,
|
|
[LDPROCTIME] [datetime] NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[T_STAMP] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("HACJID表格建立完成");
|
|
#endregion
|
|
|
|
#region HAPJID Table
|
|
// 建立HAPJID表格
|
|
LogMessage("正在建立HAPJID表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[HAPJID](
|
|
[T_STAMP] [char](19) NOT NULL,
|
|
[PJID] [char](50) NULL,
|
|
[PJ_STATUS] [int] NOT NULL,
|
|
[PJ_PRESTATUS] [int] NULL,
|
|
[CSTID] [char](20) NULL,
|
|
[AUTOSTART] [char](1) NULL,
|
|
[CJID] [char](100) NULL,
|
|
[RECIPEID] [char](4) NOT NULL,
|
|
[SEQ_NO] [int] NOT NULL,
|
|
[CREATE_TIME] [datetime] NOT NULL,
|
|
[START_TIME] [datetime] NULL,
|
|
[END_TIME] [datetime] NULL,
|
|
[SLOT_SELECT] [char](36) NULL,
|
|
[WORK_QTY] [char](3) NULL,
|
|
[SLOT_SELECT_NUM] [char](98) NULL,
|
|
[EQ_USAGE] [char](1) NULL,
|
|
[CS_SIDE_PROC] [char](1) NULL,
|
|
[RUN_MODE] [char](2) NULL,
|
|
[LOTX_VALUE] [char](12) NULL,
|
|
[LOTY_VALUE] [char](12) NULL,
|
|
[MF] [char](2) NULL,
|
|
[END_RESULT] [char](2) NOT NULL,
|
|
[IS_NORMAL_END] [char](1) NULL,
|
|
[WORKNO_01] [int] NULL,
|
|
[WORKNO_02] [int] NULL,
|
|
[WORKNO_03] [int] NULL,
|
|
[WORKNO_04] [int] NULL,
|
|
[WORKNO_05] [int] NULL,
|
|
[WORKNO_06] [int] NULL,
|
|
[WORKNO_07] [int] NULL,
|
|
[WORKNO_08] [int] NULL,
|
|
[WORKNO_09] [int] NULL,
|
|
[WORKNO_10] [int] NULL,
|
|
[WORKNO_11] [int] NULL,
|
|
[WORKNO_12] [int] NULL,
|
|
[WORKNO_13] [int] NULL,
|
|
[WORKNO_14] [int] NULL,
|
|
[WORKNO_15] [int] NULL,
|
|
[WORKNO_16] [int] NULL,
|
|
[WORKNO_17] [int] NULL,
|
|
[WORKNO_18] [int] NULL,
|
|
[WORKNO_19] [int] NULL,
|
|
[WORKNO_20] [int] NULL,
|
|
[WORKNO_21] [int] NULL,
|
|
[WORKNO_22] [int] NULL,
|
|
[WORKNO_23] [int] NULL,
|
|
[WORKNO_24] [int] NULL,
|
|
[WORKNO_25] [int] NULL,
|
|
[WORKNO_26] [int] NULL,
|
|
[WORKNO_27] [int] NULL,
|
|
[WORKNO_28] [int] NULL,
|
|
[WORKNO_29] [int] NULL,
|
|
[WORKNO_30] [int] NULL,
|
|
[WORKNO_31] [int] NULL,
|
|
[WORKNO_32] [int] NULL,
|
|
[WORKNO_33] [int] NULL,
|
|
[WORKNO_34] [int] NULL,
|
|
[WORKNO_35] [int] NULL,
|
|
[WORKNO_36] [int] NULL,
|
|
[SheetID_01] [char](26) NULL,
|
|
[SheetID_02] [char](26) NULL,
|
|
[SheetID_03] [char](26) NULL,
|
|
[SheetID_04] [char](26) NULL,
|
|
[SheetID_05] [char](26) NULL,
|
|
[SheetID_06] [char](26) NULL,
|
|
[SheetID_07] [char](26) NULL,
|
|
[SheetID_08] [char](26) NULL,
|
|
[SheetID_09] [char](26) NULL,
|
|
[SheetID_10] [char](26) NULL,
|
|
[SheetID_11] [char](26) NULL,
|
|
[SheetID_12] [char](26) NULL,
|
|
[SheetID_13] [char](26) NULL,
|
|
[SheetID_14] [char](26) NULL,
|
|
[SheetID_15] [char](26) NULL,
|
|
[SheetID_16] [char](26) NULL,
|
|
[SheetID_17] [char](26) NULL,
|
|
[SheetID_18] [char](26) NULL,
|
|
[SheetID_19] [char](26) NULL,
|
|
[SheetID_20] [char](26) NULL,
|
|
[SheetID_21] [char](26) NULL,
|
|
[SheetID_22] [char](26) NULL,
|
|
[SheetID_23] [char](26) NULL,
|
|
[SheetID_24] [char](26) NULL,
|
|
[SheetID_25] [char](26) NULL,
|
|
[SheetID_26] [char](26) NULL,
|
|
[SheetID_27] [char](26) NULL,
|
|
[SheetID_28] [char](26) NULL,
|
|
[SheetID_29] [char](26) NULL,
|
|
[SheetID_30] [char](26) NULL,
|
|
[SheetID_31] [char](26) NULL,
|
|
[SheetID_32] [char](26) NULL,
|
|
[SheetID_33] [char](26) NULL,
|
|
[SheetID_34] [char](26) NULL,
|
|
[SheetID_35] [char](26) NULL,
|
|
[SheetID_36] [char](26) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[T_STAMP] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("HAPJID表格建立完成");
|
|
#endregion
|
|
|
|
#region HASHEET Table
|
|
LogMessage("正在建立HASHEET表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[HASHEET](
|
|
[SEQ_NO] [nvarchar](255) NOT NULL,
|
|
[T_STAMP] [char](19) NULL,
|
|
[SHT_ID] [char](26) NULL,
|
|
[FINAL_ID] [char](26) NULL,
|
|
[BCR_SHT_ID] [char](26) NULL,
|
|
[WORK_NO] [char](5) NOT NULL,
|
|
[LOT_ID] [char](22) NULL,
|
|
[JOB_NO] [char](5) NOT NULL,
|
|
[PROD_ID] [char](20) NOT NULL,
|
|
[SHT_STAT] [int] NULL,
|
|
[SOURCE_CST_ID] [char](20) NULL,
|
|
[SOURCE_SLOT_NO] [int] NOT NULL,
|
|
[SOURCE_PORT_NO] [int] NOT NULL,
|
|
[TARGET_CST_ID] [char](20) NULL,
|
|
[TARGET_SLOT_NO] [int] NULL,
|
|
[TARGET_PORT_NO] [int] NULL,
|
|
[RECIPE_ID] [char](4) NULL,
|
|
[TAKE_OUT_STAT] [char](1) NULL,
|
|
[NODE_ID] [char](15) NULL,
|
|
[CST_ID] [char](20) NULL,
|
|
[SLOT_NO] [int] NOT NULL,
|
|
[TAKE_OUT_TIME] [char](16) NULL,
|
|
[SCRAP_CODE] [char](5) NULL,
|
|
[REASON_CODE] [char](5) NULL,
|
|
[LOGON_TIME] [datetime] NULL,
|
|
[LOGOff_TIME] [datetime] NULL,
|
|
[SELECTFLAG] [char](1) NULL,
|
|
[BCR_RESULT] [char](17) NULL,
|
|
[BCR_MISDISPO] [char](16) NULL,
|
|
[IPQC_RESULT] [char](16) NULL,
|
|
[USER_ID] [nvarchar](20) NULL,
|
|
[PJID] [char](50) NULL,
|
|
[CJID] [char](100) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SEQ_NO] ASC
|
|
),
|
|
UNIQUE NONCLUSTERED
|
|
(
|
|
[WORK_NO] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("HASHEET表格建立完成");
|
|
#endregion
|
|
|
|
#region HASHTDCS Table
|
|
LogMessage("正在建立HASHTDCS表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[HASHTDCS](
|
|
[SEQ_NO] [nvarchar](255) NOT NULL,
|
|
[T_STAMP] [char](19) NULL,
|
|
[SHT_ID] [char](26) NULL,
|
|
[WORK_NO] [char](5) NULL,
|
|
[ST1] [char](15) NULL,
|
|
[ST1_UNIT] [char](15) NULL,
|
|
[ST1_RECIPE] [char](4) NULL,
|
|
[ST1_ONTIME] [datetime] NULL,
|
|
[ST1_OFFTIME] [datetime] NULL,
|
|
[ST1_TIMEOUT] [char](3) NULL,
|
|
[ST2] [char](15) NULL,
|
|
[ST2_UNIT] [char](15) NULL,
|
|
[ST2_RECIPE] [char](4) NULL,
|
|
[ST2_ONTIME] [datetime] NULL,
|
|
[ST2_OFFTIME] [datetime] NULL,
|
|
[ST2_TIMEOUT] [char](3) NULL,
|
|
[ST3] [char](15) NULL,
|
|
[ST3_UNIT] [char](15) NULL,
|
|
[ST3_RECIPE] [char](4) NULL,
|
|
[ST3_ONTIME] [datetime] NULL,
|
|
[ST3_OFFTIME] [datetime] NULL,
|
|
[ST3_TIMEOUT] [char](3) NULL,
|
|
[ST4] [char](15) NULL,
|
|
[ST4_UNIT] [char](15) NULL,
|
|
[ST4_RECIPE] [char](4) NULL,
|
|
[ST4_ONTIME] [datetime] NULL,
|
|
[ST4_OFFTIME] [datetime] NULL,
|
|
[ST4_TIMEOUT] [char](3) NULL,
|
|
[ST5] [char](15) NULL,
|
|
[ST5_UNIT] [char](15) NULL,
|
|
[ST5_RECIPE] [char](4) NULL,
|
|
[ST5_ONTIME] [datetime] NULL,
|
|
[ST5_OFFTIME] [datetime] NULL,
|
|
[ST5_TIMEOUT] [char](3) NULL,
|
|
[ST6] [char](15) NULL,
|
|
[ST6_UNIT] [char](15) NULL,
|
|
[ST6_RECIPE] [char](4) NULL,
|
|
[ST6_ONTIME] [datetime] NULL,
|
|
[ST6_OFFTIME] [datetime] NULL,
|
|
[ST6_TIMEOUT] [char](3) NULL,
|
|
[START_WAIT_TIME] [datetime] NULL,
|
|
[NEXTST] [char](1) NULL,
|
|
[TOTLAST] [char](1) NULL,
|
|
[NEXTEPQT] [char](15) NULL,
|
|
[NEXTUNIT] [char](15) NULL,
|
|
[CURRENTEQ] [char](15) NULL,
|
|
[CURRENTUNIT] [char](15) NULL,
|
|
[CURRENTSTAGE] [char](2) NULL,
|
|
[CURRENTSLOT] [char](2) NULL,
|
|
[ROBOTNO] [char](1) NULL,
|
|
[ONROBOT] [char](1) NULL,
|
|
[TURN_FLAG] [char](1) NULL,
|
|
[SCRAP_FLAG] [char](1) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SEQ_NO] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("HASHTDCS表格建立完成");
|
|
#endregion
|
|
|
|
#region HASHTDESC Table
|
|
LogMessage("正在建立HASHTDESC表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE HASHTDESC (
|
|
SEQ_NO NVARCHAR(255) NOT NULL,
|
|
T_STAMP CHAR(19) NULL,
|
|
SHT_ID CHAR(26) NULL,
|
|
WORK_NO CHAR(5) NULL,
|
|
ARRAY_REPAIR_TYPE CHAR(800) NULL,
|
|
LCVD_REPAIR_TYPE CHAR(800) NULL,
|
|
PANEL_JUDGE CHAR(800) NULL,
|
|
LOT_TOP CHAR(1) NULL,
|
|
LOT_BUTTOM CHAR(1) NULL,
|
|
SHT_THICKNESS FLOAT NULL,
|
|
SHT_SIZE CHAR(1) NULL,
|
|
MASK_ID CHAR(30) NULL,
|
|
MAKER_ID CHAR(20) NULL,
|
|
TURN_GLASS CHAR(1) NULL,
|
|
PROBER_ID CHAR(20) NULL,
|
|
EXP_UNIT_ID CHAR(20) NULL,
|
|
EXP_RECIPE_ID CHAR(30) NULL,
|
|
SHT_ID_TYPE CHAR(1) NULL,
|
|
SMPL_FLAG CHAR(1) NULL,
|
|
REWORK_CNT INT NULL DEFAULT 0,
|
|
DUMMY_USED_CNT INT NULL DEFAULT 0,
|
|
JOB_TYPE INT NULL DEFAULT 0,
|
|
LOT_STARTEND CHAR(1) NULL,
|
|
LOT_JUDGE CHAR(1) NULL,
|
|
RECIPE_01 CHAR(2) NULL,
|
|
RECIPE_02 CHAR(2) NULL,
|
|
RECIPE_03 CHAR(2) NULL,
|
|
RECIPE_04 CHAR(2) NULL,
|
|
RECIPE_05 CHAR(2) NULL,
|
|
RECIPE_06 CHAR(2) NULL,
|
|
RECIPE_07 CHAR(2) NULL,
|
|
RECIPE_08 CHAR(2) NULL,
|
|
SHT_TYPE CHAR(1) NULL,
|
|
PRIMARY KEY CLUSTERED (SEQ_NO)
|
|
)";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("HASHTDESC表格建立完成");
|
|
#endregion
|
|
|
|
#region HASUBFRAME Table
|
|
LogMessage("正在建立HASUBFRAME表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[HASUBFRAME](
|
|
[SEQ_NO] [nvarchar](255) NOT NULL,
|
|
[T_STAMP] [char](19) NULL,
|
|
[SUB_NO] [int] NOT NULL,
|
|
[SUBFRAME_ID] [char](26) NOT NULL,
|
|
[Port_No] [int] NOT NULL,
|
|
[SHT_ID] [char](26) NULL,
|
|
[FINAL_ID] [char](26) NULL,
|
|
[LOT_ID] [char](22) NULL,
|
|
[CST_ID] [char](20) NULL,
|
|
[SOURCE_CST_ID] [char](20) NOT NULL,
|
|
[TARGET_CST_ID] [char](20) NULL,
|
|
[SLOT_NO] [int] NOT NULL,
|
|
[BCR_SUBFRAME_ID] [char](26) NULL,
|
|
[BCR_RESULT] [char](17) NULL,
|
|
[IS_USED_STAT] [int] NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SEQ_NO] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("HASUBFRAME表格建立完成");
|
|
#endregion
|
|
|
|
#region HOPERATION Table
|
|
// 建立HOPERATION表格
|
|
LogMessage("正在建立HOPERATION表格...");
|
|
// 使用currentSql變數儲存SQL語句,方便例外時記錄
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[HOPERATION](
|
|
[SEQ_NO] [nvarchar](255) NOT NULL,
|
|
[T_STAMP] [char](19) NULL,
|
|
[USER_ID] [nvarchar](20) NULL,
|
|
[FORM_NAME] [nvarchar](30) NULL,
|
|
[ACTION] [nvarchar](max) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[SEQ_NO] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("HOPERATION表格建立完成");
|
|
#endregion
|
|
|
|
#region UASFNC Table
|
|
// 建立UASFNC表格
|
|
LogMessage("正在建立UASFNC表格...");
|
|
// 使用currentSql變數儲存SQL語句,方便例外時記錄
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[UASFNC](
|
|
[FUNC_CODE] [nvarchar](60) NOT NULL,
|
|
[FUNC_NAME] [nvarchar](80) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[FUNC_CODE] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("UASFNC表格建立完成");
|
|
#endregion
|
|
|
|
#region UASUFNC Table
|
|
// 建立UASUFNC表格
|
|
LogMessage("正在建立UASUFNC表格...");
|
|
// 使用currentSql變數儲存SQL語句,方便例外時記錄
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[UASUFNC](
|
|
[USER_GRP] [nvarchar](20) NOT NULL,
|
|
[FUNC_CODE] [nvarchar](60) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[USER_GRP] ASC,
|
|
[FUNC_CODE] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("UASUFNC表格建立完成");
|
|
#endregion
|
|
|
|
#region UASUSR Table
|
|
// 建立UASUSR表格
|
|
LogMessage("正在建立UASUSR表格...");
|
|
// 使用currentSql變數儲存SQL語句,方便例外時記錄
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[UASUSR](
|
|
[USER_ID] [nvarchar](20) NOT NULL,
|
|
[PASSWD] [nvarchar](max) NULL,
|
|
[BADGE_NUMBER] [nvarchar](80) NULL,
|
|
[USER_NAME] [nvarchar](30) NULL,
|
|
[DISABLE_FLG] [char](1) NULL,
|
|
[POWER_USER_FLG] [char](1) NULL,
|
|
[ADMIN_FLG] [char](1) NULL,
|
|
[USER_GRP] [nvarchar](20) NULL,
|
|
[DEPARTMENT] [nvarchar](20) NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[USER_ID] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("UASUSR表格建立完成");
|
|
#endregion
|
|
|
|
#region UASUSRGRP Table
|
|
// 建立UASUSRGRP表格
|
|
LogMessage("正在建立UASUSRGRP表格...");
|
|
// 使用currentSql變數儲存SQL語句,方便例外時記錄
|
|
currentSql = $@"
|
|
CREATE TABLE [dbo].[UASUSRGRP](
|
|
[USER_GRP] [nvarchar](20) NOT NULL,
|
|
PRIMARY KEY CLUSTERED
|
|
(
|
|
[USER_GRP] ASC
|
|
)
|
|
) ON [PRIMARY]";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("UASUSRGRP表格建立完成");
|
|
#endregion
|
|
|
|
#region UNITPOSITION Table
|
|
// 建立UNITPOSITION表格
|
|
LogMessage("正在建立UNITPOSITION表格...");
|
|
// 使用currentSql變數儲存SQL語句,方便例外時記錄
|
|
currentSql = $@"
|
|
CREATE TABLE UNITPOSITION (
|
|
EQPT_REAL_ID CHAR(15) NOT NULL,
|
|
POSITION CHAR(2) NOT NULL,
|
|
UPDATETIME CHAR(19) NULL,
|
|
UNIT_ID CHAR(15) NULL,
|
|
UNIT_NO CHAR(2) NULL,
|
|
SLOT_NO CHAR(4) NULL,
|
|
TYPE CHAR(1) NULL,
|
|
UNIT_STATE CHAR(15) NULL,
|
|
GLASS_ID CHAR(26) NULL,
|
|
UNIT_RESULT CHAR(15) NULL,
|
|
DESCRIPTION CHAR(40) NULL,
|
|
PRIMARY KEY CLUSTERED (EQPT_REAL_ID, POSITION)
|
|
);";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("UNITPOSITION表格建立完成");
|
|
#endregion
|
|
|
|
#region VERSION Table
|
|
// 建立VERSION表格
|
|
LogMessage("正在建立VERSION表格...");
|
|
currentSql = $@"
|
|
CREATE TABLE VERSION (
|
|
VERSION NVARCHAR(25) NULL
|
|
)";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("VERSION表格建立完成");
|
|
|
|
// 寫入當前版本號
|
|
currentSql = $@"
|
|
INSERT INTO VERSION (VERSION) VALUES (@version)";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.Parameters.AddWithValue("@version", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString());
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("版本號寫入完成");
|
|
#endregion
|
|
|
|
#region VSHEET View
|
|
// 建立VSHEET View
|
|
LogMessage("正在建立VSHEET View...");
|
|
currentSql = $@"
|
|
IF EXISTS (SELECT * FROM sys.views WHERE name = 'VSHEET')
|
|
BEGIN
|
|
DROP VIEW VSHEET
|
|
END";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("已刪除舊的VSHEET View");
|
|
|
|
currentSql = $@"
|
|
CREATE VIEW VSHEET AS
|
|
Select s.SHT_ID, s.WORK_NO, s.JOB_NO, s.PROD_ID, s.SHT_STAT, s.SOURCE_CST_ID, s.SOURCE_SLOT_NO, s.SOURCE_PORT_NO,
|
|
s.TARGET_CST_ID, s.TARGET_SLOT_NO, s.TARGET_PORT_NO, s.RECIPE_ID, s.BCR_Result, s.IPQC_SampleNo, s.IPQC_Result,
|
|
s.TAKE_OUT_STAT,
|
|
s.CST_ID, s.SLOT_NO, s.TAKE_OUT_TIME, s.CJID, s.PJID,
|
|
c.CURRENTEQ, c.CURRENTUNIT, c.CURRENTSTAGE, c.CURRENTSLOT, c.ROBOTNO, c.ONROBOT,
|
|
e.CSTLOGON_TIME, e.CSTLOGOFF_TIME, e.CST_STAT, e.CSTPROCSTART_TIME, e.CSTPROCEND_TIME,
|
|
f.PJIDSTART_TIME,
|
|
g.CJCREATE_TIME, g.CJ_AUTOSTART
|
|
FROM ASHEET AS s
|
|
LEFT JOIN ASHTDCS AS c ON s.SHT_ID = c.SHT_ID
|
|
LEFT JOIN ACASSETTE AS e ON s.SOURCE_CST_ID = e.CST_ID
|
|
LEFT JOIN APJID AS f ON s.PJID = f.PJID
|
|
LEFT JOIN ACJID AS g ON s.CJID = g.CJID";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("VSHEET View建立完成");
|
|
#endregion
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"建立表格時發生錯誤: {ex.Message}", true);
|
|
LogMessage($"錯誤詳細資訊: {ex.StackTrace}", true);
|
|
LogMessage($"發生錯誤的SQL語句: {currentSql}", true);
|
|
|
|
throw; // 重新拋出例外,讓上層處理交易回滾
|
|
}
|
|
}
|
|
|
|
private void InsertInitialData(SqlConnection conn, SqlTransaction transaction)
|
|
{
|
|
string currentSql = string.Empty;
|
|
try
|
|
{
|
|
LogMessage("開始插入基本資料...");
|
|
|
|
#region UASFNC Table
|
|
// 插入UASFNC基本資料
|
|
LogMessage("正在插入UASFNC基本資料...");
|
|
currentSql = @"
|
|
INSERT INTO UASFNC (FUNC_CODE, FUNC_NAME) VALUES
|
|
(N'FUNC_ALARM_INFO_QUERY', N'Alarm Information'),
|
|
(N'FUNC_ALARM_MAINTENANCE', N'Alarm Maintenance'),
|
|
(N'FUNC_CJPJ_MAINTENANCE', N'CJ/PJ Maintenance'),
|
|
(N'FUNC_CJPJINFORMATION', N'CJ/PJ Information'),
|
|
(N'FUNC_CLOSE_BC', N'System Close'),
|
|
(N'FUNC_COMMAND_INFO_QUERY', N'Command Information'),
|
|
(N'FUNC_CONNECTION_CONTROL', N'Connection Control'),
|
|
(N'FUNC_CST_DATA_OPERATION', N'CST Data Operation'),
|
|
(N'FUNC_CST_INFO_QUERY', N'CST Information'),
|
|
(N'FUNC_DATABASE_SETTINGS', N'Database Settings'),
|
|
(N'FUNC_DEBUG_CYCLE_RUN', N'Cycle Run'),
|
|
(N'FUNC_DEBUG_EAP_REPORT', N'EAP Report'),
|
|
(N'FUNC_DEBUG_ENGINEERING_MODE', N'Engineering Mode'),
|
|
(N'FUNC_DEBUG_PLC_REPORT', N'PLC Report'),
|
|
(N'FUNC_EQ_CONSTANT', N'Equipment Constant'),
|
|
(N'FUNC_EQ_MESSAGE_QUERY', N'EQ Message'),
|
|
(N'FUNC_EQ_RECIPE_LIST', N'EQ Recipe'),
|
|
(N'FUNC_HOST_MESSAGE_QUERY', N'Host Message'),
|
|
(N'FUNC_LOGOUT', N'User LogOut'),
|
|
(N'FUNC_OPERATION_PERFORMANCE', N'Operation Performance'),
|
|
(N'FUNC_PASSWORD_CHANGE', N'Password Change'),
|
|
(N'FUNC_PORT_MAINTENANCE', N'Port Maintenance'),
|
|
(N'FUNC_RUN_MODE_CHANGE', N'Run Mode Change'),
|
|
(N'FUNC_TERMINAL_REQUEST', N'Terminal Request'),
|
|
(N'FUNC_TIP_MESSAGE_QUERY', N'Tip Message'),
|
|
(N'FUNC_USER_MANAGEMENT', N'User Account Management'),
|
|
(N'FUNC_USER_OPERATION_RECORD', N'User Operation Records'),
|
|
(N'FUNC_WIP_INFO_QUERY', N'WIP Information')";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("UASFNC基本資料插入完成");
|
|
#endregion
|
|
|
|
#region UASUSRGRP Table
|
|
// 插入UASUSRGRP基本資料
|
|
LogMessage("正在插入UASUSRGRP基本資料...");
|
|
currentSql = @"
|
|
INSERT INTO UASUSRGRP (USER_GRP) VALUES
|
|
(N'ADMIN'),
|
|
(N'ENG'),
|
|
(N'OPER')";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("UASUSRGRP基本資料插入完成");
|
|
#endregion
|
|
|
|
#region UASUSR Table
|
|
// 插入UASUSR基本資料 (預設管理員帳號)
|
|
LogMessage("正在插入UASUSR基本資料...");
|
|
currentSql = @"
|
|
INSERT INTO UASUSR (USER_ID, PASSWD, BADGE_NUMBER, USER_NAME, DISABLE_FLG, POWER_USER_FLG, ADMIN_FLG, USER_GRP, DEPARTMENT) VALUES
|
|
(N'ADMIN ', N'ml22099478', NULL, N'Admin ', N'N', N'N', N'N', N'ADMIN ', N'FA '),
|
|
(N'ENG ', N'umtckf', NULL, N'Eng ', N'N', N'N', N'N', N'ENG ', N' '),
|
|
(N'OP ', N'123', NULL, N' ', N'N', NULL, N'N', N'OPER ', N' ')";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("UASUSR基本資料插入完成");
|
|
#endregion
|
|
|
|
#region UASUFNC Table
|
|
// 插入UASUFNC基本資料 (管理員權限)
|
|
LogMessage("正在插入UASUFNC基本資料...");
|
|
currentSql = @"
|
|
INSERT INTO UASUFNC (USER_GRP, FUNC_CODE) VALUES
|
|
(N'ADMIN', N'FUNC_ALARM_INFO_QUERY'),
|
|
(N'ADMIN', N'FUNC_ALARM_MAINTENANCE'),
|
|
(N'ADMIN', N'FUNC_CJPJ_MAINTENANCE'),
|
|
(N'ADMIN', N'FUNC_CJPJINFORMATION'),
|
|
(N'ADMIN', N'FUNC_CLOSE_BC'),
|
|
(N'ADMIN', N'FUNC_CONNECTION_CONTROL'),
|
|
(N'ADMIN', N'FUNC_CST_DATA_OPERATION'),
|
|
(N'ADMIN', N'FUNC_DATABASE_SETTINGS'),
|
|
(N'ADMIN', N'FUNC_DEBUG_CYCLE_RUN'),
|
|
(N'ADMIN', N'FUNC_DEBUG_EAP_REPORT'),
|
|
(N'ADMIN', N'FUNC_DEBUG_ENGINEERING_MODE'),
|
|
(N'ADMIN', N'FUNC_DEBUG_PLC_REPORT'),
|
|
(N'ADMIN', N'FUNC_EQ_CONSTANT'),
|
|
(N'ADMIN', N'FUNC_EQ_MESSAGE_QUERY'),
|
|
(N'ADMIN', N'FUNC_EQ_RECIPE_LIST'),
|
|
(N'ADMIN', N'FUNC_HOST_MESSAGE_QUERY'),
|
|
(N'ADMIN', N'FUNC_LOGOUT'),
|
|
(N'ADMIN', N'FUNC_OPERATION_PERFORMANCE'),
|
|
(N'ADMIN', N'FUNC_PASSWORD_CHANGE'),
|
|
(N'ADMIN', N'FUNC_PORT_MAINTENANCE'),
|
|
(N'ADMIN', N'FUNC_PPID_MAINTENANCE'),
|
|
(N'ADMIN', N'FUNC_RUN_MODE_CHANGE'),
|
|
(N'ADMIN', N'FUNC_TERMINAL_REQUEST'),
|
|
(N'ADMIN', N'FUNC_TIP_MESSAGE_QUERY'),
|
|
(N'ADMIN', N'FUNC_USER_MANAGEMENT'),
|
|
(N'ADMIN', N'FUNC_USER_OPERATION_RECORD'),
|
|
(N'ADMIN', N'FUNC_WIP_INFO_QUERY')";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
|
|
// 插入ENG權限
|
|
currentSql = @"
|
|
INSERT INTO UASUFNC (USER_GRP, FUNC_CODE) VALUES
|
|
(N'ENG', N'FUNC_ALARM_INFO_QUERY'),
|
|
(N'ENG', N'FUNC_ALARM_MAINTENANCE'),
|
|
(N'ENG', N'FUNC_CJPJ_MAINTENANCE'),
|
|
(N'ENG', N'FUNC_CJPJINFORMATION'),
|
|
(N'ENG', N'FUNC_CLOSE_BC'),
|
|
(N'ENG', N'FUNC_CONNECTION_CONTROL'),
|
|
(N'ENG', N'FUNC_CST_DATA_OPERATION'),
|
|
(N'ENG', N'FUNC_DATABASE_SETTINGS'),
|
|
(N'ENG', N'FUNC_DEBUG_CYCLE_RUN'),
|
|
(N'ENG', N'FUNC_DEBUG_EAP_REPORT'),
|
|
(N'ENG', N'FUNC_DEBUG_ENGINEERING_MODE'),
|
|
(N'ENG', N'FUNC_DEBUG_PLC_REPORT'),
|
|
(N'ENG', N'FUNC_EQ_CONSTANT'),
|
|
(N'ENG', N'FUNC_EQ_MESSAGE_QUERY'),
|
|
(N'ENG', N'FUNC_EQ_RECIPE_LIST'),
|
|
(N'ENG', N'FUNC_HOST_MESSAGE_QUERY'),
|
|
(N'ENG', N'FUNC_LOGOUT'),
|
|
(N'ENG', N'FUNC_OPERATION_PERFORMANCE'),
|
|
(N'ENG', N'FUNC_PASSWORD_CHANGE'),
|
|
(N'ENG', N'FUNC_PORT_MAINTENANCE'),
|
|
(N'ENG', N'FUNC_PPID_MAINTENANCE'),
|
|
(N'ENG', N'FUNC_RUN_MODE_CHANGE'),
|
|
(N'ENG', N'FUNC_TERMINAL_REQUEST'),
|
|
(N'ENG', N'FUNC_TIP_MESSAGE_QUERY'),
|
|
(N'ENG', N'FUNC_USER_OPERATION_RECORD'),
|
|
(N'ENG', N'FUNC_WIP_INFO_QUERY')";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
|
|
// 插入OPER權限
|
|
currentSql = @"
|
|
INSERT INTO UASUFNC (USER_GRP, FUNC_CODE) VALUES
|
|
(N'OPER', N'FUNC_ALARM_INFO_QUERY'),
|
|
(N'OPER', N'FUNC_CJPJ_MAINTENANCE'),
|
|
(N'OPER', N'FUNC_CJPJINFORMATION'),
|
|
(N'OPER', N'FUNC_CONNECTION_CONTROL'),
|
|
(N'OPER', N'FUNC_CST_DATA_OPERATION'),
|
|
(N'OPER', N'FUNC_EQ_MESSAGE_QUERY'),
|
|
(N'OPER', N'FUNC_EQ_RECIPE_LIST'),
|
|
(N'OPER', N'FUNC_HOST_MESSAGE_QUERY'),
|
|
(N'OPER', N'FUNC_LOGOUT'),
|
|
(N'OPER', N'FUNC_OPERATION_PERFORMANCE'),
|
|
(N'OPER', N'FUNC_TIP_MESSAGE_QUERY'),
|
|
(N'OPER', N'FUNC_USER_OPERATION_RECORD'),
|
|
(N'OPER', N'FUNC_WIP_INFO_QUERY')";
|
|
using (var command = new SqlCommand(currentSql, conn, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
LogMessage("UASUFNC基本資料插入完成");
|
|
#endregion
|
|
|
|
LogMessage("所有基本資料插入完成");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"插入基本資料時發生錯誤: {ex.Message}", true);
|
|
LogMessage($"錯誤詳細資訊: {ex.StackTrace}", true);
|
|
LogMessage($"發生錯誤的SQL語句: {currentSql}", true);
|
|
throw; // 重新拋出例外,讓上層處理交易回滾
|
|
}
|
|
}
|
|
|
|
private async void btn_UpdateDatabase_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!CheckUnsavedChanges()) return;
|
|
SetOperationButtonsState(false, "更新資料庫");
|
|
|
|
string backupDbName = null;
|
|
bool overallUpdateSuccess = false; // Renamed for clarity
|
|
var cts = new CancellationTokenSource(); // For cancellation
|
|
|
|
try
|
|
{
|
|
PauseTraceDataReportTimer();
|
|
var confirmResult = MessageBox.Show("確定要更新資料庫嗎?\n\n註:更新失敗時,資料庫會自動盡可能的還原至更新前的狀態。\n\nAre you sure you want to update the database?\n\nNote: If the update fails, the database will attempt to restore to the state before the update as much as possible.",
|
|
"確認操作 / Confirm Operation", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
|
if (confirmResult != MessageBoxResult.Yes)
|
|
{
|
|
LogMessage("使用者取消操作");
|
|
return;
|
|
}
|
|
|
|
LogMessage("資料更新作業開始");
|
|
if (!ValidateDatabaseSettings()) return;
|
|
UpdateConnectionStrings();
|
|
if (!CheckAccountTables())
|
|
{
|
|
LogMessage("使用者因資料表風險取消操作");
|
|
return;
|
|
}
|
|
|
|
// Check if BC_Dev exists
|
|
using (var connection = new SqlConnection(_masterConnectionString))
|
|
{
|
|
await connection.OpenAsync(cts.Token);
|
|
using (var command = new SqlCommand("SELECT COUNT(*) FROM sys.databases WHERE name = 'BC_Dev'", connection))
|
|
{
|
|
if ((int)await command.ExecuteScalarAsync(cts.Token) == 0)
|
|
{
|
|
LogMessage("找不到 BC_Dev 資料庫,無法執行更新");
|
|
MessageBox.Show("找不到 BC_Dev 資料庫,無法執行更新。\n\nDatabase 'BC_Dev' not found. Update cannot be performed.", "提示 / Information", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Backup Database
|
|
var backupTaskResult = await Task.Run(async () =>
|
|
{
|
|
System.Threading.Thread.CurrentThread.Name = "UpdateDatabase_BackupTask";
|
|
_currentOperation = "更新資料庫 - 備份";
|
|
return await BackupDatabase(); // BackupDatabase itself should handle cancellation token if made async and passed cts.Token
|
|
}, cts.Token);
|
|
|
|
|
|
if (backupTaskResult == null || backupTaskResult.DatabaseName == null)
|
|
{
|
|
LogMessage($"資料庫備份失敗: {backupTaskResult?.ErrorMessage ?? "未知錯誤"}", true);
|
|
MessageBox.Show("資料庫備份失敗,無法繼續執行更新操作。\n請參閱操作記錄了解詳細資訊。", "提示 / Information", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
backupDbName = backupTaskResult.DatabaseName;
|
|
LogMessage($"資料庫成功備份為: {backupDbName}");
|
|
|
|
|
|
// Delete original BC_Dev (MDF and LDF files as well)
|
|
try
|
|
{
|
|
LogMessage("開始刪除原始BC_Dev資料庫...");
|
|
// ... (code to delete database and files, ensure it uses cts.Token if async)
|
|
using (var connection = new SqlConnection(_masterConnectionString))
|
|
{
|
|
await connection.OpenAsync(cts.Token);
|
|
string mdfPath = string.Empty, ldfPath = string.Empty;
|
|
bool bcDevExists = false;
|
|
using (var cmd = new SqlCommand("SELECT COUNT(*) FROM sys.databases WHERE name = 'BC_Dev'", connection))
|
|
bcDevExists = (int)await cmd.ExecuteScalarAsync(cts.Token) > 0;
|
|
|
|
if (bcDevExists)
|
|
{
|
|
using (var cmd = new SqlCommand("SELECT physical_name, type_desc FROM sys.master_files WHERE database_id = DB_ID('BC_Dev')", connection))
|
|
using (var reader = await cmd.ExecuteReaderAsync(cts.Token))
|
|
{
|
|
while (await reader.ReadAsync(cts.Token))
|
|
{
|
|
if (reader["type_desc"].ToString() == "ROWS") mdfPath = reader["physical_name"].ToString();
|
|
else if (reader["type_desc"].ToString() == "LOG") ldfPath = reader["physical_name"].ToString();
|
|
}
|
|
}
|
|
using (var cmd = new SqlCommand("ALTER DATABASE [BC_Dev] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [BC_Dev];", connection))
|
|
await cmd.ExecuteNonQueryAsync(cts.Token);
|
|
LogMessage("BC_Dev資料庫已從SQL Server移除");
|
|
|
|
await Task.Delay(1000, cts.Token); // Wait for file release
|
|
if (!string.IsNullOrEmpty(mdfPath) && IOFile.Exists(mdfPath)) try { IOFile.Delete(mdfPath); LogMessage($"已刪除MDF檔案: {mdfPath}"); } catch (Exception ex) { LogMessage($"警告:無法刪除MDF檔案: {ex.Message}", true); }
|
|
if (!string.IsNullOrEmpty(ldfPath) && IOFile.Exists(ldfPath)) try { IOFile.Delete(ldfPath); LogMessage($"已刪除LDF檔案: {ldfPath}"); } catch (Exception ex) { LogMessage($"警告:無法刪除LDF檔案: {ex.Message}", true); }
|
|
}
|
|
else LogMessage("找不到BC_Dev資料庫,無需刪除");
|
|
}
|
|
}
|
|
catch (OperationCanceledException) { LogMessage("刪除原始資料庫操作已取消。"); throw; } // Rethrow to main catch
|
|
catch (Exception ex) { LogMessage($"刪除原始資料庫時發生錯誤: {ex.Message}", true); /* continue, Create will try again */ }
|
|
|
|
|
|
var progressIndicator = new Progress<string>(message => { DoEvents(); });
|
|
|
|
// Create new DB (without initial data)
|
|
await Task.Run(async () => await CreateDatabaseWithProgress(false, progressIndicator, cts.Token), cts.Token);
|
|
|
|
|
|
// Migrate Data
|
|
bool migrationSuccess = false;
|
|
// _migrationFailedDueToError will be set by MigrateData if it's an internal error
|
|
|
|
migrationSuccess = await Task.Run(() => MigrateData(progressIndicator, cts.Token), cts.Token);
|
|
|
|
|
|
if (!migrationSuccess) // This means MigrateData returned false
|
|
{
|
|
if (_migrationFailedDueToError) // MigrateData itself had an error
|
|
{
|
|
LogMessage("資料庫更新因資料遷移錯誤而失敗,將還原資料庫...", true);
|
|
MessageBox.Show("資料庫更新因資料遷移錯誤而失敗,已還原至原始狀態。\n請檢查日誌以獲取詳細資訊。", "遷移錯誤 / Migration Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
else // User chose to abort during MigrateData (e.g., from a dialog) or some other non-OperationCanceled failure
|
|
{
|
|
LogMessage("資料遷移未成功 (可能由使用者在提示中選擇中止),將還原資料庫...", true);
|
|
MessageBox.Show("資料庫更新期間,資料遷移未完成,操作已中止並還原至原始狀態。", "操作中止 / Operation Aborted", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
await RestoreDatabase(backupDbName); // Restore is critical here
|
|
return; // Stop further execution
|
|
}
|
|
|
|
// If migration was successful
|
|
LogMessage("資料庫更新作業成功完成");
|
|
overallUpdateSuccess = true; // Mark overall success
|
|
backupDbName = null; // Prevent restore in finally if we got here
|
|
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
try
|
|
{
|
|
LogMessage("更新資料庫成功,開始檢查欄位定義一致性...");
|
|
bool hasInconsistentFields = CheckFieldConsistency(false);
|
|
if (hasInconsistentFields)
|
|
{
|
|
MessageBox.Show("資料庫更新成功,但發現欄位定義不一致的情況,詳情請查看日誌報告。", "更新完成但有警告", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show("資料庫更新成功!\n需要重新啟動程式才能生效。", "系統提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"更新後檢查欄位一致性時發生錯誤: {ex.Message}", true);
|
|
MessageBox.Show("資料庫更新成功!\n需要重新啟動程式才能生效。", "系統提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
MessageBox.Show("資料庫更新完成,程式即將關閉。\n請手動重新啟動程式。", "系統提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
ShutdownApplication();
|
|
}), DispatcherPriority.Normal);
|
|
}
|
|
catch (OperationCanceledException opEx) // Catches cancellations from any await Task.Run or from MigrateData
|
|
{
|
|
LogMessage($"資料庫更新操作被取消: {opEx.Message}", true);
|
|
if (!string.IsNullOrEmpty(backupDbName))
|
|
{
|
|
try
|
|
{
|
|
LogMessage("因操作取消,嘗試還原資料庫...", true);
|
|
await RestoreDatabase(backupDbName); // Restore if backup was made
|
|
MessageBox.Show("資料庫更新操作已取消,並已還原至原始狀態。", "操作已取消", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
catch (Exception restoreEx)
|
|
{
|
|
LogMessage($"因操作取消而還原資料庫時發生錯誤: {restoreEx.Message}", true);
|
|
MessageBox.Show($"資料庫更新操作已取消,但在還原過程中發生錯誤。\n請聯繫系統管理員。\n\n{restoreEx.Message}", "還原錯誤", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show("資料庫更新操作已取消。", "操作已取消", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
}
|
|
catch (Exception ex) // General errors during the update process
|
|
{
|
|
LogMessage($"更新資料庫時發生未預期錯誤: {ex.Message}{Environment.NewLine}StackTrace: {ex.StackTrace}", true);
|
|
if (!string.IsNullOrEmpty(backupDbName) && !overallUpdateSuccess) // only restore if update wasn't flagged as successful
|
|
{
|
|
try
|
|
{
|
|
LogMessage($"因更新錯誤,嘗試還原資料庫 {backupDbName}...", true);
|
|
await RestoreDatabase(backupDbName);
|
|
MessageBox.Show("資料庫更新失敗,已自動還原至更新前的狀態。\n請參閱操作記錄了解詳細錯誤資訊。", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
catch (Exception restoreEx)
|
|
{
|
|
LogMessage($"還原資料庫 {backupDbName} 時發生錯誤: {restoreEx.Message}", true);
|
|
MessageBox.Show($"資料庫更新失敗,且還原過程中也發生錯誤。\n請聯繫系統管理員進行手動還原。\n\n更新錯誤: {ex.Message}\n還原錯誤: {restoreEx.Message}", "嚴重錯誤 / Severe Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
else if (overallUpdateSuccess)
|
|
{
|
|
LogMessage($"更新資料庫作業標記為成功,但後續發生錯誤: {ex.Message}。資料庫可能已更新。", true);
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show($"資料庫更新失敗,且無有效備份可供還原。\n錯誤訊息:{ex.Message}\n請參閱操作記錄。", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
SetOperationButtonsState(true);
|
|
ResumeTraceDataReportTimer();
|
|
_migrationFailedDueToError = false; // Reset flag for next operation
|
|
}
|
|
}
|
|
|
|
// 定義一個結構來返回備份結果和可能的失敗原因
|
|
private class BackupResult
|
|
{
|
|
public string DatabaseName { get; set; } // 成功時為備份資料庫名稱,失敗時為null
|
|
public string ErrorMessage { get; set; } // 失敗原因,成功時為null
|
|
|
|
public static BackupResult Success(string dbName)
|
|
{
|
|
return new BackupResult { DatabaseName = dbName };
|
|
}
|
|
|
|
public static BackupResult Failure(string message)
|
|
{
|
|
return new BackupResult { ErrorMessage = message };
|
|
}
|
|
}
|
|
|
|
private async Task<BackupResult> BackupDatabase()
|
|
{
|
|
string backupDbName = null;
|
|
bool singleUserModeEnabled = false;
|
|
string backupFilePath = null;
|
|
|
|
try
|
|
{
|
|
LogMessage("開始備份資料庫...");
|
|
|
|
// 生成備份資料庫名稱和備份檔案路徑
|
|
backupDbName = "BC_Dev_" + DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
|
|
|
using (var connection = new SqlConnection(_masterConnectionString))
|
|
{
|
|
await connection.OpenAsync();
|
|
|
|
// 檢查資料庫是否存在
|
|
using (var command = new SqlCommand(@"
|
|
SELECT database_id
|
|
FROM sys.databases
|
|
WHERE name = 'BC_Dev'", connection))
|
|
{
|
|
var result = await command.ExecuteScalarAsync();
|
|
if (result == null)
|
|
{
|
|
// 返回具體的失敗原因,不拋出例外
|
|
return BackupResult.Failure("找不到 BC_Dev 資料庫,無法執行備份");
|
|
}
|
|
}
|
|
|
|
// 取得資料檔案所在目錄,用於存放備份檔案
|
|
string backupDir = string.Empty;
|
|
using (var command = new SqlCommand(@"
|
|
SELECT
|
|
LEFT(physical_name, LEN(physical_name) - CHARINDEX('\', REVERSE(physical_name)) + 1)
|
|
FROM sys.master_files
|
|
WHERE database_id = DB_ID('BC_Dev')
|
|
AND type_desc = 'ROWS'", connection))
|
|
{
|
|
backupDir = (string)await command.ExecuteScalarAsync();
|
|
}
|
|
|
|
// 確保備份目錄存在
|
|
if (!Directory.Exists(backupDir))
|
|
{
|
|
LogMessage($"備份目錄 {backupDir} 不存在,嘗試使用系統臨時目錄");
|
|
backupDir = IOPath.GetTempPath();
|
|
}
|
|
|
|
// 生成備份檔案名稱
|
|
backupFilePath = IOPath.Combine(backupDir, $"{backupDbName}.bak");
|
|
LogMessage($"備份檔案路徑: {backupFilePath}");
|
|
|
|
// 執行備份
|
|
try
|
|
{
|
|
LogMessage("執行資料庫備份...");
|
|
string backupCommand = $@"
|
|
BACKUP DATABASE [BC_Dev]
|
|
TO DISK = '{backupFilePath}'
|
|
WITH FORMAT, INIT, SKIP, NOREWIND, NOUNLOAD, STATS = 10";
|
|
|
|
using (var command = new SqlCommand(backupCommand, connection))
|
|
{
|
|
command.CommandTimeout = 300; // 5分鐘超時
|
|
await command.ExecuteNonQueryAsync();
|
|
}
|
|
LogMessage("資料庫備份檔案建立成功");
|
|
|
|
// 還原為新名稱
|
|
LogMessage($"開始還原為新資料庫: {backupDbName}");
|
|
|
|
// 取得資料庫檔案路徑
|
|
string dataDir = string.Empty;
|
|
string logDir = string.Empty;
|
|
using (var command = new SqlCommand(@"
|
|
SELECT
|
|
SERVERPROPERTY('InstanceDefaultDataPath') AS DefaultDataPath,
|
|
SERVERPROPERTY('InstanceDefaultLogPath') AS DefaultLogPath", connection))
|
|
{
|
|
using (var reader = await command.ExecuteReaderAsync())
|
|
{
|
|
if (await reader.ReadAsync())
|
|
{
|
|
dataDir = reader["DefaultDataPath"] as string ?? string.Empty;
|
|
logDir = reader["DefaultLogPath"] as string ?? string.Empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果無法獲取SQL Server默認目錄,嘗試從現有數據文件路徑獲取
|
|
if (string.IsNullOrEmpty(dataDir) || string.IsNullOrEmpty(logDir))
|
|
{
|
|
using (var command = new SqlCommand(@"
|
|
SELECT
|
|
LEFT(physical_name, LEN(physical_name) - CHARINDEX('\', REVERSE(physical_name)) + 1) AS DirectoryPath,
|
|
type_desc
|
|
FROM sys.master_files
|
|
WHERE database_id = DB_ID('BC_Dev')", connection))
|
|
{
|
|
using (var reader = await command.ExecuteReaderAsync())
|
|
{
|
|
while (await reader.ReadAsync())
|
|
{
|
|
string dirPath = reader["DirectoryPath"].ToString();
|
|
string typeDesc = reader["type_desc"].ToString();
|
|
|
|
if (typeDesc == "ROWS" && string.IsNullOrEmpty(dataDir))
|
|
dataDir = dirPath;
|
|
else if (typeDesc == "LOG" && string.IsNullOrEmpty(logDir))
|
|
logDir = dirPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 仍然無法獲取,使用備份目錄
|
|
if (string.IsNullOrEmpty(dataDir)) dataDir = backupDir;
|
|
if (string.IsNullOrEmpty(logDir)) logDir = dataDir;
|
|
|
|
string newDataFile = IOPath.Combine(dataDir, $"{backupDbName}.mdf");
|
|
string newLogFile = IOPath.Combine(logDir, $"{backupDbName}_log.ldf");
|
|
|
|
LogMessage($"新資料檔案路徑: {newDataFile}");
|
|
LogMessage($"新日誌檔案路徑: {newLogFile}");
|
|
|
|
// 檢查並刪除可能存在的檔案
|
|
if (IOFile.Exists(newDataFile))
|
|
{
|
|
LogMessage($"發現存在的資料檔案,嘗試刪除: {newDataFile}");
|
|
try { IOFile.Delete(newDataFile); }
|
|
catch (Exception ex) { LogMessage($"刪除舊資料檔案失敗: {ex.Message}", true); }
|
|
}
|
|
if (IOFile.Exists(newLogFile))
|
|
{
|
|
LogMessage($"發現存在的日誌檔案,嘗試刪除: {newLogFile}");
|
|
try { IOFile.Delete(newLogFile); }
|
|
catch (Exception ex) { LogMessage($"刪除舊日誌檔案失敗: {ex.Message}", true); }
|
|
}
|
|
|
|
// 執行還原
|
|
string restoreCommand = $@"
|
|
RESTORE DATABASE [{backupDbName}]
|
|
FROM DISK = '{backupFilePath}'
|
|
WITH
|
|
MOVE 'BC_Dev' TO '{newDataFile}',
|
|
MOVE 'BC_Dev_log' TO '{newLogFile}',
|
|
RECOVERY, REPLACE, STATS = 10";
|
|
|
|
using (var command = new SqlCommand(restoreCommand, connection))
|
|
{
|
|
command.CommandTimeout = 300; // 5分鐘超時
|
|
await command.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
LogMessage("資料庫還原為新名稱完成");
|
|
|
|
// 確認資料庫狀態
|
|
string checkCommand = $@"
|
|
SELECT state_desc FROM sys.databases
|
|
WHERE name = '{backupDbName}'";
|
|
|
|
using (var command = new SqlCommand(checkCommand, connection))
|
|
{
|
|
string state = (string)await command.ExecuteScalarAsync();
|
|
LogMessage($"新資料庫狀態: {state}");
|
|
|
|
if (state != "ONLINE")
|
|
{
|
|
using (var setOnlineCmd = new SqlCommand(
|
|
$"ALTER DATABASE [{backupDbName}] SET ONLINE", connection))
|
|
{
|
|
await setOnlineCmd.ExecuteNonQueryAsync();
|
|
LogMessage("已將新資料庫設置為線上狀態");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 清理備份檔案
|
|
try
|
|
{
|
|
IOFile.Delete(backupFilePath);
|
|
LogMessage("已刪除臨時備份檔案");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"無法刪除臨時備份檔案: {ex.Message}", true);
|
|
LogMessage($"請手動刪除檔案: {backupFilePath}", true);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"執行備份還原操作失敗: {ex.Message}", true);
|
|
|
|
// 清理資源
|
|
try
|
|
{
|
|
if (IOFile.Exists(backupFilePath))
|
|
{
|
|
IOFile.Delete(backupFilePath);
|
|
LogMessage("已刪除臨時備份檔案");
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
throw;
|
|
}
|
|
|
|
LogMessage("資料庫備份完成");
|
|
return BackupResult.Success(backupDbName);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"備份資料庫失敗: {ex.Message}", true);
|
|
|
|
// 清理任何剩餘的資源
|
|
if (backupFilePath != null && IOFile.Exists(backupFilePath))
|
|
{
|
|
try
|
|
{
|
|
IOFile.Delete(backupFilePath);
|
|
LogMessage("已清理備份檔案");
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async Task RestoreDatabase(string backupDbName)
|
|
{
|
|
string backupFilePath = null;
|
|
|
|
try
|
|
{
|
|
LogMessage($"開始還原資料庫 {backupDbName} 到 BC_Dev...");
|
|
|
|
// 在 UI 執行緒上更新連線字串
|
|
//await Dispatcher.InvokeAsync(() => UpdateConnectionStrings());
|
|
UpdateConnectionStrings();
|
|
using (var connection = new SqlConnection(_masterConnectionString))
|
|
{
|
|
await connection.OpenAsync();
|
|
|
|
// 1. 檢查來源資料庫是否存在
|
|
bool sourceExists = false;
|
|
using (var command = new SqlCommand($"SELECT 1 FROM sys.databases WHERE name = '{backupDbName}'", connection))
|
|
{
|
|
var result = await command.ExecuteScalarAsync();
|
|
sourceExists = result != null;
|
|
}
|
|
|
|
if (!sourceExists)
|
|
{
|
|
throw new Exception($"找不到來源資料庫 {backupDbName}");
|
|
}
|
|
|
|
// 2. 檢查並刪除目標資料庫
|
|
bool targetExists = false;
|
|
using (var command = new SqlCommand("SELECT 1 FROM sys.databases WHERE name = 'BC_Dev'", connection))
|
|
{
|
|
var result = await command.ExecuteScalarAsync();
|
|
targetExists = result != null;
|
|
}
|
|
|
|
if (targetExists)
|
|
{
|
|
LogMessage("找到現有的 BC_Dev 資料庫,準備刪除...");
|
|
try
|
|
{
|
|
// 先確保所有連線都斷開
|
|
using (var command = new SqlCommand(@"
|
|
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'BC_Dev')
|
|
BEGIN
|
|
ALTER DATABASE [BC_Dev] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
|
DROP DATABASE [BC_Dev];
|
|
END", connection))
|
|
{
|
|
command.CommandTimeout = 180; // 設定3分鐘超時
|
|
await command.ExecuteNonQueryAsync();
|
|
LogMessage("已刪除現有的 BC_Dev 資料庫");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new Exception($"無法刪除現有的 BC_Dev 資料庫: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// 3. 獲取資料庫檔案路徑
|
|
string dataDir = null;
|
|
|
|
// 嘗試獲取 SQL Server 資料目錄
|
|
using (var command = new SqlCommand("SELECT SERVERPROPERTY('InstanceDefaultDataPath')", connection))
|
|
{
|
|
var result = await command.ExecuteScalarAsync();
|
|
dataDir = result?.ToString();
|
|
}
|
|
|
|
// 如果無法獲取預設路徑,使用備份資料庫的檔案路徑
|
|
if (string.IsNullOrEmpty(dataDir))
|
|
{
|
|
using (var command = new SqlCommand($@"
|
|
SELECT TOP 1
|
|
LEFT(physical_name, LEN(physical_name) - CHARINDEX('\', REVERSE(physical_name)) + 1)
|
|
FROM sys.master_files
|
|
WHERE database_id = DB_ID('{backupDbName}')
|
|
AND type_desc = 'ROWS'", connection))
|
|
{
|
|
var result = await command.ExecuteScalarAsync();
|
|
dataDir = result?.ToString();
|
|
}
|
|
}
|
|
|
|
// 如果仍然無法獲取,使用臨時目錄
|
|
if (string.IsNullOrEmpty(dataDir))
|
|
{
|
|
dataDir = IOPath.GetTempPath();
|
|
LogMessage($"無法獲取資料庫目錄,使用臨時目錄: {dataDir}");
|
|
}
|
|
|
|
// 4. 創建臨時備份檔案路徑
|
|
backupFilePath = IOPath.Combine(dataDir, $"BC_Dev_Restore_{DateTime.Now.ToString("yyyyMMddHHmmss")}.bak");
|
|
LogMessage($"備份檔案路徑: {backupFilePath}");
|
|
|
|
// 5. 備份來源資料庫
|
|
LogMessage($"備份資料庫 {backupDbName} 到檔案...");
|
|
using (var command = new SqlCommand($@"
|
|
BACKUP DATABASE [{backupDbName}]
|
|
TO DISK = '{backupFilePath}'
|
|
WITH FORMAT, INIT, STATS = 10", connection))
|
|
{
|
|
command.CommandTimeout = 300; // 5分鐘超時
|
|
await command.ExecuteNonQueryAsync();
|
|
}
|
|
LogMessage("資料庫備份完成");
|
|
|
|
// 6. 從備份檔還原為 BC_Dev
|
|
LogMessage("從備份檔還原為 BC_Dev...");
|
|
|
|
// 獲取備份文件中的邏輯名稱
|
|
string dataLogicalName = null;
|
|
string logLogicalName = null;
|
|
string newMdfPath = IOPath.Combine(dataDir, "BC_Dev.mdf");
|
|
string newLdfPath = IOPath.Combine(dataDir, "BC_Dev_log.ldf");
|
|
|
|
using (var command = new SqlCommand($"RESTORE FILELISTONLY FROM DISK = '{backupFilePath}'", connection))
|
|
{
|
|
using (var reader = await command.ExecuteReaderAsync())
|
|
{
|
|
while (await reader.ReadAsync())
|
|
{
|
|
string logicalName = reader["LogicalName"].ToString();
|
|
string type = reader["Type"].ToString();
|
|
|
|
if (type == "D" || type == "0")
|
|
{
|
|
dataLogicalName = logicalName;
|
|
}
|
|
else if (type == "L" || type == "1")
|
|
{
|
|
logLogicalName = logicalName;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(dataLogicalName) || string.IsNullOrEmpty(logLogicalName))
|
|
{
|
|
throw new Exception("無法從備份檔中獲取邏輯檔案名稱");
|
|
}
|
|
|
|
// 檢查並刪除可能存在的檔案
|
|
if (IOFile.Exists(newMdfPath))
|
|
{
|
|
try { IOFile.Delete(newMdfPath); } catch { /* 忽略錯誤 */ }
|
|
}
|
|
|
|
if (IOFile.Exists(newLdfPath))
|
|
{
|
|
try { IOFile.Delete(newLdfPath); } catch { /* 忽略錯誤 */ }
|
|
}
|
|
|
|
// 還原資料庫,並確保在還原過程中的獨佔存取
|
|
string restoreCmd = $@"
|
|
-- 如果資料庫存在,先設定為單一使用者模式
|
|
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'BC_Dev')
|
|
BEGIN
|
|
ALTER DATABASE [BC_Dev] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
|
END;
|
|
|
|
-- 執行還原
|
|
RESTORE DATABASE [BC_Dev]
|
|
FROM DISK = '{backupFilePath}'
|
|
WITH MOVE '{dataLogicalName}' TO '{newMdfPath}',
|
|
MOVE '{logLogicalName}' TO '{newLdfPath}',
|
|
STATS = 10, REPLACE;
|
|
|
|
-- 還原完成後設定為多使用者模式
|
|
ALTER DATABASE [BC_Dev] SET MULTI_USER;";
|
|
|
|
using (var command = new SqlCommand(restoreCmd, connection))
|
|
{
|
|
command.CommandTimeout = 1800; // 30分鐘超時,確保有足夠時間完成還原
|
|
await command.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
LogMessage("資料庫還原成功");
|
|
|
|
// 清理臨時檔案
|
|
try
|
|
{
|
|
if (IOFile.Exists(backupFilePath))
|
|
{
|
|
IOFile.Delete(backupFilePath);
|
|
LogMessage("已刪除臨時備份檔案");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"警告:無法刪除臨時備份檔案: {ex.Message}", true);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"還原資料庫時發生錯誤: {ex.Message}", true);
|
|
LogMessage($"還原資料庫時發生錯誤,詳細資訊: {ex.StackTrace}", true);
|
|
|
|
// 清理臨時檔案
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(backupFilePath) && IOFile.Exists(backupFilePath))
|
|
{
|
|
IOFile.Delete(backupFilePath);
|
|
LogMessage("已清理臨時備份檔案");
|
|
}
|
|
}
|
|
catch (Exception cleanupEx)
|
|
{
|
|
LogMessage($"清理臨時檔案時發生錯誤: {cleanupEx.Message}", true);
|
|
}
|
|
|
|
// 重新拋出例外,確保呼叫端知道操作失敗
|
|
throw;
|
|
}
|
|
|
|
}
|
|
private async Task<bool> MigrateData(IProgress<string> progress = null, CancellationToken cancellationToken = default)
|
|
{
|
|
_migrationFailedDueToError = false; // Reset the flag at the beginning
|
|
List<string> tablesForMigrationLoop = new List<string>(); // Define tables list here for broader scope
|
|
|
|
try
|
|
{
|
|
LogMessage("開始資料遷移...");
|
|
progress?.Report("Starting data migration...");
|
|
|
|
if (!await WaitForDatabaseConnection(_connectionString, "BC_Dev", 30, 1000, 10000))
|
|
{
|
|
LogMessage("無法連接到資料庫,取消資料遷移", true);
|
|
progress?.Report("Cannot connect to the database. Cancelling data migration.");
|
|
_migrationFailedDueToError = true;
|
|
return false;
|
|
}
|
|
|
|
using (var connection = new SqlConnection(_connectionString))
|
|
{
|
|
await connection.OpenAsync(cancellationToken);
|
|
|
|
string backupDbName = "";
|
|
using (var command = new SqlCommand(@"
|
|
SELECT name
|
|
FROM sys.databases
|
|
WHERE name LIKE 'BC_Dev_%'
|
|
AND name != 'BC_Dev'
|
|
ORDER BY create_date DESC", connection))
|
|
{
|
|
var result = await command.ExecuteScalarAsync(cancellationToken);
|
|
if (result != null && result != DBNull.Value)
|
|
{
|
|
backupDbName = result.ToString();
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(backupDbName))
|
|
{
|
|
LogMessage("找不到備份資料庫", true);
|
|
progress?.Report("Backup database not found.");
|
|
_migrationFailedDueToError = true;
|
|
return false;
|
|
}
|
|
|
|
LogMessage($"找到備份資料庫: {backupDbName}");
|
|
progress?.Report($"Found backup database: {backupDbName}");
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var columnLengthIssues = new List<(string TableName, string ColumnName, int OldLength, int NewLength, string SampleValue)>();
|
|
var incompatibleColumnTypes = new List<(string TableName, string ColumnName, string OldType, string NewType)>();
|
|
var notNullIncompatibleColumns = new List<(string TableName, string ColumnName, string ColumnType, string DefaultValue)>();
|
|
var textTypeColumns = new List<(string TableName, string ColumnName, string NewType, bool IsCompatible)>();
|
|
var tablesToSkipMigration = new HashSet<string>();
|
|
|
|
var tableRowCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
var tableColumnInfoCache = new Dictionary<string, Dictionary<string, (string DataType, int MaxLength, bool IsNullable, string ColumnDefault)>>(StringComparer.OrdinalIgnoreCase);
|
|
var backupTableColumnInfoCache = new Dictionary<string, Dictionary<string, (string DataType, int MaxLength, bool IsNullable, string ColumnDefault)>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
LogMessage("檢查資料欄位類型相容性及長度...");
|
|
progress?.Report("Checking data column type compatibility and length...");
|
|
|
|
using (var checkTransaction = connection.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
|
|
{
|
|
try
|
|
{
|
|
// Populate tablesForMigrationLoop here, within the transaction for schema queries
|
|
using (var command = new SqlCommand("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = 'dbo'", connection, checkTransaction))
|
|
{
|
|
using (var reader = await command.ExecuteReaderAsync(cancellationToken))
|
|
{
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
tablesForMigrationLoop.Add(reader.GetString(0));
|
|
}
|
|
}
|
|
}
|
|
|
|
LogMessage($"發現 {tablesForMigrationLoop.Count} 個資料表進行檢查。");
|
|
progress?.Report($"Found {tablesForMigrationLoop.Count} tables to check.");
|
|
|
|
LogMessage("預先查詢所有表格的資料筆數...");
|
|
progress?.Report("Pre-querying row counts for all tables...");
|
|
foreach (var table in tablesForMigrationLoop) // Use the scoped list
|
|
{
|
|
if (table.Equals("VERSION", StringComparison.OrdinalIgnoreCase)) continue;
|
|
try
|
|
{
|
|
using (var command = new SqlCommand($"SELECT COUNT_BIG(*) FROM [{backupDbName}].[dbo].[{table}] WITH (NOLOCK)", connection, checkTransaction))
|
|
{
|
|
var countResult = await command.ExecuteScalarAsync(cancellationToken);
|
|
tableRowCounts[table] = Convert.ToInt32(countResult);
|
|
LogMessage($"表格 {table} (來源) 資料筆數: {tableRowCounts[table]}");
|
|
}
|
|
}
|
|
catch (SqlException ex) when (ex.Number == 208)
|
|
{
|
|
LogMessage($"警告:來源備份資料庫 {backupDbName} 中找不到表格 {table}。將跳過此表格的欄位檢查和資料遷移。", true);
|
|
tableRowCounts[table] = 0;
|
|
tablesToSkipMigration.Add(table);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"查詢表格 {table} 資料筆數時出錯: {ex.Message}", true);
|
|
tableRowCounts[table] = 0;
|
|
tablesToSkipMigration.Add(table);
|
|
}
|
|
}
|
|
|
|
LogMessage("預先查詢所有有資料表格的欄位資訊 (來源與目標)...");
|
|
progress?.Report("Pre-querying column info for tables with data (source and target)...");
|
|
foreach (var table in tablesForMigrationLoop) // Use the scoped list
|
|
{
|
|
if (table.Equals("VERSION", StringComparison.OrdinalIgnoreCase) || tablesToSkipMigration.Contains(table)) continue;
|
|
if (!tableRowCounts.ContainsKey(table) || tableRowCounts[table] == 0)
|
|
{
|
|
LogMessage($"表格 {table} 在來源中無資料,跳過欄位資訊查詢。");
|
|
continue;
|
|
}
|
|
|
|
var currentTableOldColumnInfo = new Dictionary<string, (string DataType, int MaxLength, bool IsNullable, string ColumnDefault)>(StringComparer.OrdinalIgnoreCase);
|
|
try
|
|
{
|
|
using (var command = new SqlCommand($@"
|
|
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT,
|
|
CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE
|
|
FROM [{backupDbName}].INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tableName AND TABLE_SCHEMA = 'dbo'", connection, checkTransaction))
|
|
{
|
|
command.Parameters.AddWithValue("@tableName", table);
|
|
using (var reader = await command.ExecuteReaderAsync(cancellationToken))
|
|
{
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
string columnName = reader.GetString(reader.GetOrdinal("COLUMN_NAME"));
|
|
string dataType = reader.GetString(reader.GetOrdinal("DATA_TYPE"));
|
|
bool isNullable = reader.GetString(reader.GetOrdinal("IS_NULLABLE")).Equals("YES", StringComparison.OrdinalIgnoreCase);
|
|
string columnDefault = reader.IsDBNull(reader.GetOrdinal("COLUMN_DEFAULT")) ? null : reader.GetString(reader.GetOrdinal("COLUMN_DEFAULT"));
|
|
int maxLength = reader.IsDBNull(reader.GetOrdinal("CHARACTER_MAXIMUM_LENGTH")) ? 0 : reader.GetInt32(reader.GetOrdinal("CHARACTER_MAXIMUM_LENGTH"));
|
|
if ((dataType.Equals("nvarchar", StringComparison.OrdinalIgnoreCase) || dataType.Equals("varchar", StringComparison.OrdinalIgnoreCase) || dataType.Equals("varbinary", StringComparison.OrdinalIgnoreCase)) && maxLength == -1)
|
|
maxLength = int.MaxValue / 2 - 10; // Normalize MAX to a large number for comparison
|
|
currentTableOldColumnInfo[columnName] = (dataType, maxLength, isNullable, columnDefault);
|
|
}
|
|
}
|
|
}
|
|
backupTableColumnInfoCache[table] = currentTableOldColumnInfo;
|
|
}
|
|
catch (SqlException ex) when (ex.Number == 208)
|
|
{
|
|
LogMessage($"警告:查詢來源表格 [{backupDbName}].[dbo].[{table}] 的欄位資訊時發生錯誤 (可能不存在)。跳過此表的欄位獲取。", true);
|
|
backupTableColumnInfoCache[table] = new Dictionary<string, (string DataType, int MaxLength, bool IsNullable, string ColumnDefault)>(StringComparer.OrdinalIgnoreCase);
|
|
tablesToSkipMigration.Add(table);
|
|
continue;
|
|
}
|
|
|
|
var currentTableNewColumnInfo = new Dictionary<string, (string DataType, int MaxLength, bool IsNullable, string ColumnDefault)>(StringComparer.OrdinalIgnoreCase);
|
|
using (var command = new SqlCommand($@"
|
|
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT,
|
|
CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE
|
|
FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tableName AND TABLE_SCHEMA = 'dbo'", connection, checkTransaction))
|
|
{
|
|
command.Parameters.AddWithValue("@tableName", table);
|
|
using (var reader = await command.ExecuteReaderAsync(cancellationToken))
|
|
{
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
string columnName = reader.GetString(reader.GetOrdinal("COLUMN_NAME"));
|
|
string dataType = reader.GetString(reader.GetOrdinal("DATA_TYPE"));
|
|
bool isNullable = reader.GetString(reader.GetOrdinal("IS_NULLABLE")).Equals("YES", StringComparison.OrdinalIgnoreCase);
|
|
string columnDefault = reader.IsDBNull(reader.GetOrdinal("COLUMN_DEFAULT")) ? null : reader.GetString(reader.GetOrdinal("COLUMN_DEFAULT"));
|
|
int maxLength = reader.IsDBNull(reader.GetOrdinal("CHARACTER_MAXIMUM_LENGTH")) ? 0 : reader.GetInt32(reader.GetOrdinal("CHARACTER_MAXIMUM_LENGTH"));
|
|
if ((dataType.Equals("nvarchar", StringComparison.OrdinalIgnoreCase) || dataType.Equals("varchar", StringComparison.OrdinalIgnoreCase) || dataType.Equals("varbinary", StringComparison.OrdinalIgnoreCase)) && maxLength == -1)
|
|
maxLength = int.MaxValue / 2 - 10; // Normalize MAX
|
|
currentTableNewColumnInfo[columnName] = (dataType, maxLength, isNullable, columnDefault);
|
|
}
|
|
}
|
|
}
|
|
tableColumnInfoCache[table] = currentTableNewColumnInfo;
|
|
}
|
|
|
|
foreach (var table in tablesForMigrationLoop) // Use the scoped list
|
|
{
|
|
if (table.Equals("VERSION", StringComparison.OrdinalIgnoreCase) || tablesToSkipMigration.Contains(table) ||
|
|
!tableRowCounts.ContainsKey(table) || tableRowCounts[table] == 0 ||
|
|
!backupTableColumnInfoCache.ContainsKey(table) || !tableColumnInfoCache.ContainsKey(table) ||
|
|
!backupTableColumnInfoCache[table].Any())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
LogMessage($"檢查表格 {table} 的欄位定義...");
|
|
progress?.Report($"Checking column definitions for table {table}...");
|
|
|
|
var sourceColumnDetails = backupTableColumnInfoCache[table];
|
|
var targetColumnDetails = tableColumnInfoCache[table];
|
|
|
|
foreach (var targetColKvp in targetColumnDetails)
|
|
{
|
|
var targetColName = targetColKvp.Key;
|
|
var targetColInfo = targetColKvp.Value;
|
|
if (!targetColInfo.IsNullable && targetColInfo.ColumnDefault == null)
|
|
{
|
|
if (!sourceColumnDetails.ContainsKey(targetColName))
|
|
{
|
|
LogMessage($"錯誤:目標表格 {table} 中的欄位 {targetColName} 設定為 NOT NULL 且無預設值,但在來源表格中不存在。這是資料庫設計錯誤,請修正欄位定義。", true);
|
|
var result = MessageBox.Show($"發現資料庫設計錯誤:\n目標表格 [{table}] 新增了必填欄位 [{targetColName}],但未提供預設值。\n即便是新版新增的欄位,這會造成該筆資料無法寫入,\n如果是在設定參數型的資料,會造成系統無法正常執行\n除了資料是可以捨棄的狀況外,請修正欄位定義再重新更新。\n\n是否跳過此表格 [{table}] 的遷移?\n選擇「否」將中止更新並進行還原。",
|
|
"必要欄位遺失警告", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
|
if (result == MessageBoxResult.Yes) { tablesToSkipMigration.Add(table); break; }
|
|
else { LogMessage("使用者選擇中止更新。", true); _migrationFailedDueToError = true; checkTransaction.Rollback(); return false; }
|
|
}
|
|
}
|
|
}
|
|
if (tablesToSkipMigration.Contains(table)) continue;
|
|
|
|
var commonColumnNames = sourceColumnDetails.Keys.Intersect(targetColumnDetails.Keys, StringComparer.OrdinalIgnoreCase).ToList();
|
|
foreach (var columnName in commonColumnNames)
|
|
{
|
|
var oldInfo = sourceColumnDetails[columnName];
|
|
var newInfo = targetColumnDetails[columnName];
|
|
|
|
bool isOldTextType = oldInfo.DataType.Equals("text", StringComparison.OrdinalIgnoreCase) || oldInfo.DataType.Equals("ntext", StringComparison.OrdinalIgnoreCase);
|
|
if (isOldTextType)
|
|
{
|
|
bool newTypeCanStoreString = IsTypeCompatibleForText(newInfo.DataType);
|
|
textTypeColumns.Add((table, columnName, newInfo.DataType, newTypeCanStoreString));
|
|
LogMessage(newTypeCanStoreString ? $"TEXT 類型欄位相容: 表格 {table}, 欄位 {columnName}, 舊: {oldInfo.DataType}, 新: {newInfo.DataType}" : $"TEXT 類型欄位不相容: 表格 {table}, 欄位 {columnName}, 舊: {oldInfo.DataType}, 新: {newInfo.DataType}", !newTypeCanStoreString);
|
|
}
|
|
// Use the updated IsTypeCompatible with 4 arguments
|
|
else if (!IsTypeCompatible(oldInfo.DataType, newInfo.DataType, oldInfo.MaxLength, newInfo.MaxLength))
|
|
{
|
|
incompatibleColumnTypes.Add((table, columnName, oldInfo.DataType, newInfo.DataType));
|
|
LogMessage($"欄位類型不相容: 表格 {table}, 欄位 {columnName}, 舊: {oldInfo.DataType}({oldInfo.MaxLength}), 新: {newInfo.DataType}({newInfo.MaxLength})", true);
|
|
if (!newInfo.IsNullable && newInfo.ColumnDefault == null)
|
|
{
|
|
notNullIncompatibleColumns.Add((table, columnName, newInfo.DataType, newInfo.ColumnDefault));
|
|
LogMessage($"警告:欄位 {columnName} 在表格 {table} 類型不相容且設為 NOT NULL 無預設值。", true);
|
|
}
|
|
}
|
|
|
|
bool oldIsCharOrText = oldInfo.DataType.IndexOf("char", StringComparison.OrdinalIgnoreCase) >= 0 || oldInfo.DataType.IndexOf("text", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
bool newIsCharOrText = newInfo.DataType.IndexOf("char", StringComparison.OrdinalIgnoreCase) >= 0 || newInfo.DataType.IndexOf("text", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
|
|
// Check for string truncation if new length is smaller (and not MAX type)
|
|
if (oldIsCharOrText && newIsCharOrText &&
|
|
oldInfo.MaxLength < (int.MaxValue / 2 - 20) && newInfo.MaxLength < (int.MaxValue / 2 - 20) && // Exclude MAX types from direct numeric comparison
|
|
oldInfo.MaxLength > newInfo.MaxLength)
|
|
{
|
|
int affectedRows = 0, currentMaxLengthInSource = 0; string sampleValue = "";
|
|
using (var checkCmd = new SqlCommand($"SELECT COUNT_BIG(*), MAX(LEN(CAST([{columnName}] AS NVARCHAR(MAX)))) FROM [{backupDbName}].[dbo].[{table}] WITH (NOLOCK) WHERE LEN(CAST([{columnName}] AS NVARCHAR(MAX))) > {newInfo.MaxLength}", connection, checkTransaction))
|
|
using (var reader = await checkCmd.ExecuteReaderAsync(cancellationToken))
|
|
{
|
|
if (await reader.ReadAsync(cancellationToken) && reader[0] != DBNull.Value)
|
|
{
|
|
affectedRows = Convert.ToInt32(reader[0]);
|
|
if (reader[1] != DBNull.Value) currentMaxLengthInSource = Convert.ToInt32(reader[1]);
|
|
}
|
|
}
|
|
if (affectedRows > 0)
|
|
{
|
|
using (var sampleCmd = new SqlCommand($"SELECT TOP 1 CAST([{columnName}] AS NVARCHAR(MAX)) FROM [{backupDbName}].[dbo].[{table}] WITH (NOLOCK) WHERE LEN(CAST([{columnName}] AS NVARCHAR(MAX))) = {currentMaxLengthInSource}", connection, checkTransaction))
|
|
{
|
|
|
|
object sampleValueObj = await sampleCmd.ExecuteScalarAsync(cancellationToken);
|
|
string rawSampleValue = (sampleValueObj as string) ?? "";
|
|
sampleValue = rawSampleValue.Length <= 50 ? rawSampleValue : rawSampleValue.Substring(0, 50);
|
|
}
|
|
columnLengthIssues.Add((table, columnName, oldInfo.MaxLength, newInfo.MaxLength, sampleValue));
|
|
LogMessage($"潛在資料截斷: 表格 {table}, 欄位 {columnName}, 舊長度: {oldInfo.MaxLength}, 新長度: {newInfo.MaxLength}, {affectedRows} 筆資料受影響, 最大長度: {currentMaxLengthInSource}, 範例: '{sampleValue}'", true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
checkTransaction.Commit();
|
|
LogMessage("欄位定義初步檢查完成。");
|
|
progress?.Report("Initial column definition check completed.");
|
|
}
|
|
catch (OperationCanceledException) { LogMessage("欄位定義檢查操作已取消。", true); try { checkTransaction?.Rollback(); } catch (Exception rbEx) { LogMessage($"欄位檢查取消時回滾失敗: {rbEx.Message}", true); } throw; }
|
|
catch (Exception ex) { LogMessage($"檢查欄位定義時發生嚴重錯誤: {ex.Message}{Environment.NewLine}StackTrace: {ex.StackTrace}", true); try { checkTransaction?.Rollback(); } catch (Exception rbEx) { LogMessage($"欄位檢查錯誤後回滾失敗: {rbEx.Message}", true); } _migrationFailedDueToError = true; return false; }
|
|
}
|
|
|
|
if (columnLengthIssues.Any() || incompatibleColumnTypes.Any() || notNullIncompatibleColumns.Any())
|
|
{
|
|
string issueSummary = "資料遷移檢查發現以下潛在問題:\n";
|
|
if (columnLengthIssues.Any()) issueSummary += $"\n- {columnLengthIssues.Count} 個欄位可能發生資料截斷。";
|
|
if (incompatibleColumnTypes.Any()) issueSummary += $"\n- {incompatibleColumnTypes.Count} 個欄位類型不相容。";
|
|
if (notNullIncompatibleColumns.Any()) issueSummary += $"\n- {notNullIncompatibleColumns.Count} 個類型不相容的欄位被設為 NOT NULL 且無預設值。";
|
|
issueSummary += "\n\n是否繼續遷移?選擇「是」將嘗試遷移資料(可能會有資料損失或錯誤),選擇「否」將中止更新過程。";
|
|
LogMessage(issueSummary.Replace("\n", Environment.NewLine), true);
|
|
if (MessageBox.Show(issueSummary.Replace("\\n", Environment.NewLine), "遷移警告", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.No)
|
|
{ LogMessage("使用者選擇因潛在問題中止遷移。", true); return false; } // User abort, not an internal error
|
|
LogMessage("使用者選擇繼續遷移,忽略警告。");
|
|
}
|
|
|
|
LogMessage("開始遷移資料表...");
|
|
progress?.Report("Starting table data migration...");
|
|
using (var transaction = connection.BeginTransaction(System.Data.IsolationLevel.ReadCommitted))
|
|
{
|
|
try
|
|
{
|
|
foreach (var table in tablesForMigrationLoop) // Use the scoped list
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
if (table.Equals("VERSION", StringComparison.OrdinalIgnoreCase) || tablesToSkipMigration.Contains(table) || !tableRowCounts.ContainsKey(table) || tableRowCounts[table] == 0)
|
|
{
|
|
if (!tableRowCounts.ContainsKey(table) || tableRowCounts[table] == 0 && !tablesToSkipMigration.Contains(table) && !table.Equals("VERSION", StringComparison.OrdinalIgnoreCase))
|
|
LogMessage($"表格 {table} 來源無資料,跳過遷移。");
|
|
else if (tablesToSkipMigration.Contains(table))
|
|
LogMessage($"跳過表格 {table} 的遷移(使用者選擇或預先檢查跳過)。");
|
|
progress?.Report($"Skipping table {table} (no data or pre-check skip).");
|
|
continue;
|
|
}
|
|
LogMessage($"準備遷移表格: {table}, {tableRowCounts[table]} 筆資料。");
|
|
progress?.Report($"Preparing to migrate table: {table}, {tableRowCounts[table]} rows.");
|
|
|
|
var sourceSchema = backupTableColumnInfoCache[table]; // Already checked for key existence
|
|
var currentTargetSchema = tableColumnInfoCache[table]; // Corrected variable name
|
|
|
|
var columnsToInclude = sourceSchema.Keys.Intersect(currentTargetSchema.Keys, StringComparer.OrdinalIgnoreCase)
|
|
.Where(cn => !textTypeColumns.Any(ttc => ttc.TableName.Equals(table, StringComparison.OrdinalIgnoreCase) && ttc.ColumnName.Equals(cn, StringComparison.OrdinalIgnoreCase) && !ttc.IsCompatible))
|
|
.ToList();
|
|
var columnsToInsertInTarget = new List<string>();
|
|
var selectClausesForSource = new List<string>();
|
|
|
|
foreach (var commonColName in columnsToInclude)
|
|
{
|
|
var sourceInfo = sourceSchema[commonColName];
|
|
var targetInfo = currentTargetSchema[commonColName]; // Use corrected variable
|
|
|
|
// Use the updated IsTypeCompatible with 4 arguments
|
|
if (!IsTypeCompatible(sourceInfo.DataType, targetInfo.DataType, sourceInfo.MaxLength, targetInfo.MaxLength) &&
|
|
!textTypeColumns.Any(t => t.TableName.Equals(table, StringComparison.OrdinalIgnoreCase) && t.ColumnName.Equals(commonColName, StringComparison.OrdinalIgnoreCase) && t.IsCompatible))
|
|
{
|
|
LogMessage($"遷移時再次確認欄位 {table}.{commonColName} 類型不相容 ({sourceInfo.DataType} -> {targetInfo.DataType}),此欄位將不被包含在 INSERT 中。", true);
|
|
continue;
|
|
}
|
|
columnsToInsertInTarget.Add($"[{commonColName}]");
|
|
string selectClause = $"S.[{commonColName}]"; // Alias source table as S
|
|
|
|
bool sourceIsCharOrText = sourceInfo.DataType.IndexOf("char", StringComparison.OrdinalIgnoreCase) >= 0 || sourceInfo.DataType.IndexOf("text", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
bool targetIsCharOrText = targetInfo.DataType.IndexOf("char", StringComparison.OrdinalIgnoreCase) >= 0 || targetInfo.DataType.IndexOf("text", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
|
|
if (sourceIsCharOrText && targetIsCharOrText &&
|
|
sourceInfo.MaxLength < (int.MaxValue / 2 - 20) && targetInfo.MaxLength < (int.MaxValue / 2 - 20) &&
|
|
sourceInfo.MaxLength > targetInfo.MaxLength)
|
|
{
|
|
selectClause = $"LEFT(S.[{commonColName}], {targetInfo.MaxLength})";
|
|
}
|
|
else if ((sourceInfo.DataType.Equals("text", StringComparison.OrdinalIgnoreCase) || sourceInfo.DataType.Equals("ntext", StringComparison.OrdinalIgnoreCase)) &&
|
|
textTypeColumns.Any(t => t.TableName.Equals(table, StringComparison.OrdinalIgnoreCase) && t.ColumnName.Equals(commonColName, StringComparison.OrdinalIgnoreCase) && t.IsCompatible))
|
|
{
|
|
string targetTypeForCast = targetInfo.DataType;
|
|
if (targetInfo.DataType.IndexOf("varchar", StringComparison.OrdinalIgnoreCase) >= 0 && (targetInfo.MaxLength >= (int.MaxValue / 2 - 20) || targetInfo.MaxLength == -1))
|
|
targetTypeForCast += "(MAX)";
|
|
else if (targetInfo.DataType.IndexOf("varchar", StringComparison.OrdinalIgnoreCase) >= 0) // Non-MAX varchar
|
|
targetTypeForCast += $"({targetInfo.MaxLength})";
|
|
// For text/ntext target, no length needed in CAST
|
|
selectClause = $"CAST(S.[{commonColName}] AS {targetTypeForCast})";
|
|
}
|
|
selectClausesForSource.Add(selectClause + $" AS [{commonColName}]");
|
|
}
|
|
|
|
if (!columnsToInsertInTarget.Any()) { LogMessage($"表格 {table} 沒有可遷移的共同且相容的欄位,跳過。"); progress?.Report($"Table {table} has no common compatible columns, skipping."); continue; }
|
|
|
|
foreach (var targetColNameToInsert in columnsToInsertInTarget.Select(c => c.Trim('[', ']')))
|
|
{
|
|
var targetColInfo = currentTargetSchema[targetColNameToInsert]; // Use corrected variable
|
|
if (!targetColInfo.IsNullable && targetColInfo.ColumnDefault == null && sourceSchema.ContainsKey(targetColNameToInsert))
|
|
{
|
|
int nullCountInSource = 0;
|
|
using (var cmdNullCheck = new SqlCommand($"SELECT COUNT_BIG(*) FROM [{backupDbName}].[dbo].[{table}] WITH (NOLOCK) WHERE [{targetColNameToInsert}] IS NULL", connection, transaction))
|
|
{
|
|
var res = await cmdNullCheck.ExecuteScalarAsync(cancellationToken);
|
|
if (res != DBNull.Value) nullCountInSource = Convert.ToInt32(res);
|
|
}
|
|
if (nullCountInSource > 0)
|
|
{
|
|
LogMessage($"錯誤:資料庫設計錯誤 - 表格 [{table}] 的欄位 [{targetColNameToInsert}] 在新版本被設定為必填(NOT NULL)且無預設值,但舊版本資料中有 {nullCountInSource} 筆包含 NULL 值。", true);
|
|
if (MessageBox.Show($"發現資料庫設計錯誤:\n\n表格 [{table}] 的欄位 [{targetColNameToInsert}] 在新版本被設定為必填(NOT NULL)且無預設值,但舊版本資料中有 {nullCountInSource} 筆包含 NULL 值。\n\n此欄位設計不相容於舊版本,請修正欄位定義再重新更新。\n\n警告:即使選擇跳過此表格繼續執行,系統仍可能因資料庫結構不一致而無法正常運作。\n\n是否跳過此表格 [{table}] 的資料遷移?\n選擇「是」以跳過此表格。\n選擇「否」以中止整個資料庫更新過程。",
|
|
"資料庫設計錯誤", MessageBoxButton.YesNo, MessageBoxImage.Error) == MessageBoxResult.Yes)
|
|
{ tablesToSkipMigration.Add(table); goto NextTableLoop; }
|
|
else { LogMessage("使用者選擇因 NULL 值衝突中止更新。", true); _migrationFailedDueToError = true; transaction.Rollback(); return false; }
|
|
}
|
|
}
|
|
}
|
|
|
|
string insertColumnsSql = string.Join(", ", columnsToInsertInTarget);
|
|
string selectColumnsSql = string.Join(", ", selectClausesForSource);
|
|
bool hasIdentity = Convert.ToInt32(await new SqlCommand($"SELECT OBJECTPROPERTY(OBJECT_ID('[dbo].[{table}]'), 'TableHasIdentity')", connection, transaction).ExecuteScalarAsync(cancellationToken)) == 1;
|
|
if (hasIdentity) await new SqlCommand($"SET IDENTITY_INSERT [dbo].[{table}] ON", connection, transaction).ExecuteNonQueryAsync(cancellationToken);
|
|
|
|
// Alias source table as S in the FROM clause
|
|
string migrationSql = $"INSERT INTO [dbo].[{table}] ({insertColumnsSql}) SELECT {selectColumnsSql} FROM [{backupDbName}].[dbo].[{table}] S";
|
|
LogMessage($"執行遷移 SQL: {migrationSql}");
|
|
using (var migrateCmd = new SqlCommand(migrationSql, connection, transaction))
|
|
{
|
|
migrateCmd.CommandTimeout = 600;
|
|
int rowsAffected = await migrateCmd.ExecuteNonQueryAsync(cancellationToken);
|
|
LogMessage($"表格 {table} 資料遷移完成,影響 {rowsAffected} 筆。");
|
|
progress?.Report($"Table {table} migrated, {rowsAffected} rows affected.");
|
|
}
|
|
if (hasIdentity) await new SqlCommand($"SET IDENTITY_INSERT [dbo].[{table}] OFF", connection, transaction).ExecuteNonQueryAsync(cancellationToken);
|
|
NextTableLoop:;
|
|
}
|
|
|
|
if (textTypeColumns.Any(t => t.IsCompatible)) // This separate TEXT/NTEXT update loop might be redundant if CAST in main loop handles it
|
|
{
|
|
LogMessage("開始遷移/更新相容的 TEXT/NTEXT 類型欄位資料 (批次)...");
|
|
progress?.Report("Updating compatible TEXT/NTEXT column data (batched)...");
|
|
foreach (var textColInfo in textTypeColumns.Where(t => t.IsCompatible))
|
|
{
|
|
if (tablesToSkipMigration.Contains(textColInfo.TableName) || !tableRowCounts.ContainsKey(textColInfo.TableName) || tableRowCounts[textColInfo.TableName] == 0) continue;
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
LogMessage($"處理表格 {textColInfo.TableName} 的 TEXT/NTEXT 欄位 {textColInfo.ColumnName} (遷移到 {textColInfo.NewType})");
|
|
|
|
string pkColumnName = "";
|
|
using (SqlCommand pkCmd = new SqlCommand($"SELECT KU.COLUMN_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KU ON TC.CONSTRAINT_TYPE = 'PRIMARY KEY' AND TC.CONSTRAINT_NAME = KU.CONSTRAINT_NAME AND KU.TABLE_SCHEMA = TC.TABLE_SCHEMA AND KU.TABLE_NAME = TC.TABLE_NAME WHERE TC.TABLE_NAME = '{textColInfo.TableName}' AND TC.TABLE_SCHEMA = 'dbo' ORDER BY KU.ORDINAL_POSITION", connection, transaction))
|
|
pkColumnName = await pkCmd.ExecuteScalarAsync(cancellationToken) as string;
|
|
|
|
if (string.IsNullOrEmpty(pkColumnName)) { LogMessage($"警告:表格 {textColInfo.TableName} 找不到主鍵,無法批次更新 TEXT 欄位 {textColInfo.ColumnName}。此欄位資料可能未透過主 INSERT 遷移。", true); continue; }
|
|
|
|
var currentTargetSchemaForText = tableColumnInfoCache[textColInfo.TableName]; // Get target schema for length
|
|
var targetColForText = currentTargetSchemaForText[textColInfo.ColumnName];
|
|
string targetTypeWithLength = targetColForText.DataType;
|
|
if (targetTypeWithLength.IndexOf("varchar", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
targetTypeWithLength += (targetColForText.MaxLength >= (int.MaxValue / 2 - 20) || targetColForText.MaxLength == -1) ? "(MAX)" : $"({targetColForText.MaxLength})";
|
|
}
|
|
// For text/ntext itself, no length in cast
|
|
|
|
int batchSize = 100, offset = 0; bool moreRecords = true;
|
|
while (moreRecords)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
string updateSql = $@"
|
|
UPDATE T SET T.[{textColInfo.ColumnName}] = CAST(S.[{textColInfo.ColumnName}] AS {targetTypeWithLength})
|
|
FROM [dbo].[{textColInfo.TableName}] T INNER JOIN (
|
|
SELECT [{pkColumnName}], [{textColInfo.ColumnName}] FROM [{backupDbName}].[dbo].[{textColInfo.TableName}]
|
|
ORDER BY [{pkColumnName}] OFFSET {offset} ROWS FETCH NEXT {batchSize} ROWS ONLY
|
|
) S ON T.[{pkColumnName}] = S.[{pkColumnName}] WHERE S.[{textColInfo.ColumnName}] IS NOT NULL;";
|
|
using (var textUpdateCmd = new SqlCommand(updateSql, connection, transaction))
|
|
{
|
|
textUpdateCmd.CommandTimeout = 300;
|
|
int rows = await textUpdateCmd.ExecuteNonQueryAsync(cancellationToken);
|
|
LogMessage($"TEXT/NTEXT 欄位 {textColInfo.TableName}.{textColInfo.ColumnName} 更新了 {rows} 筆資料 (批次 {offset / batchSize + 1})。");
|
|
if (rows < batchSize) moreRecords = false;
|
|
offset += batchSize;
|
|
}
|
|
progress?.Report($"Table {textColInfo.TableName}.{textColInfo.ColumnName} TEXT data batch {offset / batchSize} migrated.");
|
|
}
|
|
}
|
|
}
|
|
transaction.Commit();
|
|
LogMessage("所有表格資料遷移完成。");
|
|
progress?.Report("All table data migration completed.");
|
|
return true;
|
|
}
|
|
catch (OperationCanceledException) { LogMessage("資料遷移操作已取消。", true); progress?.Report("Data migration operation cancelled."); try { transaction?.Rollback(); } catch (Exception rbEx) { LogMessage($"取消時回滾失敗: {rbEx.Message}", true); } throw; }
|
|
catch (Exception ex) { LogMessage($"資料遷移時發生錯誤: {ex.Message}{Environment.NewLine}SQL: {(ex is SqlException sqlEx ? sqlEx.ToString() : string.Empty)}{Environment.NewLine}StackTrace: {ex.StackTrace}", true); progress?.Report($"Error during data migration: {ex.Message}"); try { transaction?.Rollback(); } catch (Exception rbEx) { LogMessage($"錯誤後回滾失敗: {rbEx.Message}", true); } _migrationFailedDueToError = true; return false; }
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException opEx) { LogMessage($"資料遷移任務被使用者或程式邏輯取消: {opEx.Message}", true); progress?.Report("Data migration task was cancelled."); throw; }
|
|
catch (Exception ex) { LogMessage($"資料遷移時發生未預期錯誤 (外層): {ex.Message}{Environment.NewLine}StackTrace: {ex.StackTrace}", true); progress?.Report($"Data migration failed due to an unexpected error (outer): {ex.Message}"); _migrationFailedDueToError = true; return false; }
|
|
}
|
|
|
|
// Helper function to determine if a target type can store text from a source TEXT column
|
|
private bool IsTypeCompatibleForText(string newType)
|
|
{
|
|
string nType = newType.ToLower();
|
|
return nType.Contains("char") || nType.Contains("text") || nType.Contains("xml") || nType.Contains("json");
|
|
}
|
|
// Helper function (add this within your class if it doesn't exist)
|
|
private bool IsTypeCompatible(string oldType, string newType, int oldLength = 0, int newLength = 0)
|
|
{
|
|
oldType = oldType.ToLower();
|
|
newType = newType.ToLower();
|
|
|
|
if (oldType == newType)
|
|
{
|
|
// If types are the same, check length for char types (excluding MAX which is represented by large int or -1)
|
|
bool oldIsChar = oldType.Contains("char");
|
|
bool oldIsMax = oldLength <= 0 || oldLength >= (int.MaxValue / 2 - 20); // Approximation for MAX
|
|
bool newIsMax = newLength <= 0 || newLength >= (int.MaxValue / 2 - 20);
|
|
|
|
if (oldIsChar && !oldIsMax && !newIsMax && oldLength > newLength)
|
|
{
|
|
//LogMessage($"類型 {oldType} 相同但長度縮短從 {oldLength} 到 {newLength},視為不完全相容(可能截斷)。");
|
|
return false; // Strict: if length shrinks, consider incompatible for direct migration without truncation
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Allow numeric to numeric (int, smallint, bigint, tinyint, decimal, numeric, float, real, money, smallmoney)
|
|
List<string> numericTypes = new List<string> { "int", "smallint", "bigint", "tinyint", "decimal", "numeric", "float", "real", "money", "smallmoney" };
|
|
if (numericTypes.Contains(oldType) && numericTypes.Contains(newType)) return true;
|
|
|
|
// Allow char types to char types (char, varchar, nchar, nvarchar)
|
|
// TEXT to char types is handled by IsTypeCompatibleForText or specific logic
|
|
List<string> charTypes = new List<string> { "char", "varchar", "nchar", "nvarchar" };
|
|
if (charTypes.Contains(oldType) && charTypes.Contains(newType))
|
|
{
|
|
// If converting between char types, and new length is smaller (and not MAX), it's a potential issue handled by truncation logic, but base types are compatible.
|
|
// bool oldIsMax = oldLength <= 0 || oldLength >= (int.MaxValue / 2 - 20);
|
|
// bool newIsMax = newLength <= 0 || newLength >= (int.MaxValue / 2 - 20);
|
|
// if (!oldIsMax && !newIsMax && oldLength > newLength) return false; // Already handled by truncation logic, so here it's type compatible
|
|
return true;
|
|
}
|
|
|
|
// Allow TEXT to NTEXT and vice-versa, and TEXT/NTEXT to compatible char(max) types
|
|
if ((oldType == "text" || oldType == "ntext") && (newType == "text" || newType == "ntext")) return true;
|
|
if ((oldType == "text" || oldType == "ntext") && (newType.Contains("varchar") || newType.Contains("nvarchar")) && (newLength >= (int.MaxValue / 2 - 20) || newLength == -1)) return true; // To varchar(max)/nvarchar(max)
|
|
|
|
|
|
// Allow datetime to datetime (datetime, smalldatetime, date, time, datetime2, datetimeoffset)
|
|
List<string> dateTimeTypes = new List<string> { "datetime", "smalldatetime", "date", "time", "datetime2", "datetimeoffset" };
|
|
if (dateTimeTypes.Contains(oldType) && dateTimeTypes.Contains(newType)) return true;
|
|
|
|
if (oldType == "bit" && newType == "bit") return true;
|
|
if (oldType == "uniqueidentifier" && newType == "uniqueidentifier") return true;
|
|
if (oldType == "timestamp" && newType == "binary" && newLength == 8) return true; // timestamp is rowversion (binary(8))
|
|
if (oldType == "rowversion" && newType == "binary" && newLength == 8) return true;
|
|
|
|
|
|
// Add more specific compatibility rules if needed
|
|
// For example, int to varchar might be acceptable in some scenarios (but needs explicit cast and is handled by migration logic, not here)
|
|
// For now, keep it relatively strict for automatic migration check.
|
|
|
|
LogMessage($"類型不相容判斷: 舊類型 '{oldType}' (長度 {oldLength}) 到 新類型 '{newType}' (長度 {newLength}) 被認為不相容。", true);
|
|
return false;
|
|
}
|
|
|
|
// Helper to check for numeric types for PK batching
|
|
private bool IsNumericType(string dataType)
|
|
{
|
|
string dt = dataType.ToLower();
|
|
return dt.Contains("int") || dt.Contains("decimal") || dt.Contains("numeric") || dt.Contains("float") || dt.Contains("real") || dt.Contains("money");
|
|
}
|
|
|
|
private async Task<bool> MigrateData_backup(IProgress<string> progress = null, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
LogMessage("開始資料遷移...");
|
|
|
|
// 檢查資料庫連線狀態
|
|
if (!await WaitForDatabaseConnection(_connectionString, "BC_Dev", 30, 1000, 10000))
|
|
{
|
|
LogMessage("無法連接到資料庫,取消資料遷移", true);
|
|
return false;
|
|
}
|
|
|
|
using (var connection = new SqlConnection(_connectionString))
|
|
{
|
|
await connection.OpenAsync();
|
|
|
|
// 取得備份資料庫名稱
|
|
string backupDbName = "";
|
|
using (var command = new SqlCommand(@"
|
|
SELECT name
|
|
FROM sys.databases
|
|
WHERE name LIKE 'BC_Dev_%'
|
|
AND name != 'BC_Dev'
|
|
ORDER BY create_date DESC", connection))
|
|
{
|
|
backupDbName = (string)command.ExecuteScalar();
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(backupDbName))
|
|
{
|
|
throw new Exception("找不到備份資料庫");
|
|
}
|
|
|
|
LogMessage($"找到備份資料庫: {backupDbName}");
|
|
// 移除重複的進度報告
|
|
// progress?.Report($"找到備份資料庫: {backupDbName}");
|
|
|
|
// 檢查是否請求取消
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
// 定義一個結構來存儲欄位長度資訊
|
|
var columnLengthIssues = new List<(string TableName, string ColumnName, int OldLength, int NewLength, string SampleValue)>();
|
|
|
|
// 定義一個結構來存儲欄位類型不相容的資訊
|
|
var incompatibleColumnTypes = new List<(string TableName, string ColumnName, string OldType, string NewType)>();
|
|
|
|
// 定義一個結構來存儲不允許NULL且不相容的欄位資訊
|
|
var notNullIncompatibleColumns = new List<(string TableName, string ColumnName, string ColumnType, string DefaultValue)>();
|
|
|
|
// 定義一個結構來存儲TEXT類型欄位的資訊
|
|
var textTypeColumns = new List<(string TableName, string ColumnName, string NewType, bool IsCompatible)>();
|
|
|
|
// 預先緩存所有表格的資料筆數
|
|
var tableRowCounts = new Dictionary<string, int>();
|
|
|
|
// 預先緩存所有表格的欄位資訊
|
|
var tableColumnsCache = new Dictionary<string, List<string>>();
|
|
var tableColumnInfoCache = new Dictionary<string, Dictionary<string, (string DataType, int MaxLength)>>();
|
|
var backupTableColumnsCache = new Dictionary<string, List<string>>();
|
|
var backupTableColumnInfoCache = new Dictionary<string, Dictionary<string, (string DataType, int MaxLength)>>();
|
|
|
|
// 在遷移之前檢查欄位類型相容性和長度問題
|
|
LogMessage("檢查資料欄位類型相容性及長度...");
|
|
using (var checkTransaction = connection.BeginTransaction())
|
|
{
|
|
try
|
|
{
|
|
// 取得所有表格名稱
|
|
var tables = new List<string>();
|
|
using (var command = new SqlCommand("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'", connection, checkTransaction))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
tables.Add(reader.GetString(0));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 取得所有表格的 NOT NULL 欄位資訊
|
|
var notNullColumnsInfo = new Dictionary<string, HashSet<string>>();
|
|
var columnDefaultValues = new Dictionary<string, Dictionary<string, string>>();
|
|
|
|
LogMessage("檢查資料表的 NOT NULL 欄位...");
|
|
foreach (var table in tables)
|
|
{
|
|
notNullColumnsInfo[table] = new HashSet<string>();
|
|
columnDefaultValues[table] = new Dictionary<string, string>();
|
|
|
|
using (var command = new SqlCommand($@"
|
|
SELECT
|
|
c.name AS ColumnName,
|
|
t.name AS DataType,
|
|
c.is_nullable AS IsNullable,
|
|
d.definition AS DefaultValue
|
|
FROM sys.columns c
|
|
INNER JOIN sys.types t ON c.system_type_id = t.system_type_id
|
|
LEFT JOIN sys.default_constraints d ON c.default_object_id = d.object_id
|
|
WHERE OBJECT_NAME(c.object_id) = '{table}'
|
|
AND c.is_nullable = 0", connection, checkTransaction))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
string columnName = reader["ColumnName"].ToString();
|
|
string defaultValue = reader["DefaultValue"] != DBNull.Value ? reader["DefaultValue"].ToString() : null;
|
|
notNullColumnsInfo[table].Add(columnName);
|
|
columnDefaultValues[table][columnName] = defaultValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 預先查詢所有表格的資料筆數
|
|
LogMessage("預先查詢所有表格的資料筆數...");
|
|
foreach (var table in tables)
|
|
{
|
|
// 跳過 VERSION 表格
|
|
if (table == "VERSION")
|
|
continue;
|
|
|
|
using (var command = new SqlCommand($"SELECT COUNT(*) FROM {backupDbName}.dbo.{table}", connection, checkTransaction))
|
|
{
|
|
try
|
|
{
|
|
int count = Convert.ToInt32(command.ExecuteScalar());
|
|
tableRowCounts[table] = count;
|
|
LogMessage($"表格 {table} 資料筆數: {count}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"查詢表格 {table} 資料筆數時出錯: {ex.Message}", true);
|
|
tableRowCounts[table] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 預先查詢所有有資料表格的欄位資訊
|
|
LogMessage("預先查詢所有有資料表格的欄位資訊...");
|
|
foreach (var table in tables)
|
|
{
|
|
// 跳過 VERSION 表格和無資料表格
|
|
if (table == "VERSION" || !tableRowCounts.ContainsKey(table) || tableRowCounts[table] == 0)
|
|
continue;
|
|
|
|
// 查詢備份資料庫的欄位資訊
|
|
var oldColumnInfo = new Dictionary<string, (string DataType, int MaxLength)>();
|
|
using (var command = new SqlCommand($@"
|
|
SELECT COLUMN_NAME, DATA_TYPE,
|
|
CASE
|
|
WHEN CHARACTER_MAXIMUM_LENGTH IS NULL THEN 0
|
|
ELSE CHARACTER_MAXIMUM_LENGTH
|
|
END AS CHARACTER_MAXIMUM_LENGTH
|
|
FROM {backupDbName}.INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_NAME = '{table}'", connection, checkTransaction))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
var columns = new List<string>();
|
|
while (reader.Read())
|
|
{
|
|
string columnName = reader.GetString(0);
|
|
string dataType = reader.GetString(1);
|
|
int maxLength = reader.GetInt32(2);
|
|
oldColumnInfo[columnName] = (dataType, maxLength);
|
|
columns.Add(columnName);
|
|
}
|
|
backupTableColumnsCache[table] = columns;
|
|
}
|
|
}
|
|
backupTableColumnInfoCache[table] = oldColumnInfo;
|
|
|
|
// 查詢新資料庫的欄位資訊
|
|
var newColumnInfo = new Dictionary<string, (string DataType, int MaxLength)>();
|
|
using (var command = new SqlCommand($@"
|
|
SELECT COLUMN_NAME, DATA_TYPE,
|
|
CASE
|
|
WHEN CHARACTER_MAXIMUM_LENGTH IS NULL THEN 0
|
|
ELSE CHARACTER_MAXIMUM_LENGTH
|
|
END AS CHARACTER_MAXIMUM_LENGTH
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_NAME = '{table}'", connection, checkTransaction))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
var columns = new List<string>();
|
|
while (reader.Read())
|
|
{
|
|
string columnName = reader.GetString(0);
|
|
string dataType = reader.GetString(1);
|
|
int maxLength = reader.GetInt32(2);
|
|
newColumnInfo[columnName] = (dataType, maxLength);
|
|
columns.Add(columnName);
|
|
}
|
|
tableColumnsCache[table] = columns;
|
|
}
|
|
}
|
|
tableColumnInfoCache[table] = newColumnInfo;
|
|
}
|
|
|
|
// 檢查每個表格的字符欄位長度
|
|
foreach (var table in tables)
|
|
{
|
|
// 跳過 VERSION 表格
|
|
if (table == "VERSION")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// 使用預先查詢的資料筆數,若無資料則跳過所有欄位檢查
|
|
if (!tableRowCounts.ContainsKey(table) || tableRowCounts[table] == 0)
|
|
{
|
|
LogMessage($"表格 {table} 無資料,跳過欄位檢查");
|
|
continue; // 跳過無資料的表格
|
|
}
|
|
|
|
LogMessage($"表格 {table} 有 {tableRowCounts[table]} 筆資料,進行欄位檢查");
|
|
|
|
// 使用緩存的欄位資訊
|
|
if (!backupTableColumnInfoCache.ContainsKey(table) || !tableColumnInfoCache.ContainsKey(table))
|
|
{
|
|
LogMessage($"表格 {table} 無欄位緩存資訊,跳過欄位檢查");
|
|
continue;
|
|
}
|
|
|
|
var oldColumnInfo = backupTableColumnInfoCache[table];
|
|
var newColumnInfo = tableColumnInfoCache[table];
|
|
|
|
// 找出共同存在的欄位
|
|
var commonColumns = oldColumnInfo.Keys.Intersect(newColumnInfo.Keys).ToList();
|
|
|
|
// 檢查每個欄位的類型相容性和長度問題
|
|
foreach (var column in commonColumns)
|
|
{
|
|
var oldInfo = oldColumnInfo[column];
|
|
var newInfo = newColumnInfo[column];
|
|
|
|
// 檢查類型是否相容
|
|
bool isIncompatible = false;
|
|
|
|
// 定義相容的類型組合
|
|
bool isCompatible = false;
|
|
|
|
// 檢查是否為TEXT類型
|
|
bool isTextType = oldInfo.DataType.ToLower() == "text";
|
|
if (isTextType)
|
|
{
|
|
// 檢查新類型是否可以存儲字串
|
|
bool newTypeCanStoreString = newInfo.DataType.ToLower().Contains("char") ||
|
|
newInfo.DataType.ToLower().Contains("text") ||
|
|
newInfo.DataType.ToLower().Contains("xml") ||
|
|
newInfo.DataType.ToLower().Contains("json");
|
|
|
|
if (newTypeCanStoreString)
|
|
{
|
|
// 記錄TEXT類型欄位資訊
|
|
textTypeColumns.Add((table, column, newInfo.DataType, true));
|
|
isCompatible = true;
|
|
LogMessage($"發現TEXT類型欄位可能相容: 表格 {table}, 欄位 {column}, 新類型 {newInfo.DataType}");
|
|
}
|
|
else
|
|
{
|
|
// 記錄不相容的TEXT類型欄位
|
|
textTypeColumns.Add((table, column, newInfo.DataType, false));
|
|
isCompatible = false;
|
|
LogMessage($"發現TEXT類型欄位不相容: 表格 {table}, 欄位 {column}, 新類型 {newInfo.DataType}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 字符類型間相容(如varchar/nvarchar/char/nchar)
|
|
bool oldIsCharType = oldInfo.DataType.ToLower().Contains("char");
|
|
bool newIsCharType = newInfo.DataType.ToLower().Contains("char");
|
|
if (oldIsCharType && newIsCharType)
|
|
{
|
|
isCompatible = true;
|
|
LogMessage($"發現可相容的欄位類型變更: 表格 {table}, 欄位 {column}, 舊類型 {oldInfo.DataType}, 新類型 {newInfo.DataType}");
|
|
}
|
|
|
|
// 數值類型間相容(如int/smallint/tinyint/bigint)
|
|
bool oldIsIntType = oldInfo.DataType.ToLower().Contains("int");
|
|
bool newIsIntType = newInfo.DataType.ToLower().Contains("int");
|
|
if (oldIsIntType && newIsIntType)
|
|
{
|
|
isCompatible = true;
|
|
LogMessage($"發現可相容的欄位類型變更: 表格 {table}, 欄位 {column}, 舊類型 {oldInfo.DataType}, 新類型 {newInfo.DataType}");
|
|
}
|
|
}
|
|
|
|
// 如果不是相容的類型組合,且不是TEXT類型,則標記為不相容
|
|
if (!isCompatible && !isTextType)
|
|
{
|
|
isIncompatible = true;
|
|
LogMessage($"發現不相容的欄位類型: 表格 {table}, 欄位 {column}, 舊類型 {oldInfo.DataType}, 新類型 {newInfo.DataType}");
|
|
}
|
|
|
|
if (isIncompatible)
|
|
{
|
|
incompatibleColumnTypes.Add((table, column, oldInfo.DataType, newInfo.DataType));
|
|
|
|
// 檢查此欄位是否設為NOT NULL且沒有預設值
|
|
bool isNotNull = false;
|
|
string defaultValue = null;
|
|
|
|
// 檢查欄位的NULL約束和預設值
|
|
using (var nullCheckCommand = new SqlCommand($@"
|
|
SELECT
|
|
CASE WHEN c.is_nullable = 0 THEN 1 ELSE 0 END AS IsNotNull,
|
|
CASE WHEN d.definition IS NULL THEN NULL ELSE d.definition END AS DefaultValue
|
|
FROM sys.columns c
|
|
INNER JOIN sys.tables t ON c.object_id = t.object_id
|
|
LEFT JOIN sys.default_constraints d ON c.default_object_id = d.object_id
|
|
WHERE t.name = '{table}' AND c.name = '{column}'", connection, checkTransaction))
|
|
{
|
|
using (var reader = nullCheckCommand.ExecuteReader())
|
|
{
|
|
if (reader.Read())
|
|
{
|
|
isNotNull = Convert.ToBoolean(reader["IsNotNull"]);
|
|
if (!reader.IsDBNull(reader.GetOrdinal("DefaultValue")))
|
|
{
|
|
defaultValue = reader["DefaultValue"].ToString();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果是NOT NULL且沒有預設值,記錄到特殊列表
|
|
if (isNotNull && defaultValue == null)
|
|
{
|
|
notNullIncompatibleColumns.Add((table, column, newInfo.DataType, defaultValue));
|
|
LogMessage($"警告:欄位類型不相容且設為NOT NULL沒有預設值: 表格 {table}, 欄位 {column}", true);
|
|
}
|
|
}
|
|
|
|
// 檢查字符欄位長度問題(與原本的邏輯一致)
|
|
bool isCharType = oldInfo.DataType.Contains("char") && newInfo.DataType.Contains("char");
|
|
|
|
if (isCharType && oldInfo.MaxLength > newInfo.MaxLength && oldInfo.MaxLength > 0 && newInfo.MaxLength > 0)
|
|
{
|
|
// 先輸出簡化的日誌,保持與原本格式一致
|
|
LogMessage($"發現欄位長度變更: 表格 {table}, 欄位 {column}, 舊長度 {oldInfo.MaxLength}, 新長度 {newInfo.MaxLength}");
|
|
|
|
// 檢查是否有實際資料超出新長度限制
|
|
string sampleValue = "";
|
|
bool hasIssue = false;
|
|
int affectedRows = 0;
|
|
int maxLength = 0;
|
|
|
|
// 計算受影響的資料筆數
|
|
using (var countCommand = new SqlCommand($@"
|
|
SELECT COUNT(*)
|
|
FROM {backupDbName}.dbo.{table}
|
|
WHERE LEN({column}) > {newInfo.MaxLength}", connection, checkTransaction))
|
|
{
|
|
affectedRows = Convert.ToInt32(countCommand.ExecuteScalar());
|
|
}
|
|
|
|
// 取得最大長度值
|
|
using (var maxCommand = new SqlCommand($@"
|
|
SELECT MAX(LEN({column}))
|
|
FROM {backupDbName}.dbo.{table}
|
|
WHERE LEN({column}) > {newInfo.MaxLength}", connection, checkTransaction))
|
|
{
|
|
var maxResult = maxCommand.ExecuteScalar();
|
|
if (maxResult != null && maxResult != DBNull.Value)
|
|
{
|
|
maxLength = Convert.ToInt32(maxResult);
|
|
}
|
|
}
|
|
|
|
// 取得範例值
|
|
using (var command = new SqlCommand($@"
|
|
SELECT TOP 1 {column}
|
|
FROM {backupDbName}.dbo.{table}
|
|
WHERE LEN({column}) > {newInfo.MaxLength}
|
|
ORDER BY LEN({column}) DESC", connection, checkTransaction))
|
|
{
|
|
var result = command.ExecuteScalar();
|
|
if (result != null && result != DBNull.Value)
|
|
{
|
|
sampleValue = result.ToString();
|
|
hasIssue = true;
|
|
}
|
|
}
|
|
|
|
if (hasIssue)
|
|
{
|
|
// 先輸出簡化的日誌,保持與原本格式一致
|
|
LogMessage($"發現欄位長度問題: 表格 {table}, 欄位 {column}, 舊長度 {oldInfo.MaxLength}, 新長度 {newInfo.MaxLength}");
|
|
|
|
// 記錄更詳細的日誌
|
|
LogMessage($"【欄位長度問題詳情】表格: {table}");
|
|
LogMessage($" 欄位名稱: {column}");
|
|
LogMessage($" 欄位類型: {oldInfo.DataType}");
|
|
LogMessage($" 舊長度上限: {oldInfo.MaxLength}, 新長度上限: {newInfo.MaxLength}");
|
|
LogMessage($" 受影響資料筆數: {affectedRows}筆");
|
|
LogMessage($" 最大實際長度: {maxLength}字元");
|
|
if (sampleValue.Length > 100)
|
|
{
|
|
LogMessage($" 範例資料: {sampleValue.Substring(0, 97)}...(已截斷)");
|
|
}
|
|
else
|
|
{
|
|
LogMessage($" 範例資料: {sampleValue}");
|
|
}
|
|
|
|
columnLengthIssues.Add((table, column, oldInfo.MaxLength, newInfo.MaxLength, sampleValue));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
checkTransaction.Commit();
|
|
LogMessage("欄位類型相容性和長度檢查完成");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
checkTransaction.Rollback();
|
|
throw new Exception($"檢查欄位時發生錯誤: {ex.Message}", ex);
|
|
}
|
|
}
|
|
|
|
// 處理特別情況:不允許NULL且不相容的欄位
|
|
if (notNullIncompatibleColumns.Any())
|
|
{
|
|
var sb = new System.Text.StringBuilder();
|
|
sb.AppendLine("【重要警告】發現以下欄位同時符合三個條件:");
|
|
sb.AppendLine("1. 資料類型不相容,無法自動轉換");
|
|
sb.AppendLine("2. 欄位設定為NOT NULL(不接受NULL值)");
|
|
sb.AppendLine("3. 欄位沒有設定預設值");
|
|
sb.AppendLine();
|
|
sb.AppendLine("如果跳過這些欄位的資料遷移,INSERT操作可能會失敗:");
|
|
sb.AppendLine();
|
|
|
|
foreach (var issue in notNullIncompatibleColumns)
|
|
{
|
|
sb.AppendLine($"【表格:{issue.TableName}】");
|
|
sb.AppendLine($"欄位: {issue.ColumnName}");
|
|
sb.AppendLine($"類型: {issue.ColumnType}");
|
|
sb.AppendLine($"預設值: {(issue.DefaultValue == null ? "無" : issue.DefaultValue)}");
|
|
sb.AppendLine("==============================");
|
|
}
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine("請選擇處理方式:");
|
|
sb.AppendLine("- 「是」:仍然嘗試遷移資料(可能會失敗)");
|
|
sb.AppendLine("- 「否」:取消整個操作並還原資料庫");
|
|
|
|
// 在UI線程上彈出確認對話框
|
|
bool continueWithNotNullIssues = false;
|
|
// 記錄警告對話框內容到日誌
|
|
LogMessage("【對話框】顯示NOT NULL欄位不相容警告對話框", true);
|
|
LogMessage("對話框內容:");
|
|
LogMessage(sb.ToString());
|
|
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
var result = MessageBox.Show(
|
|
sb.ToString(),
|
|
"重要警告:NOT NULL欄位不相容 / Important Warning: NOT NULL Field Incompatibility",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Error);
|
|
|
|
continueWithNotNullIssues = (result == MessageBoxResult.Yes);
|
|
|
|
// 記錄用戶選擇
|
|
if (continueWithNotNullIssues)
|
|
{
|
|
LogMessage("【用戶選擇】用戶選擇繼續操作(是),即使有NOT NULL欄位不相容的風險");
|
|
}
|
|
else
|
|
{
|
|
LogMessage("【用戶選擇】用戶選擇取消操作(否),將放棄資料遷移並還原資料庫");
|
|
}
|
|
});
|
|
|
|
if (!continueWithNotNullIssues)
|
|
{
|
|
LogMessage("使用者選擇取消資料遷移,將還原資料庫");
|
|
return false;
|
|
}
|
|
|
|
LogMessage("使用者選擇繼續資料遷移,即使有NOT NULL不相容欄位的風險");
|
|
LogMessage("受影響的NOT NULL欄位清單:");
|
|
foreach (var issue in notNullIncompatibleColumns)
|
|
{
|
|
LogMessage($"- 表格 {issue.TableName}, 欄位 {issue.ColumnName}, 類型 {issue.ColumnType}");
|
|
}
|
|
}
|
|
|
|
// 處理一般不相容的欄位類型問題
|
|
if (incompatibleColumnTypes.Any())
|
|
{
|
|
var sb = new System.Text.StringBuilder();
|
|
sb.AppendLine("發現以下欄位的資料類型不相容,無法自動轉換:");
|
|
sb.AppendLine();
|
|
|
|
// 排除已在NOT NULL警告中顯示的欄位
|
|
var regularIncompatibleColumns = incompatibleColumnTypes
|
|
.Where(ic => !notNullIncompatibleColumns.Any(nnc =>
|
|
nnc.TableName == ic.TableName && nnc.ColumnName == ic.ColumnName))
|
|
.ToList();
|
|
|
|
foreach (var issue in regularIncompatibleColumns)
|
|
{
|
|
sb.AppendLine($"【表格:{issue.TableName}】");
|
|
sb.AppendLine($"欄位: {issue.ColumnName}");
|
|
sb.AppendLine($"舊類型: {issue.OldType}, 新類型: {issue.NewType}");
|
|
sb.AppendLine("==============================");
|
|
}
|
|
|
|
// 如果所有不相容欄位都是NOT NULL問題且已經顯示過警告,則跳過此對話框
|
|
if (regularIncompatibleColumns.Any())
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine("請選擇處理方式:");
|
|
sb.AppendLine("- 「是」:跳過這些欄位的資料遷移,這些欄位將保持為預設值或NULL");
|
|
sb.AppendLine("- 「否」:取消更新並還原資料庫");
|
|
|
|
// 在UI線程上彈出確認對話框
|
|
bool skipIncompatibleColumns = false;
|
|
// 記錄警告對話框內容到日誌
|
|
LogMessage("【對話框】顯示欄位類型不相容警告對話框", true);
|
|
LogMessage("對話框內容:");
|
|
LogMessage(sb.ToString());
|
|
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
var result = MessageBox.Show(
|
|
sb.ToString(),
|
|
"欄位類型不相容 / Field Type Incompatibility",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Warning);
|
|
|
|
skipIncompatibleColumns = (result == MessageBoxResult.Yes);
|
|
|
|
// 記錄用戶選擇
|
|
if (skipIncompatibleColumns)
|
|
{
|
|
LogMessage("【用戶選擇】用戶選擇跳過不相容欄位(是),將繼續資料遷移");
|
|
}
|
|
else
|
|
{
|
|
LogMessage("【用戶選擇】用戶選擇取消操作(否),將放棄資料遷移並還原資料庫");
|
|
}
|
|
});
|
|
|
|
if (!skipIncompatibleColumns)
|
|
{
|
|
LogMessage("使用者選擇取消資料遷移,將還原資料庫");
|
|
return false;
|
|
}
|
|
|
|
LogMessage("使用者選擇跳過不相容的欄位,將繼續資料遷移");
|
|
LogMessage("受影響的一般欄位清單:");
|
|
foreach (var issue in regularIncompatibleColumns)
|
|
{
|
|
LogMessage($"- 表格 {issue.TableName}, 欄位 {issue.ColumnName}, 舊類型 {issue.OldType}, 新類型 {issue.NewType}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果有欄位長度問題,詢問用戶如何處理
|
|
if (columnLengthIssues.Any())
|
|
{
|
|
var sb = new System.Text.StringBuilder();
|
|
sb.AppendLine("發現以下欄位中的資料長度超過新資料庫欄位允許的長度:");
|
|
sb.AppendLine();
|
|
|
|
foreach (var issue in columnLengthIssues)
|
|
{
|
|
sb.AppendLine($"【表格:{issue.TableName}】");
|
|
sb.AppendLine($"欄位: {issue.ColumnName}");
|
|
sb.AppendLine($"資料長度限制: {issue.OldLength}→{issue.NewLength} (減少 {issue.OldLength - issue.NewLength} 字元)");
|
|
sb.AppendLine($"範例值: {(issue.SampleValue.Length > 50 ? issue.SampleValue.Substring(0, 47) + "..." : issue.SampleValue)}");
|
|
sb.AppendLine($"此值長度: {issue.SampleValue.Length} (超出 {issue.SampleValue.Length - issue.NewLength} 字元)");
|
|
sb.AppendLine("==============================");
|
|
}
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine("是否仍然要繼續?");
|
|
sb.AppendLine("- 「是」:將截斷超過長度的資料");
|
|
sb.AppendLine("- 「否」:取消更新並還原資料庫");
|
|
|
|
// 在UI線程上彈出確認對話框
|
|
bool continueMigration = false;
|
|
// 記錄警告對話框內容到日誌
|
|
LogMessage("【對話框】顯示資料長度問題警告對話框", true);
|
|
LogMessage("對話框內容:");
|
|
LogMessage(sb.ToString());
|
|
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
var result = MessageBox.Show(
|
|
sb.ToString(),
|
|
"資料長度問題 / Data Length Issue",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Warning);
|
|
|
|
continueMigration = (result == MessageBoxResult.Yes);
|
|
|
|
// 記錄用戶選擇
|
|
if (continueMigration)
|
|
{
|
|
LogMessage("【用戶選擇】用戶選擇截斷超過長度的資料(是),將繼續資料遷移");
|
|
}
|
|
else
|
|
{
|
|
LogMessage("【用戶選擇】用戶選擇取消操作(否),將放棄資料遷移並還原資料庫");
|
|
}
|
|
});
|
|
|
|
if (!continueMigration)
|
|
{
|
|
LogMessage("使用者選擇取消資料遷移,將還原資料庫");
|
|
return false;
|
|
}
|
|
|
|
LogMessage("使用者選擇繼續資料遷移,將截斷超過長度的資料");
|
|
LogMessage("受影響的欄位清單:");
|
|
foreach (var issue in columnLengthIssues)
|
|
{
|
|
LogMessage($"- 表格 {issue.TableName}, 欄位 {issue.ColumnName}, 舊長度 {issue.OldLength}, 新長度 {issue.NewLength}");
|
|
}
|
|
}
|
|
|
|
using (var transaction = connection.BeginTransaction())
|
|
{
|
|
try
|
|
{
|
|
// 取得所有表格名稱
|
|
var tables = new List<string>();
|
|
using (var command = new SqlCommand("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'", connection, transaction))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
tables.Add(reader.GetString(0));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 對每個表格進行資料遷移
|
|
foreach (var table in tables)
|
|
{
|
|
// 跳過 VERSION 表格的資料遷移
|
|
if (table == "VERSION")
|
|
{
|
|
LogMessage("跳過 VERSION 表格的資料遷移");
|
|
continue;
|
|
}
|
|
|
|
LogMessage($"處理表格 {table} 的資料遷移");
|
|
|
|
// 使用緩存的資料筆數
|
|
if (!tableRowCounts.ContainsKey(table) || tableRowCounts[table] == 0)
|
|
{
|
|
LogMessage($"表格 {table} 無資料或未緩存,跳過遷移");
|
|
continue;
|
|
}
|
|
|
|
int rowCount = tableRowCounts[table];
|
|
LogMessage($"表格 {table} 有 {rowCount} 筆資料需要遷移");
|
|
|
|
// 使用緩存的欄位資訊
|
|
List<string> oldColumns;
|
|
List<string> newColumns;
|
|
|
|
if (backupTableColumnsCache.ContainsKey(table))
|
|
{
|
|
oldColumns = backupTableColumnsCache[table];
|
|
}
|
|
else
|
|
{
|
|
oldColumns = new List<string>();
|
|
// 從備份資料庫取得欄位資訊
|
|
using (var command = new SqlCommand($@"
|
|
SELECT COLUMN_NAME
|
|
FROM {backupDbName}.INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_NAME = '{table}'", connection, transaction))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
oldColumns.Add(reader.GetString(0));
|
|
}
|
|
}
|
|
}
|
|
// 緩存結果,以便後續使用
|
|
backupTableColumnsCache[table] = oldColumns;
|
|
}
|
|
|
|
if (tableColumnsCache.ContainsKey(table))
|
|
{
|
|
newColumns = tableColumnsCache[table];
|
|
}
|
|
else
|
|
{
|
|
newColumns = new List<string>();
|
|
// 從新資料庫取得欄位資訊
|
|
using (var command = new SqlCommand($@"
|
|
SELECT COLUMN_NAME
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_NAME = '{table}'", connection, transaction))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
newColumns.Add(reader.GetString(0));
|
|
}
|
|
}
|
|
}
|
|
// 緩存結果,以便後續使用
|
|
tableColumnsCache[table] = newColumns;
|
|
}
|
|
|
|
// 找出共同存在的欄位
|
|
var commonColumns = oldColumns.Intersect(newColumns).ToList();
|
|
LogMessage($"表格 {table} 找到 {commonColumns.Count} 個共同欄位");
|
|
|
|
if (commonColumns.Any())
|
|
{
|
|
// 檢查表格是否有 IDENTITY 欄位
|
|
bool hasIdentity = false;
|
|
using (var command = new SqlCommand($@"
|
|
SELECT COUNT(*)
|
|
FROM sys.columns c
|
|
INNER JOIN sys.tables t ON c.object_id = t.object_id
|
|
WHERE t.name = '{table}' AND c.is_identity = 1", connection, transaction))
|
|
{
|
|
hasIdentity = (int)command.ExecuteScalar() > 0;
|
|
}
|
|
|
|
if (hasIdentity)
|
|
{
|
|
LogMessage($"表格 {table} 有 IDENTITY 欄位,設定 IDENTITY_INSERT ON");
|
|
using (var command = new SqlCommand($"SET IDENTITY_INSERT {table} ON", connection, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
|
|
// 使用緩存的欄位資訊
|
|
var columnInfos = new Dictionary<string, (string DataType, int MaxLength, bool IsIncompatible)>();
|
|
|
|
// 從緩存中獲取欄位類型和長度信息
|
|
if (tableColumnInfoCache.ContainsKey(table))
|
|
{
|
|
var cachedColumnInfo = tableColumnInfoCache[table];
|
|
|
|
foreach (var kvp in cachedColumnInfo)
|
|
{
|
|
string columnName = kvp.Key;
|
|
string dataType = kvp.Value.DataType;
|
|
int maxLength = kvp.Value.MaxLength;
|
|
|
|
// 檢查此欄位是否在不相容欄位列表中
|
|
bool isIncompatible = incompatibleColumnTypes.Any(ic =>
|
|
ic.TableName.Equals(table, StringComparison.OrdinalIgnoreCase) &&
|
|
ic.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
columnInfos[columnName] = (dataType, maxLength, isIncompatible);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 如果沒有緩存,則仍然執行查詢
|
|
using (var command = new SqlCommand($@"
|
|
SELECT COLUMN_NAME, DATA_TYPE,
|
|
CASE
|
|
WHEN CHARACTER_MAXIMUM_LENGTH IS NULL THEN 0
|
|
ELSE CHARACTER_MAXIMUM_LENGTH
|
|
END AS CHARACTER_MAXIMUM_LENGTH
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_NAME = '{table}'", connection, transaction))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
string columnName = reader.GetString(0);
|
|
string dataType = reader.GetString(1);
|
|
int maxLength = reader.GetInt32(2);
|
|
|
|
// 檢查此欄位是否在不相容欄位列表中
|
|
bool isIncompatible = incompatibleColumnTypes.Any(ic =>
|
|
ic.TableName.Equals(table, StringComparison.OrdinalIgnoreCase) &&
|
|
ic.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
columnInfos[columnName] = (dataType, maxLength, isIncompatible);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 為每個欄位應用適當的轉換
|
|
var columnClauses = new List<string>();
|
|
var columnsToInclude = new List<string>();
|
|
|
|
foreach (var column in commonColumns)
|
|
{
|
|
// 跳過不在獲取的欄位資訊中的欄位
|
|
if (!columnInfos.ContainsKey(column))
|
|
continue;
|
|
|
|
var columnInfo = columnInfos[column];
|
|
|
|
if (columnInfo.IsIncompatible)
|
|
{
|
|
// 跳過不相容的欄位
|
|
LogMessage($"跳過表格 {table} 中不相容的欄位 {column} 的資料遷移");
|
|
continue;
|
|
}
|
|
|
|
// 記錄要包含的欄位
|
|
columnsToInclude.Add(column);
|
|
|
|
// 如果是字符類型,檢查是否需要截斷
|
|
if (columnInfo.DataType.Contains("char") && columnInfo.MaxLength > 0)
|
|
{
|
|
columnClauses.Add($"CASE WHEN LEN([{column}]) > {columnInfo.MaxLength} THEN LEFT([{column}], {columnInfo.MaxLength}) ELSE [{column}] END AS [{column}]");
|
|
}
|
|
else
|
|
{
|
|
columnClauses.Add($"[{column}]");
|
|
}
|
|
}
|
|
|
|
// 如果沒有相容的欄位,則跳過此表格
|
|
if (!columnsToInclude.Any())
|
|
{
|
|
LogMessage($"表格 {table} 沒有相容的欄位,跳過此表格的資料遷移");
|
|
continue;
|
|
}
|
|
|
|
string columns = string.Join(", ", columnsToInclude.Select(c => $"[{c}]"));
|
|
string selectColumns = string.Join(", ", columnClauses);
|
|
|
|
// 檢查是否有資料,避免對空表格執行複雜操作
|
|
int recordCount = 0;
|
|
using (var countCommand = new SqlCommand($"SELECT COUNT(*) FROM {backupDbName}.dbo.{table}", connection, transaction))
|
|
{
|
|
recordCount = Convert.ToInt32(countCommand.ExecuteScalar());
|
|
}
|
|
|
|
if (recordCount == 0)
|
|
{
|
|
LogMessage($"表格 {table} 無資料,跳過遷移");
|
|
continue;
|
|
}
|
|
|
|
LogMessage($"表格 {table} 有 {recordCount} 筆資料需要遷移");
|
|
|
|
// 檢查不允許 NULL 的欄位是否有 NULL 值
|
|
var notNullColumns = new List<string>();
|
|
using (var schemaCommand = new SqlCommand($@"
|
|
SELECT COLUMN_NAME
|
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_NAME = '{table}'
|
|
AND IS_NULLABLE = 'NO'
|
|
AND COLUMN_DEFAULT IS NULL", connection, transaction))
|
|
{
|
|
using (var reader = schemaCommand.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
notNullColumns.Add(reader.GetString(0));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (notNullColumns.Any())
|
|
{
|
|
foreach (var column in notNullColumns)
|
|
{
|
|
// 檢查來源資料是否有 NULL 值
|
|
using (var nullCheckCommand = new SqlCommand($@"
|
|
SELECT COUNT(*)
|
|
FROM {backupDbName}.dbo.{table}
|
|
WHERE [{column}] IS NULL", connection, transaction))
|
|
{
|
|
int nullCount = Convert.ToInt32(nullCheckCommand.ExecuteScalar());
|
|
if (nullCount > 0)
|
|
{
|
|
// 在 UI 線程上顯示確認對話框
|
|
bool skipNullRecords = false;
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
var message = $"表格 {table} 中的欄位 {column} 不允許 NULL 值,但在來源資料中發現 {nullCount} 筆 NULL 值記錄。\n\n" +
|
|
"是否要跳過這些記錄?\n" +
|
|
"- 「是」:跳過包含 NULL 值的記錄\n" +
|
|
"- 「否」:中止整個遷移過程";
|
|
|
|
LogMessage("【對話框】顯示 NULL 值處理確認對話框");
|
|
LogMessage(message);
|
|
|
|
var result = MessageBox.Show(
|
|
message,
|
|
"NULL 值處理確認 / NULL Value Handling Confirmation",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Warning);
|
|
|
|
skipNullRecords = (result == MessageBoxResult.Yes);
|
|
|
|
if (skipNullRecords)
|
|
{
|
|
LogMessage($"【用戶選擇】用戶選擇跳過 NULL 值記錄(是)");
|
|
}
|
|
else
|
|
{
|
|
LogMessage($"【用戶選擇】用戶選擇中止遷移(否)");
|
|
}
|
|
});
|
|
|
|
if (!skipNullRecords)
|
|
{
|
|
LogMessage("根據用戶選擇,中止資料遷移");
|
|
return false;
|
|
}
|
|
|
|
// 修改 SQL 查詢以排除 NULL 值記錄
|
|
selectColumns = $"SELECT {selectColumns} FROM {backupDbName}.dbo.{table} WHERE [{column}] IS NOT NULL";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 對大表使用批次處理以提高效率
|
|
const int batchSize = 5000; // 每批次處理的記錄數
|
|
|
|
if (recordCount > batchSize)
|
|
{
|
|
// 檢查表是否有主鍵或唯一索引可用於分頁
|
|
string keyColumn = null;
|
|
using (var keyCommand = new SqlCommand($@"
|
|
SELECT c.name
|
|
FROM {backupDbName}.sys.indexes i
|
|
JOIN {backupDbName}.sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
JOIN {backupDbName}.sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
JOIN {backupDbName}.sys.tables t ON i.object_id = t.object_id
|
|
WHERE i.is_primary_key = 1 OR i.is_unique = 1
|
|
AND t.name = '{table}'
|
|
AND ic.key_ordinal = 1", connection, transaction))
|
|
{
|
|
var keyObj = keyCommand.ExecuteScalar();
|
|
if (keyObj != null)
|
|
{
|
|
keyColumn = keyObj.ToString();
|
|
}
|
|
}
|
|
|
|
if (keyColumn != null && columnsToInclude.Contains(keyColumn))
|
|
{
|
|
LogMessage($"使用主鍵 {keyColumn} 進行批次處理");
|
|
|
|
// 獲取最小和最大值
|
|
int minKey = 0, maxKey = 0;
|
|
using (var minCommand = new SqlCommand($"SELECT MIN([{keyColumn}]) FROM {backupDbName}.dbo.{table}", connection, transaction))
|
|
{
|
|
var result = minCommand.ExecuteScalar();
|
|
if (result != null && result != DBNull.Value)
|
|
{
|
|
minKey = Convert.ToInt32(result);
|
|
}
|
|
}
|
|
|
|
using (var maxCommand = new SqlCommand($"SELECT MAX([{keyColumn}]) FROM {backupDbName}.dbo.{table}", connection, transaction))
|
|
{
|
|
var result = maxCommand.ExecuteScalar();
|
|
if (result != null && result != DBNull.Value)
|
|
{
|
|
maxKey = Convert.ToInt32(result);
|
|
}
|
|
}
|
|
|
|
int totalBatches = (int)Math.Ceiling((double)(maxKey - minKey + 1) / batchSize);
|
|
LogMessage($"將分 {totalBatches} 批處理");
|
|
|
|
for (int currentBatch = 0; currentBatch < totalBatches; currentBatch++)
|
|
{
|
|
int startKey = minKey + (currentBatch * batchSize);
|
|
int endKey = Math.Min(startKey + batchSize - 1, maxKey);
|
|
|
|
string batchInsertSql = $@"
|
|
INSERT INTO {table} ({columns})
|
|
SELECT {selectColumns}
|
|
FROM {backupDbName}.dbo.{table}
|
|
WHERE [{keyColumn}] BETWEEN {startKey} AND {endKey}";
|
|
|
|
using (var command = new SqlCommand(batchInsertSql, connection, transaction))
|
|
{
|
|
command.CommandTimeout = 60; // 每批次1分鐘超時
|
|
int affected = command.ExecuteNonQuery();
|
|
LogMessage($"批次 {currentBatch + 1}/{totalBatches} 完成,遷移 {affected} 筆資料");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 無法使用主鍵分頁,使用一般方式
|
|
string insertSql = $@"
|
|
INSERT INTO {table} ({columns})
|
|
SELECT {selectColumns}
|
|
FROM {backupDbName}.dbo.{table}";
|
|
|
|
using (var command = new SqlCommand(insertSql, connection, transaction))
|
|
{
|
|
command.CommandTimeout = 300; // 設定5分鐘超時
|
|
command.ExecuteNonQuery();
|
|
LogMessage($"表格 {table} 的資料遷移完成");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 小表直接遷移
|
|
string insertSql = $@"
|
|
INSERT INTO {table} ({columns})
|
|
SELECT {selectColumns}
|
|
FROM {backupDbName}.dbo.{table}";
|
|
|
|
using (var command = new SqlCommand(insertSql, connection, transaction))
|
|
{
|
|
command.CommandTimeout = 60; // 1分鐘超時足夠小表使用
|
|
command.ExecuteNonQuery();
|
|
LogMessage($"表格 {table} 的資料遷移完成");
|
|
}
|
|
}
|
|
|
|
if (hasIdentity)
|
|
{
|
|
LogMessage($"表格 {table} 資料遷移完成,設定 IDENTITY_INSERT OFF");
|
|
using (var command = new SqlCommand($"SET IDENTITY_INSERT {table} OFF", connection, transaction))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 處理TEXT類型欄位
|
|
foreach (var textColumn in textTypeColumns)
|
|
{
|
|
string table = textColumn.TableName;
|
|
string column = textColumn.ColumnName;
|
|
string newDataType = textColumn.NewType;
|
|
bool isCompatible = textColumn.IsCompatible;
|
|
|
|
if (isCompatible)
|
|
{
|
|
LogMessage($"處理TEXT類型欄位: 表格 {table}, 欄位 {column}, 新類型 {newDataType}");
|
|
|
|
// 檢查是否有資料,避免對空表格執行複雜操作
|
|
int recordCount = 0;
|
|
using (var countCommand = new SqlCommand($"SELECT COUNT(*) FROM {backupDbName}.dbo.{table}", connection, transaction))
|
|
{
|
|
recordCount = Convert.ToInt32(countCommand.ExecuteScalar());
|
|
}
|
|
|
|
if (recordCount == 0)
|
|
{
|
|
LogMessage($"表格 {table} 無資料,跳過遷移");
|
|
continue;
|
|
}
|
|
|
|
LogMessage($"表格 {table} 有 {recordCount} 筆資料需要遷移");
|
|
|
|
// 對大表使用批次處理以提高效率
|
|
const int batchSize = 5000; // 每批次處理的記錄數
|
|
|
|
if (recordCount > batchSize)
|
|
{
|
|
// 檢查表是否有主鍵或唯一索引可用於分頁
|
|
string keyColumn = null;
|
|
using (var keyCommand = new SqlCommand($@"
|
|
SELECT c.name
|
|
FROM {backupDbName}.sys.indexes i
|
|
JOIN {backupDbName}.sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
JOIN {backupDbName}.sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
JOIN {backupDbName}.sys.tables t ON i.object_id = t.object_id
|
|
WHERE i.is_primary_key = 1 OR i.is_unique = 1
|
|
AND t.name = '{table}'
|
|
AND ic.key_ordinal = 1", connection, transaction))
|
|
{
|
|
var keyObj = keyCommand.ExecuteScalar();
|
|
if (keyObj != null)
|
|
{
|
|
keyColumn = keyObj.ToString();
|
|
}
|
|
}
|
|
|
|
if (keyColumn != null)
|
|
{
|
|
LogMessage($"使用主鍵 {keyColumn} 進行批次處理");
|
|
|
|
// 獲取最小和最大值
|
|
int minKey = 0, maxKey = 0;
|
|
using (var minCommand = new SqlCommand($"SELECT MIN([{keyColumn}]) FROM {backupDbName}.dbo.{table}", connection, transaction))
|
|
{
|
|
var result = minCommand.ExecuteScalar();
|
|
if (result != null && result != DBNull.Value)
|
|
{
|
|
minKey = Convert.ToInt32(result);
|
|
}
|
|
}
|
|
|
|
using (var maxCommand = new SqlCommand($"SELECT MAX([{keyColumn}]) FROM {backupDbName}.dbo.{table}", connection, transaction))
|
|
{
|
|
var result = maxCommand.ExecuteScalar();
|
|
if (result != null && result != DBNull.Value)
|
|
{
|
|
maxKey = Convert.ToInt32(result);
|
|
}
|
|
}
|
|
|
|
int totalBatches = (int)Math.Ceiling((double)(maxKey - minKey + 1) / batchSize);
|
|
LogMessage($"將分 {totalBatches} 批處理");
|
|
|
|
for (int currentBatch = 0; currentBatch < totalBatches; currentBatch++)
|
|
{
|
|
int startKey = minKey + (currentBatch * batchSize);
|
|
int endKey = Math.Min(startKey + batchSize - 1, maxKey);
|
|
|
|
string batchUpdateSql = $@"
|
|
UPDATE {table}
|
|
SET {column} = LEFT({column}, {newDataType.Length})
|
|
WHERE [{keyColumn}] BETWEEN {startKey} AND {endKey}";
|
|
|
|
using (var command = new SqlCommand(batchUpdateSql, connection, transaction))
|
|
{
|
|
command.CommandTimeout = 60; // 每批次1分鐘超時
|
|
int affected = command.ExecuteNonQuery();
|
|
LogMessage($"批次 {currentBatch + 1}/{totalBatches} 完成,遷移 {affected} 筆資料");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 無法使用主鍵分頁,使用一般方式
|
|
string updateSql = $@"
|
|
UPDATE {table}
|
|
SET {column} = LEFT({column}, {newDataType.Length})";
|
|
|
|
using (var command = new SqlCommand(updateSql, connection, transaction))
|
|
{
|
|
command.CommandTimeout = 300; // 設定5分鐘超時
|
|
command.ExecuteNonQuery();
|
|
LogMessage($"表格 {table} 的資料遷移完成");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 小表直接遷移
|
|
string updateSql = $@"
|
|
UPDATE {table}
|
|
SET {column} = LEFT({column}, {newDataType.Length})";
|
|
|
|
using (var command = new SqlCommand(updateSql, connection, transaction))
|
|
{
|
|
command.CommandTimeout = 60; // 1分鐘超時足夠小表使用
|
|
command.ExecuteNonQuery();
|
|
LogMessage($"表格 {table} 的資料遷移完成");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogMessage($"跳過不相容的TEXT類型欄位: 表格 {table}, 欄位 {column}, 新類型 {newDataType}");
|
|
}
|
|
}
|
|
|
|
transaction.Commit();
|
|
LogMessage("所有表格資料遷移完成");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
transaction.Rollback();
|
|
throw new Exception($"資料遷移時發生錯誤: {ex.Message}", ex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"資料遷移失敗: {ex.Message}", true);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private void btn_Close_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 檢查是否有未保存的變更
|
|
if (!CheckUnsavedChanges())
|
|
{
|
|
return; // 用戶取消操作
|
|
}
|
|
|
|
// 關閉視窗
|
|
Close();
|
|
}
|
|
|
|
private void ShutdownApplicationOlc()
|
|
{
|
|
try
|
|
{
|
|
// 先關閉所有視窗
|
|
foreach (Window window in Application.Current.Windows)
|
|
{
|
|
window.Close();
|
|
}
|
|
|
|
// 確保所有背景執行緒都結束
|
|
System.Threading.Thread.Sleep(100); // 給一點時間讓視窗關閉
|
|
|
|
// 強制終止應用程式
|
|
Environment.Exit(0);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 如果正常關閉失敗,使用更強制的方式
|
|
Process.GetCurrentProcess().Kill();
|
|
}
|
|
}
|
|
private void ShutdownApplication()
|
|
{
|
|
try
|
|
{
|
|
// 檢查主視窗是否存在且可見
|
|
MainWindow mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
|
|
if (mainWindow != null && mainWindow.IsVisible)
|
|
{
|
|
// 如果主視窗存在且可見,則透過主視窗的關閉流程來關閉應用程式
|
|
mainWindow.Close();
|
|
return; // 讓主視窗的關閉事件處理程序來處理後續的關閉流程
|
|
}
|
|
else
|
|
{
|
|
// 如果主視窗不存在或不可見(例如系統啟動時檢查版本),則直接關閉
|
|
foreach (Window window in Application.Current.Windows)
|
|
{
|
|
window.Close();
|
|
}
|
|
|
|
// 確保所有背景執行緒都結束
|
|
System.Threading.Thread.Sleep(100);
|
|
|
|
// 強制終止應用程式
|
|
Environment.Exit(0);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 如果正常關閉失敗,使用更強制的方式
|
|
Process.GetCurrentProcess().Kill();
|
|
}
|
|
}
|
|
private async void btn_ClearBackup_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 檢查是否有未保存的變更
|
|
if (!CheckUnsavedChanges())
|
|
{
|
|
return; // 用戶取消操作
|
|
}
|
|
|
|
// 禁用所有按鈕
|
|
SetOperationButtonsState(false, "清除備份");
|
|
|
|
try
|
|
{
|
|
if (!ValidateDatabaseSettings())
|
|
{
|
|
// 在驗證失敗時重新啟用按鈕
|
|
SetOperationButtonsState(true);
|
|
return;
|
|
}
|
|
|
|
UpdateConnectionStrings();
|
|
ClearLog();
|
|
LogMessage("開始清除備份資料庫程序");
|
|
|
|
// 先確認是否要執行清除備份資料庫
|
|
var confirmResult = MessageBox.Show("確定要清除所有備份資料庫嗎?\n此操作無法復原。\n\nAre you sure you want to clear all backup databases?\nThis operation cannot be undone.", "確認操作 / Confirm Operation",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
|
|
|
if (confirmResult != MessageBoxResult.Yes)
|
|
{
|
|
LogMessage("使用者取消清除備份資料庫操作");
|
|
// 在用戶取消操作時重新啟用按鈕
|
|
SetOperationButtonsState(true);
|
|
return;
|
|
}
|
|
|
|
// 取得備份資料庫列表
|
|
List<string> backupDbList = new List<string>();
|
|
|
|
using (var connection = new SqlConnection(_masterConnectionString))
|
|
{
|
|
connection.Open();
|
|
using (var command = new SqlCommand("SELECT name FROM sys.databases WHERE name LIKE 'BC_Dev_%'", connection))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
backupDbList.Add(reader.GetString(0));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (backupDbList.Count == 0)
|
|
{
|
|
LogMessage("沒有找到任何備份資料庫");
|
|
MessageBox.Show("沒有找到任何備份資料庫可以清除\n\nNo backup databases found to clear", "資訊 / Information", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
// 在沒有找到備份資料庫時重新啟用按鈕
|
|
SetOperationButtonsState(true);
|
|
return;
|
|
}
|
|
|
|
LogMessage($"共找到 {backupDbList.Count} 個備份資料庫");
|
|
|
|
// 在背景執行緒中執行刪除操作
|
|
await Task.Run(() =>
|
|
{
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
LogMessage("開始清除備份資料庫...");
|
|
}), DispatcherPriority.Normal);
|
|
|
|
int successCount = 0;
|
|
int failCount = 0;
|
|
|
|
using (var connection = new SqlConnection(_masterConnectionString))
|
|
{
|
|
connection.Open();
|
|
foreach (var dbName in backupDbList)
|
|
{
|
|
try
|
|
{
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
LogMessage($"正在刪除資料庫 {dbName}...");
|
|
}), DispatcherPriority.Normal);
|
|
|
|
// 獲取資料庫檔案路徑
|
|
List<string> dbFiles = new List<string>();
|
|
using (var command = new SqlCommand($"SELECT physical_name FROM sys.master_files WHERE database_id = DB_ID('{dbName}')", connection))
|
|
{
|
|
using (var reader = command.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
dbFiles.Add(reader.GetString(0));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 刪除資料庫
|
|
using (var command = new SqlCommand($"ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{dbName}]", connection))
|
|
{
|
|
command.ExecuteNonQuery();
|
|
}
|
|
|
|
successCount++;
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
LogMessage($"成功刪除資料庫 {dbName}");
|
|
}), DispatcherPriority.Normal);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failCount++;
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
LogMessage($"刪除資料庫 {dbName} 時出錯: {ex.Message}", true);
|
|
}), DispatcherPriority.Normal);
|
|
}
|
|
}
|
|
}
|
|
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
LogMessage($"清除完成。成功: {successCount}, 失敗: {failCount}");
|
|
|
|
if (failCount > 0)
|
|
{
|
|
MessageBox.Show($"清除備份資料庫完成。\n成功: {successCount}, 失敗: {failCount}\n\n請查看操作記錄了解詳細資訊。\n\nBackup database cleanup completed.\nSuccess: {successCount}, Failed: {failCount}\n\nPlease check operation log for details.",
|
|
"部分完成 / Partially Completed", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show($"成功清除所有備份資料庫。\n共清除 {successCount} 個資料庫。\n\nAll backup databases have been successfully cleared.\nCleared {successCount} databases.",
|
|
"完成 / Completed", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
|
|
// 在操作完成後重新啟用按鈕
|
|
SetOperationButtonsState(true);
|
|
}), DispatcherPriority.Normal);
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"清除備份資料庫失敗: {ex.Message}", true);
|
|
MessageBox.Show($"清除備份資料庫失敗,請參閱操作記錄了解詳細資訊\n\nFailed to clear backup databases. Please refer to the operation log for detailed information.", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
|
|
// 確保在發生錯誤時也重新啟用按鈕
|
|
SetOperationButtonsState(true);
|
|
}
|
|
}
|
|
|
|
private void btn_SaveSettings_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 禁用所有按鈕
|
|
SetOperationButtonsState(false, "儲存設定");
|
|
|
|
try
|
|
{
|
|
LogMessage("開始儲存資料庫設定...");
|
|
|
|
// 驗證設定
|
|
if (!ValidateDatabaseSettings())
|
|
{
|
|
LogMessage("驗證失敗,取消儲存", true);
|
|
// 在驗證失敗時重新啟用按鈕
|
|
SetOperationButtonsState(true);
|
|
return;
|
|
}
|
|
|
|
// 確保IP和PORT有效
|
|
string ip = txtDbIP.Text.Trim();
|
|
string port = txtDbPort.Text.Trim();
|
|
|
|
if (string.IsNullOrEmpty(ip))
|
|
{
|
|
LogMessage("錯誤:資料庫伺服器 IP 不能為空", true);
|
|
MessageBox.Show("資料庫伺服器 IP 不能為空\n\nDatabase server IP cannot be empty", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
// 驗證IP格式
|
|
if (!ip.Equals("localhost", StringComparison.OrdinalIgnoreCase) && !IsValidIPv4(ip))
|
|
{
|
|
LogMessage("錯誤:資料庫伺服器 IP 格式無效", true);
|
|
MessageBox.Show("請輸入有效的IPv4地址(如: 192.168.1.1)或 localhost\n\nPlease enter a valid IPv4 address (e.g. 192.168.1.1) or localhost", "輸入錯誤 / Input Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
txtDbIP.Focus();
|
|
txtDbIP.SelectAll();
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(port))
|
|
{
|
|
LogMessage("錯誤:資料庫伺服器 Port 不能為空", true);
|
|
MessageBox.Show("資料庫伺服器 Port 不能為空\n\nDatabase server Port cannot be empty", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return;
|
|
}
|
|
|
|
// 建立連線字串
|
|
string dataSource = $"{ip},{port}";
|
|
var builder = new SqlConnectionStringBuilder
|
|
{
|
|
DataSource = dataSource,
|
|
InitialCatalog = txtDbName.Text, // 使用 InitialCatalog 設定資料庫名稱
|
|
UserID = txtDbID.Text,
|
|
Password = txtDbPassword.Text,
|
|
MinPoolSize = int.Parse(txtMinPoolSize.Text),
|
|
MaxPoolSize = int.Parse(txtMaxPoolSize.Text)
|
|
};
|
|
|
|
string connString = builder.ConnectionString;
|
|
LogMessage("連線字串已建立");
|
|
LogMessage($"- 資料庫伺服器:{ip}:{port}");
|
|
LogMessage($"- 資料庫名稱:{txtDbName.Text}");
|
|
LogMessage($"- 使用者帳號:{txtDbID.Text}");
|
|
LogMessage($"- 連線池設定:Min={txtMinPoolSize.Text}, Max={txtMaxPoolSize.Text}");
|
|
|
|
// 確保目錄存在
|
|
string configDir = IOPath.GetDirectoryName(_configPath);
|
|
if (!Directory.Exists(configDir))
|
|
{
|
|
Directory.CreateDirectory(configDir);
|
|
LogMessage($"建立設定檔目錄:{configDir}");
|
|
}
|
|
|
|
// 讀取現有的 XML 檔案
|
|
XDocument doc;
|
|
if (IOFile.Exists(_configPath))
|
|
{
|
|
doc = XDocument.Load(_configPath);
|
|
LogMessage("讀取現有設定檔");
|
|
}
|
|
else
|
|
{
|
|
// 建立新的 XML 文件
|
|
doc = new XDocument(
|
|
new XElement("hibernate-configuration",
|
|
new XElement("session-factory")
|
|
)
|
|
);
|
|
LogMessage("建立新的設定檔");
|
|
}
|
|
|
|
// 取得命名空間
|
|
XNamespace ns = doc.Root.GetDefaultNamespace();
|
|
|
|
// 尋找或建立 session-factory 元素
|
|
var sessionFactory = doc.Root.Element(ns + "session-factory");
|
|
if (sessionFactory == null)
|
|
{
|
|
sessionFactory = new XElement(ns + "session-factory");
|
|
doc.Root.Add(sessionFactory);
|
|
}
|
|
|
|
// 尋找或建立 connection.connection_string 屬性
|
|
var propertyElement = sessionFactory.Elements(ns + "property")
|
|
.FirstOrDefault(p => p.Attribute("name")?.Value == "connection.connection_string");
|
|
|
|
if (propertyElement == null)
|
|
{
|
|
propertyElement = new XElement(ns + "property",
|
|
new XAttribute("name", "connection.connection_string")
|
|
);
|
|
sessionFactory.Add(propertyElement);
|
|
}
|
|
|
|
// 更新連線字串值
|
|
propertyElement.Value = connString;
|
|
|
|
// 儲存檔案
|
|
doc.Save(_configPath);
|
|
|
|
LogMessage("自動保存了界面設定變更");
|
|
_hasUnsavedChanges = false;
|
|
|
|
MessageBox.Show("資料庫設定已儲存\n\nDatabase settings have been saved", "成功 / Success", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
|
|
// 在操作完成後重新啟用按鈕
|
|
SetOperationButtonsState(true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"儲存資料庫設定時發生錯誤:{ex.Message}", true);
|
|
LogMessage($"錯誤詳細資訊:{ex.StackTrace}", true);
|
|
MessageBox.Show($"儲存資料庫設定時發生錯誤,請參閱操作記錄了解詳細資訊\n\nError occurred while saving database settings. Please refer to the operation log for detailed information.", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
|
|
// 確保在發生錯誤時也重新啟用按鈕
|
|
SetOperationButtonsState(true);
|
|
}
|
|
}
|
|
|
|
private async void btn_BackupDatabase_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 檢查是否有未保存的變更
|
|
if (!CheckUnsavedChanges())
|
|
{
|
|
return; // 用戶取消操作
|
|
}
|
|
|
|
// 禁用所有按鈕
|
|
SetOperationButtonsState(false, "備份資料庫");
|
|
|
|
try
|
|
{
|
|
// 暫停 TraceDataReport Timer
|
|
PauseTraceDataReportTimer();
|
|
|
|
LogMessage("開始備份資料庫操作");
|
|
|
|
// 確認是否要執行備份
|
|
var result = MessageBox.Show(
|
|
"確定要備份資料庫嗎?\n此操作會將目前的BC_Dev資料庫備份為一個新的資料庫。\n\nAre you sure you want to backup the database?\nThis operation will backup the current BC_Dev database as a new database.",
|
|
"確認操作 / Confirm Operation",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Question);
|
|
|
|
if (result != MessageBoxResult.Yes)
|
|
{
|
|
LogMessage("使用者取消備份操作");
|
|
return;
|
|
}
|
|
|
|
// 執行備份操作 - 使用共用的BackupDatabase方法
|
|
var backupResult = await Task.Run(() =>
|
|
{
|
|
// 設置當前執行緒名稱,用於標識是單獨備份操作
|
|
System.Threading.Thread.CurrentThread.Name = "StandaloneBackupTask";
|
|
_currentOperation = "備份資料庫";
|
|
return BackupDatabase();
|
|
});
|
|
|
|
// 檢查備份結果
|
|
if (backupResult.DatabaseName == null)
|
|
{
|
|
// 直接顯示具體的錯誤原因,不再重複記錄
|
|
MessageBox.Show(
|
|
backupResult.ErrorMessage,
|
|
"提示",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
// 獲取成功的備份資料庫名稱
|
|
string backupDbName = backupResult.DatabaseName;
|
|
|
|
// 備份成功
|
|
LogMessage($"資料庫備份完成:{backupDbName}");
|
|
MessageBox.Show(
|
|
$"資料庫備份已完成。\n備份資料庫名稱:{backupDbName}\n\nDatabase backup completed.\nBackup database name: {backupDbName}",
|
|
"備份完成 / Backup Completed",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"備份資料庫時發生錯誤: {ex.Message}", true);
|
|
LogMessage($"錯誤詳細資訊:{ex.StackTrace}", true);
|
|
MessageBox.Show(
|
|
$"備份資料庫時發生錯誤,請參閱操作記錄了解詳細資訊。\n\nError occurred while backing up the database. Please refer to the operation log for detailed information.",
|
|
"錯誤 / Error",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
}
|
|
finally
|
|
{
|
|
// 無論執行結果如何,確保按鈕狀態都會被恢復
|
|
SetOperationButtonsState(true);
|
|
// 恢復 TraceDataReport Timer
|
|
ResumeTraceDataReportTimer();
|
|
}
|
|
}
|
|
|
|
private async void btn_RestoreDatabase_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 檢查是否有未保存的變更
|
|
if (!CheckUnsavedChanges())
|
|
{
|
|
return; // 用戶取消操作
|
|
}
|
|
|
|
// 禁用所有按鈕
|
|
SetOperationButtonsState(false, "還原資料庫");
|
|
|
|
try
|
|
{
|
|
// 暫停 TraceDataReport Timer
|
|
PauseTraceDataReportTimer();
|
|
|
|
LogMessage("準備還原資料庫...");
|
|
|
|
// 取得當前程式版本
|
|
string currentVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
|
|
LogMessage($"當前程式版本: {currentVersion}");
|
|
|
|
// 取得最小需求版本號
|
|
string minimumRequiredVersion = ((App)Application.Current).GetMinimumRequiredVersion();
|
|
LogMessage($"最小需求版本: {minimumRequiredVersion}");
|
|
|
|
// 使用SQL查詢獲取所有備份資料庫
|
|
List<RestorableDatabaseInfo> backupDatabases = new List<RestorableDatabaseInfo>();
|
|
using (var connection = new SqlConnection(_masterConnectionString))
|
|
{
|
|
await connection.OpenAsync();
|
|
|
|
// 先取得所有備份資料庫名稱
|
|
List<string> allBackupDatabases = new List<string>();
|
|
// 查詢資料庫名稱和創建時間
|
|
using (var command = new SqlCommand(@"
|
|
SELECT name, create_date
|
|
FROM sys.databases
|
|
WHERE name LIKE 'BC_Dev_%'
|
|
ORDER BY create_date DESC", connection))
|
|
{
|
|
using (var reader = await command.ExecuteReaderAsync())
|
|
{
|
|
while (await reader.ReadAsync())
|
|
{
|
|
string dbName = reader.GetString(0);
|
|
DateTime createDate = reader.GetDateTime(1);
|
|
|
|
// 創建包含創建時間的資料庫信息對象
|
|
var dbInfo = new RestorableDatabaseInfo
|
|
{
|
|
DatabaseName = dbName,
|
|
CreationTime = createDate,
|
|
Version = "未知" // 預設為未知,後續會更新
|
|
};
|
|
|
|
allBackupDatabases.Add(dbName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 檢查每個資料庫的版本
|
|
foreach (string dbName in allBackupDatabases)
|
|
{
|
|
try
|
|
{
|
|
LogMessage($"檢查資料庫 {dbName} 的版本資訊...");
|
|
|
|
// 檢查資料庫中是否有版本資訊表和欄位
|
|
bool hasVersionInfo = false;
|
|
using (var tableCommand = new SqlCommand($@"
|
|
SELECT COUNT(*)
|
|
FROM [{dbName}].INFORMATION_SCHEMA.TABLES t
|
|
INNER JOIN [{dbName}].INFORMATION_SCHEMA.COLUMNS c
|
|
ON t.TABLE_NAME = c.TABLE_NAME
|
|
WHERE t.TABLE_NAME = 'Version'
|
|
AND t.TABLE_TYPE = 'BASE TABLE'
|
|
AND c.COLUMN_NAME = 'Version'", connection))
|
|
{
|
|
var count = (int)await tableCommand.ExecuteScalarAsync();
|
|
hasVersionInfo = count > 0;
|
|
LogMessage($"資料庫 {dbName} 版本資訊檢查結果: {count} 個符合條件的表");
|
|
}
|
|
|
|
if (!hasVersionInfo)
|
|
{
|
|
// 診斷資訊:列出所有表
|
|
using (var tableCommand = new SqlCommand($@"
|
|
SELECT TABLE_NAME
|
|
FROM [{dbName}].INFORMATION_SCHEMA.TABLES
|
|
WHERE TABLE_TYPE = 'BASE TABLE'", connection))
|
|
{
|
|
using (var reader = await tableCommand.ExecuteReaderAsync())
|
|
{
|
|
var tables = new List<string>();
|
|
while (await reader.ReadAsync())
|
|
{
|
|
tables.Add(reader.GetString(0));
|
|
}
|
|
LogMessage($"資料庫 {dbName} 中的所有表: {string.Join(", ", tables)}");
|
|
}
|
|
}
|
|
|
|
// 診斷資訊:檢查 Version 表
|
|
using (var tableCommand = new SqlCommand($@"
|
|
SELECT COLUMN_NAME
|
|
FROM [{dbName}].INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_NAME = 'Version'", connection))
|
|
{
|
|
using (var reader = await tableCommand.ExecuteReaderAsync())
|
|
{
|
|
var columns = new List<string>();
|
|
while (await reader.ReadAsync())
|
|
{
|
|
columns.Add(reader.GetString(0));
|
|
}
|
|
if (columns.Count > 0)
|
|
{
|
|
LogMessage($"資料庫 {dbName} 的 Version 表欄位: {string.Join(", ", columns)}");
|
|
}
|
|
}
|
|
}
|
|
|
|
LogMessage($"備份資料庫 {dbName} 找不到版本資訊表或 Version 欄位");
|
|
continue;
|
|
}
|
|
|
|
// 檢查版本資訊
|
|
using (var versionCommand = new SqlCommand($@"
|
|
USE [{dbName}];
|
|
SELECT Version FROM Version", connection))
|
|
{
|
|
var dbVersion = await versionCommand.ExecuteScalarAsync();
|
|
|
|
// 修改版本檢查條件:從完全相同改為符合最小需求版本
|
|
if (dbVersion != null)
|
|
{
|
|
try
|
|
{
|
|
// 比較版本
|
|
Version dbVer = new Version(dbVersion.ToString());
|
|
Version minRequiredVer = new Version(minimumRequiredVersion);
|
|
Version currentVer = new Version(currentVersion);
|
|
|
|
// 創建資料庫信息對象
|
|
var dbInfo = new RestorableDatabaseInfo
|
|
{
|
|
DatabaseName = dbName,
|
|
Version = dbVersion.ToString(),
|
|
CreationTime = DateTime.MinValue // 預設值,後續會更新
|
|
};
|
|
|
|
// 獲取資料庫創建時間
|
|
using (var createDateCommand = new SqlCommand($@"
|
|
SELECT create_date
|
|
FROM sys.databases
|
|
WHERE name = '{dbName}'", connection))
|
|
{
|
|
var createDateObj = await createDateCommand.ExecuteScalarAsync();
|
|
if (createDateObj != null && createDateObj != DBNull.Value)
|
|
{
|
|
dbInfo.CreationTime = (DateTime)createDateObj;
|
|
}
|
|
}
|
|
|
|
// 如果資料庫版本大於等於最小需求版本且小於等於當前系統版本,則允許還原
|
|
if (dbVer >= minRequiredVer && dbVer <= currentVer)
|
|
{
|
|
backupDatabases.Add(dbInfo);
|
|
LogMessage($"資料庫 {dbName} 版本符合要求: {dbVersion} (最小需求: {minimumRequiredVersion}, 當前系統: {currentVersion})");
|
|
}
|
|
else if (dbVer < minRequiredVer)
|
|
{
|
|
LogMessage($"備份資料庫 {dbName} 版本低於最小需求版本 (資料庫版本: {dbVersion}, 最小需求版本: {minimumRequiredVersion})");
|
|
}
|
|
else if (dbVer > currentVer)
|
|
{
|
|
LogMessage($"備份資料庫 {dbName} 版本高於當前系統版本 (資料庫版本: {dbVersion}, 系統版本: {currentVersion})");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"解析資料庫 {dbName} 版本時發生錯誤: {ex.Message}", true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogMessage($"備份資料庫 {dbName} 版本資訊為空");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"檢查資料庫 {dbName} 版本時發生錯誤: {ex.Message}", true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (backupDatabases.Count == 0)
|
|
{
|
|
MessageBox.Show(
|
|
"找不到符合版本要求的備份資料庫。\n" +
|
|
$"版本要求:大於等於最小需求版本 ({minimumRequiredVersion})\n" +
|
|
$"且小於等於當前系統版本 ({currentVersion})\n" +
|
|
"請確認是否有正確的備份資料庫。\n\n" +
|
|
"No backup database meeting version requirements found.\n" +
|
|
$"Version requirement: Greater than or equal to minimum required version ({minimumRequiredVersion}) \n" +
|
|
$"and less than or equal to current system version ({currentVersion})\n" +
|
|
"Please verify if you have the correct backup database.",
|
|
"找不到備份 / No Backup Found",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Information);
|
|
LogMessage("找不到任何符合版本要求的備份資料庫");
|
|
return;
|
|
}
|
|
|
|
// 建立選擇視窗
|
|
var selectWindow = new Window
|
|
{
|
|
Title = "選擇要還原的備份資料庫 (顯示版本和建立時間)",
|
|
Width = 500,
|
|
Height = 400,
|
|
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
|
ResizeMode = ResizeMode.NoResize
|
|
};
|
|
|
|
var grid = new Grid();
|
|
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
|
|
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(70) });
|
|
|
|
var listBox = new ListBox
|
|
{
|
|
Margin = new Thickness(10),
|
|
ItemsSource = backupDatabases,
|
|
FontSize = 12
|
|
};
|
|
Grid.SetRow(listBox, 0);
|
|
|
|
// 自定義ListBox項的顯示模板
|
|
DataTemplate itemTemplate = new DataTemplate();
|
|
var stackPanel = new FrameworkElementFactory(typeof(StackPanel));
|
|
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Vertical);
|
|
stackPanel.SetValue(StackPanel.MarginProperty, new Thickness(5));
|
|
|
|
var dbNameTextBlock = new FrameworkElementFactory(typeof(TextBlock));
|
|
dbNameTextBlock.SetBinding(TextBlock.TextProperty, new Binding("DatabaseName"));
|
|
dbNameTextBlock.SetValue(TextBlock.FontWeightProperty, FontWeights.Bold);
|
|
dbNameTextBlock.SetValue(TextBlock.FontSizeProperty, 14.0);
|
|
stackPanel.AppendChild(dbNameTextBlock);
|
|
|
|
var versionTextBlock = new FrameworkElementFactory(typeof(TextBlock));
|
|
versionTextBlock.SetBinding(TextBlock.TextProperty, new Binding("Version") { StringFormat = "版本: {0}" });
|
|
stackPanel.AppendChild(versionTextBlock);
|
|
|
|
var creationTimeTextBlock = new FrameworkElementFactory(typeof(TextBlock));
|
|
creationTimeTextBlock.SetBinding(TextBlock.TextProperty, new Binding("CreationTime") { StringFormat = "建立時間: {0:yyyy-MM-dd HH:mm:ss}" });
|
|
stackPanel.AppendChild(creationTimeTextBlock);
|
|
|
|
itemTemplate.VisualTree = stackPanel;
|
|
listBox.ItemTemplate = itemTemplate;
|
|
|
|
var buttonPanel = new StackPanel
|
|
{
|
|
Orientation = Orientation.Horizontal,
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
Margin = new Thickness(10)
|
|
};
|
|
Grid.SetRow(buttonPanel, 1);
|
|
|
|
var okButton = new Button
|
|
{
|
|
Content = "確定",
|
|
Width = 80,
|
|
Height = 30,
|
|
Margin = new Thickness(5),
|
|
IsEnabled = false
|
|
};
|
|
|
|
var cancelButton = new Button
|
|
{
|
|
Content = "取消",
|
|
Width = 80,
|
|
Height = 30,
|
|
Margin = new Thickness(5)
|
|
};
|
|
|
|
buttonPanel.Children.Add(okButton);
|
|
buttonPanel.Children.Add(cancelButton);
|
|
|
|
grid.Children.Add(listBox);
|
|
grid.Children.Add(buttonPanel);
|
|
|
|
selectWindow.Content = grid;
|
|
|
|
listBox.SelectionChanged += (s, args) =>
|
|
{
|
|
okButton.IsEnabled = listBox.SelectedItem != null;
|
|
};
|
|
|
|
okButton.Click += (s, args) =>
|
|
{
|
|
selectWindow.DialogResult = true;
|
|
selectWindow.Close();
|
|
};
|
|
|
|
cancelButton.Click += (s, args) =>
|
|
{
|
|
selectWindow.DialogResult = false;
|
|
selectWindow.Close();
|
|
};
|
|
|
|
if (selectWindow.ShowDialog() == true && listBox.SelectedItem != null)
|
|
{
|
|
var selectedDb = (RestorableDatabaseInfo)listBox.SelectedItem;
|
|
string selectedDatabase = selectedDb.DatabaseName;
|
|
|
|
// 確認是否要覆蓋現有資料庫
|
|
var result = MessageBox.Show(
|
|
$"還原資料庫將會覆蓋現有的 BC_Dev 資料庫。\n" +
|
|
$"選擇的備份資料庫:{selectedDatabase}\n" +
|
|
$"版本:{selectedDb.Version}\n" +
|
|
$"建立時間:{selectedDb.CreationTime:yyyy-MM-dd HH:mm:ss}\n\n" +
|
|
$"註:系統只允許還原符合以下版本要求的資料庫:\n" +
|
|
$" 大於等於最小需求版本 ({((App)Application.Current).GetMinimumRequiredVersion()}) 且\n" +
|
|
$" 小於等於當前系統版本 ({Assembly.GetExecutingAssembly().GetName().Version})\n\n" +
|
|
$"是否確定要繼續?\n\n" +
|
|
$"Restoring database will overwrite existing BC_Dev database.\n" +
|
|
$"Selected backup database: {selectedDatabase}\n" +
|
|
$"Version: {selectedDb.Version}\n" +
|
|
$"Creation time: {selectedDb.CreationTime:yyyy-MM-dd HH:mm:ss}\n\n" +
|
|
$"Note: System only allows restoring databases that meet the following version requirements:\n" +
|
|
$" Greater than or equal to minimum required version ({((App)Application.Current).GetMinimumRequiredVersion()}) and\n" +
|
|
$" Less than or equal to current system version ({Assembly.GetExecutingAssembly().GetName().Version})\n\n" +
|
|
$"Are you sure to continue?",
|
|
"確認 / Confirm",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Warning);
|
|
|
|
if (result == MessageBoxResult.Yes)
|
|
{
|
|
LogMessage($"開始還原資料庫 {selectedDatabase}...");
|
|
|
|
// 檢查ACJID和APJID表是否有資料
|
|
if (!CheckAccountTables())
|
|
{
|
|
LogMessage("使用者因資料表風險取消操作");
|
|
return;
|
|
}
|
|
// 使用進度報告和取消令牌
|
|
var progressIndicator = new Progress<string>(message =>
|
|
{
|
|
LogMessage(message);
|
|
// 在長時間運行的操作中使用 DoEvents 讓 UI 有機會更新
|
|
DoEvents();
|
|
});
|
|
|
|
// 使用 CancellationTokenSource 支持操作取消
|
|
var cts = new CancellationTokenSource();
|
|
|
|
try
|
|
{
|
|
// 使用 Task.Run 執行資料庫還原,以防止 UI 凍結
|
|
await Task.Run(async () =>
|
|
{
|
|
// 執行還原操作前報告進度
|
|
LogMessage("執行資料庫還原操作...");
|
|
await RestoreDatabase(selectedDatabase);
|
|
// 還原完成後直接使用 LogMessage,而不是嘗試使用 progressIndicator
|
|
LogMessage("資料庫還原完成");
|
|
}, cts.Token);
|
|
|
|
// 等待資料庫就緒
|
|
LogMessage("等待資料庫就緒...");
|
|
if (!await WaitForDatabaseConnection(_connectionString, "BC_Dev", 30, 1000, 10000))
|
|
{
|
|
throw new Exception("無法連接到還原後的資料庫,請檢查資料庫狀態");
|
|
}
|
|
|
|
// 還原成功後檢查欄位一致性
|
|
bool hasInconsistentFields = false;
|
|
bool checkFailed = false;
|
|
string errorMessage = string.Empty;
|
|
|
|
try
|
|
{
|
|
LogMessage("還原資料庫成功,等待資料庫就緒...");
|
|
|
|
// 最多重試 5 次,每次等待 1 秒
|
|
const int maxRetries = 5;
|
|
const int retryDelayMs = 1000;
|
|
bool checkSuccess = false;
|
|
|
|
for (int retry = 0; retry < maxRetries; retry++)
|
|
{
|
|
try
|
|
{
|
|
// 先嘗試建立連線測試資料庫是否可用
|
|
using (var conn = new SqlConnection(_connectionString))
|
|
{
|
|
await conn.OpenAsync();
|
|
// 如果能成功連線,則執行一致性檢查
|
|
LogMessage("資料庫連線成功,開始檢查欄位定義一致性...");
|
|
hasInconsistentFields = CheckFieldConsistency(false);
|
|
checkSuccess = true;
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception retryEx)
|
|
{
|
|
if (retry < maxRetries - 1)
|
|
{
|
|
LogMessage($"等待資料庫就緒中... (重試 {retry + 1}/{maxRetries})", true);
|
|
await Task.Delay(retryDelayMs);
|
|
}
|
|
else
|
|
{
|
|
throw; // 最後一次重試失敗,拋出例外
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!checkSuccess)
|
|
{
|
|
throw new Exception("資料庫連線重試次數已達上限,無法完成一致性檢查");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
checkFailed = true;
|
|
errorMessage = ex.Message;
|
|
LogMessage($"還原後檢查欄位一致性時發生錯誤: {ex.Message}", true);
|
|
}
|
|
|
|
// 根據檢查結果顯示不同的訊息
|
|
string notificationMessage;
|
|
MessageBoxImage notificationIcon;
|
|
string notificationTitle;
|
|
|
|
if (checkFailed)
|
|
{
|
|
notificationMessage = $"資料庫還原完成,但欄位一致性檢查失敗:\n{errorMessage}\n\n" +
|
|
$"建議您重新啟動程式後,再次執行一致性檢查。\n\n" +
|
|
$"系統將重新啟動以套用變更。\n\n" +
|
|
$"Database restore completed, but field consistency check failed:\n{errorMessage}\n\n" +
|
|
$"It is recommended to restart the program and perform the consistency check again.\n\n" +
|
|
$"The system will restart to apply changes.";
|
|
notificationIcon = MessageBoxImage.Warning;
|
|
notificationTitle = "還原完成但有錯誤 / Restore Completed with Errors";
|
|
}
|
|
else if (hasInconsistentFields)
|
|
{
|
|
notificationMessage = $"資料庫還原完成,但發現欄位定義不一致的情況,詳情請查看日誌報告。\n\n" +
|
|
$"系統將重新啟動以套用變更。\n\n" +
|
|
$"Database restore completed, but field definition inconsistencies were found. Please check the log report for details.\n\n" +
|
|
$"The system will restart to apply changes.";
|
|
notificationIcon = MessageBoxImage.Warning;
|
|
notificationTitle = "還原完成但有警告 / Restore Completed with Warnings";
|
|
}
|
|
else
|
|
{
|
|
notificationMessage = $"資料庫還原成功完成!\n\n" +
|
|
$"系統將重新啟動以套用變更。\n\n" +
|
|
$"Database restore completed successfully!\n\n" +
|
|
$"The system will restart to apply changes.";
|
|
notificationIcon = MessageBoxImage.Information;
|
|
notificationTitle = "還原成功 / Restore Successful";
|
|
}
|
|
|
|
MessageBox.Show(
|
|
notificationMessage,
|
|
notificationTitle,
|
|
MessageBoxButton.OK,
|
|
notificationIcon,
|
|
MessageBoxResult.OK,
|
|
MessageBoxOptions.DefaultDesktopOnly);
|
|
|
|
ShutdownApplication();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 記錄錯誤
|
|
LogMessage($"還原資料庫時發生錯誤: {ex.Message}", true);
|
|
LogMessage($"錯誤詳細資訊: {ex.StackTrace}", true);
|
|
|
|
// 顯示錯誤訊息給使用者
|
|
MessageBox.Show(
|
|
$"還原資料庫時發生錯誤。\n請查看操作記錄了解詳細資訊。\n\nError occurred while restoring the database. Please refer to the operation log for detailed information.",
|
|
"錯誤 / Error",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 用戶取消覆蓋操作
|
|
LogMessage("使用者取消還原資料庫操作");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 用戶取消選擇資料庫操作
|
|
LogMessage("使用者取消選擇備份資料庫");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"還原資料庫時發生錯誤: {ex.Message}", true);
|
|
MessageBox.Show(
|
|
$"還原資料庫時發生錯誤,請參閱操作記錄了解詳細資訊。\n\nError occurred while restoring the database. Please refer to the operation log for detailed information.",
|
|
"錯誤 / Error",
|
|
MessageBoxButton.OK,
|
|
MessageBoxImage.Error);
|
|
}
|
|
finally
|
|
{
|
|
// 無論執行結果如何,確保按鈕狀態都會被恢復
|
|
SetOperationButtonsState(true);
|
|
// 恢復 TraceDataReport Timer
|
|
ResumeTraceDataReportTimer();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 處理清除日誌按鈕點擊事件
|
|
/// </summary>
|
|
private void MenuItem_ClearLog_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 詢問用戶是否確定要清除日誌
|
|
if (MessageBox.Show("確定要清除所有日誌記錄嗎?\n\nAre you sure you want to clear all log records?", "確認清除 / Confirm Clear", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
|
{
|
|
ClearLog();
|
|
LogMessage("日誌已被手動清除");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 處理複製命令
|
|
/// </summary>
|
|
private void CommandBinding_Copy_Executed(object sender, ExecutedRoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
TextBox textBox = sender as TextBox;
|
|
if (textBox != null)
|
|
{
|
|
// 檢查是否有選取文字
|
|
if (textBox.SelectionLength > 0)
|
|
{
|
|
// 只複製選取的部分
|
|
string selectedText = textBox.SelectedText;
|
|
Clipboard.SetText(selectedText);
|
|
}
|
|
else
|
|
{
|
|
// 如果沒有選取文字,則不執行任何動作
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("複製時發生錯誤: " + ex.Message, "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private void CommandBinding_SelectAll_Executed(object sender, ExecutedRoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
TextBox textBox = sender as TextBox;
|
|
if (textBox != null)
|
|
{
|
|
// 執行全選前先確認使用者是否真的需要全選
|
|
if (MessageBox.Show("是否要全選所有日誌內容?\n\nAre you sure you want to select all log content?", "確認全選 / Confirm Select",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
|
{
|
|
textBox.SelectAll();
|
|
}
|
|
else
|
|
{
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("全選時發生錯誤: " + ex.Message + "\n\nError occurred while selecting all: " + ex.Message, "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private void TxtLog_PreviewKeyUp(object sender, KeyEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
TextBox textBox = sender as TextBox;
|
|
if (textBox != null)
|
|
{
|
|
// 處理Ctrl+C
|
|
if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
|
|
{
|
|
if (textBox.SelectionLength > 0)
|
|
{
|
|
string selectedText = textBox.SelectedText;
|
|
Clipboard.SetText(selectedText);
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
// 處理Ctrl+A
|
|
else if (e.Key == Key.A && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
|
|
{
|
|
if (MessageBox.Show("是否要全選所有日誌內容?\n\nAre you sure you want to select all log content?", "確認全選 / Confirm Select",
|
|
MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
|
|
{
|
|
textBox.SelectAll();
|
|
}
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("按鍵處理時發生錯誤: " + ex.Message + "\n\nError occurred while processing key: " + ex.Message, "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
// 新增一個方法,用於更新控件值發生變更時設置標記
|
|
private void MarkAsChanged(object sender, EventArgs e)
|
|
{
|
|
if (!_isOperationInProgress) // 只有在沒有操作進行時才標記為變更
|
|
{
|
|
_hasUnsavedChanges = true;
|
|
// 移除此處的日誌記錄,避免每次輸入字符都產生日誌
|
|
// LogMessage("界面設定已變更但尚未保存");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 檢查是否有未保存的變更並詢問用戶如何處理
|
|
/// </summary>
|
|
/// <returns>
|
|
/// true: 繼續操作 (保存變更或用戶確認使用舊值)
|
|
/// false: 取消操作
|
|
/// </returns>
|
|
private bool CheckUnsavedChanges()
|
|
{
|
|
if (!_hasUnsavedChanges)
|
|
return true; // 沒有未保存的變更,繼續操作
|
|
|
|
var result = MessageBox.Show(
|
|
"您有未保存的數據庫設定變更。\n\n" +
|
|
"是否保存這些變更後再繼續?\n" +
|
|
"「是」 - 保存變更後繼續\n" +
|
|
"「否」 - 使用原設定值繼續\n" +
|
|
"「取消」 - 終止操作\n\n" +
|
|
"You have unsaved database setting changes.\n\n" +
|
|
"Do you want to save these changes before continuing?\n" +
|
|
"'Yes' - Save changes and continue\n" +
|
|
"'No' - Continue with original settings\n" +
|
|
"'Cancel' - Abort operation",
|
|
"未保存的變更 / Unsaved Changes",
|
|
MessageBoxButton.YesNoCancel,
|
|
MessageBoxImage.Question);
|
|
|
|
switch (result)
|
|
{
|
|
case MessageBoxResult.Yes:
|
|
// 保存變更
|
|
try
|
|
{
|
|
if (!ValidateDatabaseSettings())
|
|
return false; // 驗證失敗,不繼續操作
|
|
|
|
// 更新連線字串
|
|
UpdateConnectionStrings();
|
|
|
|
// 執行保存設定操作
|
|
string ip = txtDbIP.Text.Trim();
|
|
string port = txtDbPort.Text.Trim();
|
|
string dataSource = $"{ip},{port}";
|
|
var builder = new SqlConnectionStringBuilder
|
|
{
|
|
DataSource = dataSource,
|
|
InitialCatalog = txtDbName.Text,
|
|
UserID = txtDbID.Text,
|
|
Password = txtDbPassword.Text,
|
|
MinPoolSize = int.Parse(txtMinPoolSize.Text),
|
|
MaxPoolSize = int.Parse(txtMaxPoolSize.Text)
|
|
};
|
|
|
|
string connString = builder.ConnectionString;
|
|
|
|
// 確保目錄存在
|
|
string configDir = IOPath.GetDirectoryName(_configPath);
|
|
if (!Directory.Exists(configDir))
|
|
{
|
|
Directory.CreateDirectory(configDir);
|
|
}
|
|
|
|
// 讀取現有的 XML 檔案
|
|
XDocument doc;
|
|
if (IOFile.Exists(_configPath))
|
|
{
|
|
doc = XDocument.Load(_configPath);
|
|
}
|
|
else
|
|
{
|
|
// 建立新的 XML 文件
|
|
doc = new XDocument(
|
|
new XElement("hibernate-configuration",
|
|
new XElement("session-factory")
|
|
)
|
|
);
|
|
}
|
|
|
|
// 取得命名空間
|
|
XNamespace ns = doc.Root.GetDefaultNamespace();
|
|
|
|
// 尋找或建立 session-factory 元素
|
|
var sessionFactory = doc.Root.Element(ns + "session-factory");
|
|
if (sessionFactory == null)
|
|
{
|
|
sessionFactory = new XElement(ns + "session-factory");
|
|
doc.Root.Add(sessionFactory);
|
|
}
|
|
|
|
// 尋找或建立 connection.connection_string 屬性
|
|
var propertyElement = sessionFactory.Elements(ns + "property")
|
|
.FirstOrDefault(p => p.Attribute("name")?.Value == "connection.connection_string");
|
|
|
|
if (propertyElement == null)
|
|
{
|
|
propertyElement = new XElement(ns + "property",
|
|
new XAttribute("name", "connection.connection_string")
|
|
);
|
|
sessionFactory.Add(propertyElement);
|
|
}
|
|
|
|
// 更新連線字串值
|
|
propertyElement.Value = connString;
|
|
|
|
// 儲存檔案
|
|
doc.Save(_configPath);
|
|
|
|
LogMessage("自動保存了界面設定變更");
|
|
_hasUnsavedChanges = false;
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"自動保存設定時發生錯誤:{ex.Message}", true);
|
|
MessageBox.Show($"保存設定時發生錯誤:{ex.Message}\n操作已取消\n\nError occurred while saving settings: {ex.Message}\nOperation cancelled", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
|
|
case MessageBoxResult.No:
|
|
// 使用原設定值繼續
|
|
LogMessage("用戶選擇使用原設定值繼續操作");
|
|
return true;
|
|
|
|
case MessageBoxResult.Cancel:
|
|
default:
|
|
// 取消操作
|
|
LogMessage("用戶取消了操作");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 在窗體加載完成時優化UI元素
|
|
protected override void OnContentRendered(EventArgs e)
|
|
{
|
|
base.OnContentRendered(e);
|
|
|
|
// 不做任何特殊處理,讓WPF自己管理焦點
|
|
}
|
|
|
|
// 保留搜索方法,但只用於必要地方
|
|
private IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
|
|
{
|
|
if (depObj != null)
|
|
{
|
|
// 直接使用 LINQ 和 LogicalTreeHelper 替代 VisualTreeHelper
|
|
var childrenCount = LogicalTreeHelper.GetChildren(depObj);
|
|
foreach (var child in childrenCount)
|
|
{
|
|
if (child is DependencyObject dependencyObject)
|
|
{
|
|
if (child is T t)
|
|
{
|
|
yield return t;
|
|
}
|
|
|
|
// 遞歸處理子元素
|
|
foreach (T childOfChild in FindVisualChildren<T>(dependencyObject))
|
|
{
|
|
yield return childOfChild;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 當關閉窗口時檢查是否有未保存的變更
|
|
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
|
|
{
|
|
// 如果用戶點擊窗口關閉按鈕,則檢查是否有未保存的變更
|
|
if (_hasUnsavedChanges)
|
|
{
|
|
var result = MessageBox.Show(
|
|
"您有未保存的數據庫設定變更。\n" +
|
|
"是否保存這些變更後再關閉?\n" +
|
|
"「是」 - 保存變更後關閉\n" +
|
|
"「否」 - 不保存變更直接關閉\n" +
|
|
"「取消」 - 不關閉視窗\n\n" +
|
|
"You have unsaved database settings changes.\n" +
|
|
"Do you want to save these changes before closing?\n" +
|
|
"\"Yes\" - Save changes and close\n" +
|
|
"\"No\" - Close without saving changes\n" +
|
|
"\"Cancel\" - Don't close window",
|
|
"未保存的變更 / Unsaved Changes",
|
|
MessageBoxButton.YesNoCancel,
|
|
MessageBoxImage.Question);
|
|
|
|
switch (result)
|
|
{
|
|
case MessageBoxResult.Yes:
|
|
// 保存變更
|
|
try
|
|
{
|
|
if (!ValidateDatabaseSettings())
|
|
{
|
|
e.Cancel = true; // 取消關閉視窗
|
|
return;
|
|
}
|
|
|
|
// 更新連線字串
|
|
UpdateConnectionStrings();
|
|
|
|
// 執行保存設定操作
|
|
string ip = txtDbIP.Text.Trim();
|
|
string port = txtDbPort.Text.Trim();
|
|
string dataSource = $"{ip},{port}";
|
|
var builder = new SqlConnectionStringBuilder
|
|
{
|
|
DataSource = dataSource,
|
|
InitialCatalog = txtDbName.Text,
|
|
UserID = txtDbID.Text,
|
|
Password = txtDbPassword.Text,
|
|
MinPoolSize = int.Parse(txtMinPoolSize.Text),
|
|
MaxPoolSize = int.Parse(txtMaxPoolSize.Text)
|
|
};
|
|
|
|
string connString = builder.ConnectionString;
|
|
|
|
// 確保目錄存在
|
|
string configDir = IOPath.GetDirectoryName(_configPath);
|
|
if (!Directory.Exists(configDir))
|
|
{
|
|
Directory.CreateDirectory(configDir);
|
|
}
|
|
|
|
// 讀取現有的 XML 檔案
|
|
XDocument doc;
|
|
if (IOFile.Exists(_configPath))
|
|
{
|
|
doc = XDocument.Load(_configPath);
|
|
}
|
|
else
|
|
{
|
|
// 建立新的 XML 文件
|
|
doc = new XDocument(
|
|
new XElement("hibernate-configuration",
|
|
new XElement("session-factory")
|
|
)
|
|
);
|
|
}
|
|
|
|
// 取得命名空間
|
|
XNamespace ns = doc.Root.GetDefaultNamespace();
|
|
|
|
// 尋找或建立 session-factory 元素
|
|
var sessionFactory = doc.Root.Element(ns + "session-factory");
|
|
if (sessionFactory == null)
|
|
{
|
|
sessionFactory = new XElement(ns + "session-factory");
|
|
doc.Root.Add(sessionFactory);
|
|
}
|
|
|
|
// 尋找或建立 connection.connection_string 屬性
|
|
var propertyElement = sessionFactory.Elements(ns + "property")
|
|
.FirstOrDefault(p => p.Attribute("name")?.Value == "connection.connection_string");
|
|
|
|
if (propertyElement == null)
|
|
{
|
|
propertyElement = new XElement(ns + "property",
|
|
new XAttribute("name", "connection.connection_string")
|
|
);
|
|
sessionFactory.Add(propertyElement);
|
|
}
|
|
|
|
// 更新連線字串值
|
|
propertyElement.Value = connString;
|
|
|
|
// 儲存檔案
|
|
doc.Save(_configPath);
|
|
|
|
LogMessage("關閉視窗前已自動保存設定變更");
|
|
_hasUnsavedChanges = false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"自動保存設定時發生錯誤:{ex.Message}", true);
|
|
|
|
// 詢問用戶是否仍要關閉視窗
|
|
var errorResult = MessageBox.Show($"保存設定時發生錯誤:{ex.Message}\n\n是否仍要關閉視窗?\n\nError occurred while saving settings: {ex.Message}\n\nDo you still want to close the window?",
|
|
"錯誤 / Error", MessageBoxButton.YesNo, MessageBoxImage.Error);
|
|
|
|
if (errorResult != MessageBoxResult.Yes)
|
|
{
|
|
e.Cancel = true; // 取消關閉視窗
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MessageBoxResult.No:
|
|
// 不保存直接關閉
|
|
_hasUnsavedChanges = false;
|
|
break;
|
|
|
|
case MessageBoxResult.Cancel:
|
|
default:
|
|
// 取消關閉
|
|
e.Cancel = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
base.OnClosing(e);
|
|
}
|
|
|
|
// 當點擊窗體空白處時,讓當前擁有焦點的文本框失去焦點
|
|
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
// 獲取點擊的元素
|
|
var element = e.OriginalSource as DependencyObject;
|
|
|
|
// 檢查是否是有效的點擊目標
|
|
if (element != null)
|
|
{
|
|
// 檢查點擊的是否為文本框或其子元素
|
|
bool isTextBox = false;
|
|
|
|
// 檢查點擊的元素或其父元素是否為TextBox
|
|
DependencyObject current = element;
|
|
while (current != null && !isTextBox)
|
|
{
|
|
if (current is TextBox)
|
|
{
|
|
isTextBox = true;
|
|
break;
|
|
}
|
|
// 使用VisualTreeHelper獲取更準確的父元素
|
|
current = VisualTreeHelper.GetParent(current);
|
|
}
|
|
|
|
// 如果點擊的不是文本框
|
|
if (!isTextBox)
|
|
{
|
|
// 記錄當前擁有焦點的元素
|
|
var focusedElement = Keyboard.FocusedElement as UIElement;
|
|
|
|
// 如果當前焦點在文本框上
|
|
if (focusedElement is TextBox)
|
|
{
|
|
// 將焦點轉移到窗體
|
|
this.Focus();
|
|
Keyboard.ClearFocus();
|
|
|
|
// 不要標記為已處理,允許事件繼續傳遞
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 記錄異常但不顯示給用戶,避免影響用戶體驗
|
|
_logger.Error($"處理窗體點擊事件時發生錯誤: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 檢查欄位定義不一致的按鈕點擊事件處理函數
|
|
/// </summary>
|
|
private async void btn_CheckFieldConsistency_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// 禁用所有按鈕
|
|
SetOperationButtonsState(false, "檢查欄位定義");
|
|
|
|
try
|
|
{
|
|
if (!ValidateDatabaseSettings())
|
|
{
|
|
return;
|
|
}
|
|
|
|
UpdateConnectionStrings();
|
|
ClearLog();
|
|
LogMessage("開始檢查資料庫欄位定義一致性");
|
|
|
|
bool hasInconsistentFields = false;
|
|
|
|
// 使用 Task.Run 將資料庫操作移到背景執行緒
|
|
hasInconsistentFields = await Task.Run(() => CheckFieldConsistency(true));
|
|
|
|
if (hasInconsistentFields)
|
|
{
|
|
MessageBox.Show("發現欄位定義不一致的情況,詳情請查看日誌報告。\n\nField definition inconsistencies found. Please check the log report for details.", "檢查結果 / Check Result", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show("未發現欄位定義不一致的情況。\n\nNo field definition inconsistencies found.", "檢查結果 / Check Result", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"檢查欄位定義失敗: {ex.Message}", true);
|
|
MessageBox.Show($"檢查欄位定義失敗。\n請參閱操作記錄了解詳細錯誤資訊。\n\nField definition check failed.\nPlease refer to the operation log for detailed error information.", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
finally
|
|
{
|
|
// 無論執行結果如何,確保按鈕狀態都會被恢復
|
|
SetOperationButtonsState(true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 檢查資料庫中欄位名稱相同但類型或長度不同的情況
|
|
/// </summary>
|
|
/// <param name="showSuccessMessage">是否顯示成功訊息(獨立按鈕檢查時為true,建立資料庫流程中為false)</param>
|
|
/// <returns>是否存在不一致的欄位定義</returns>
|
|
private bool CheckFieldConsistency(bool showSuccessMessage = false)
|
|
{
|
|
LogMessage("開始檢查BC_Dev資料庫所有表格中欄位定義的一致性...");
|
|
|
|
bool hasInconsistentFields = false;
|
|
int totalInconsistencies = 0;
|
|
|
|
try
|
|
{
|
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
|
{
|
|
conn.Open();
|
|
LogMessage("已連接到BC_Dev資料庫");
|
|
|
|
// 獲取所有欄位定義
|
|
var fieldDefinitions = new Dictionary<string, List<(string TableName, string DataType, int MaxLength, bool IsNullable, string DefaultValue)>>();
|
|
|
|
// 查詢所有的表和欄位
|
|
string sql = @"
|
|
SELECT
|
|
t.name AS TableName,
|
|
c.name AS ColumnName,
|
|
ty.name AS DataType,
|
|
c.max_length AS MaxLength,
|
|
c.is_nullable AS IsNullable,
|
|
dc.definition AS DefaultValue
|
|
FROM sys.tables t
|
|
INNER JOIN sys.columns c ON t.object_id = c.object_id
|
|
INNER JOIN sys.types ty ON c.user_type_id = ty.user_type_id
|
|
LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id
|
|
ORDER BY t.name, c.name";
|
|
|
|
using (SqlCommand cmd = new SqlCommand(sql, conn))
|
|
using (SqlDataReader reader = cmd.ExecuteReader())
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
string tableName = reader["TableName"].ToString();
|
|
string columnName = reader["ColumnName"].ToString();
|
|
string dataType = reader["DataType"].ToString();
|
|
int maxLength = Convert.ToInt32(reader["MaxLength"]);
|
|
bool isNullable = Convert.ToBoolean(reader["IsNullable"]);
|
|
string defaultValue = reader["DefaultValue"] == DBNull.Value ? null : reader["DefaultValue"].ToString();
|
|
|
|
// 針對 nvarchar, nchar 等 Unicode 類型,長度需要除以 2
|
|
if (dataType.StartsWith("n") && (dataType.Contains("char") || dataType.Contains("varchar")))
|
|
{
|
|
maxLength /= 2;
|
|
}
|
|
|
|
if (!fieldDefinitions.ContainsKey(columnName))
|
|
{
|
|
fieldDefinitions[columnName] = new List<(string, string, int, bool, string)>();
|
|
}
|
|
|
|
fieldDefinitions[columnName].Add((tableName, dataType, maxLength, isNullable, defaultValue));
|
|
}
|
|
}
|
|
|
|
// 檢查每個欄位是否有不同的定義
|
|
StringBuilder inconsistencyReport = new StringBuilder();
|
|
inconsistencyReport.AppendLine("========== 欄位定義不一致報告 ==========");
|
|
inconsistencyReport.AppendLine("以下是系統中相同名稱但定義不同的欄位列表:");
|
|
inconsistencyReport.AppendLine();
|
|
|
|
foreach (var entry in fieldDefinitions)
|
|
{
|
|
string columnName = entry.Key;
|
|
var definitions = entry.Value;
|
|
|
|
// 如果同名欄位出現在多個表格
|
|
if (definitions.Count > 1)
|
|
{
|
|
// 檢查是否有不同的定義
|
|
bool hasInconsistency = false;
|
|
|
|
// 收集所有不同的資料類型和長度
|
|
var uniqueDefinitions = new Dictionary<(string DataType, int MaxLength), List<string>>();
|
|
|
|
foreach (var def in definitions)
|
|
{
|
|
var key = (def.DataType, def.MaxLength);
|
|
if (!uniqueDefinitions.ContainsKey(key))
|
|
{
|
|
uniqueDefinitions[key] = new List<string>();
|
|
}
|
|
uniqueDefinitions[key].Add(def.TableName);
|
|
}
|
|
|
|
// 如果有多於一種不同的定義,則記錄不一致
|
|
if (uniqueDefinitions.Count > 1)
|
|
{
|
|
hasInconsistency = true;
|
|
hasInconsistentFields = true;
|
|
totalInconsistencies++;
|
|
|
|
// 添加到報告
|
|
inconsistencyReport.AppendLine($"【欄位】{columnName}");
|
|
inconsistencyReport.AppendLine("--------------------------------------------------------");
|
|
|
|
int defIndex = 1;
|
|
foreach (var uniqueDef in uniqueDefinitions)
|
|
{
|
|
var (dataType, maxLength) = uniqueDef.Key;
|
|
var tables = uniqueDef.Value;
|
|
|
|
inconsistencyReport.AppendLine($"定義 {defIndex}:");
|
|
inconsistencyReport.AppendLine($" - 資料類型: {dataType}");
|
|
inconsistencyReport.AppendLine($" - 欄位長度: {maxLength}");
|
|
inconsistencyReport.AppendLine($" - 出現於表格: {string.Join(", ", tables)}");
|
|
inconsistencyReport.AppendLine($" - 表格數量: {tables.Count}");
|
|
inconsistencyReport.AppendLine();
|
|
|
|
defIndex++;
|
|
}
|
|
|
|
inconsistencyReport.AppendLine("--------------------------------------------------------");
|
|
inconsistencyReport.AppendLine();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 輸出報告
|
|
if (hasInconsistentFields)
|
|
{
|
|
inconsistencyReport.AppendLine($"======= 總共發現 {totalInconsistencies} 個欄位定義不一致 =======");
|
|
inconsistencyReport.AppendLine("建議:將相同名稱的欄位統一使用相同的類型和長度,以確保資料庫設計的一致性和避免可能的問題。");
|
|
LogMessage(inconsistencyReport.ToString());
|
|
|
|
// 添加重點提示
|
|
LogMessage("注意:欄位定義不一致可能導致以下問題:", false);
|
|
LogMessage("1. 資料遷移或複製時可能截斷或轉換失敗", false);
|
|
LogMessage("2. 聯結查詢時可能導致效能問題或不正確的結果", false);
|
|
LogMessage("3. 程式中使用通用模型時可能發生錯誤", false);
|
|
|
|
// 自動開啟日誌檔案
|
|
try
|
|
{
|
|
string logPath = @"C:\MPC_Log\DbSettings.log";
|
|
|
|
if (File.Exists(logPath))
|
|
{
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = logPath,
|
|
UseShellExecute = true
|
|
});
|
|
}
|
|
else
|
|
{
|
|
LogMessage($"找不到日誌檔案:{logPath}", true);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"開啟日誌檔案時發生錯誤: {ex.Message}", true);
|
|
}
|
|
}
|
|
else if (showSuccessMessage) // 只在獨立按鈕檢查時顯示成功訊息
|
|
{
|
|
LogMessage("檢查完成,所有相同名稱的欄位定義都是一致的。");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"檢查欄位定義時發生錯誤: {ex.Message}", true);
|
|
throw;
|
|
}
|
|
|
|
return hasInconsistentFields;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 自定義標題欄拖動處理
|
|
/// </summary>
|
|
private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
|
|
{
|
|
if (e.ChangedButton == MouseButton.Left)
|
|
{
|
|
this.DragMove(); // 允許拖動窗口
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 檢查ACJID和APJID表格是否有資料,若有則警告使用者有操作風險
|
|
/// </summary>
|
|
/// <returns>使用者選擇繼續操作則返回true,選擇取消則返回false</returns>
|
|
private bool CheckAccountTables()
|
|
{
|
|
try
|
|
{
|
|
if (!ValidateDatabaseSettings())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UpdateConnectionStrings();
|
|
|
|
bool hasData = false;
|
|
string message = string.Empty;
|
|
|
|
// 檢查資料庫是否存在
|
|
using (SqlConnection conn = new SqlConnection(_masterConnectionString))
|
|
{
|
|
conn.Open();
|
|
string checkDbExistsSql = $"SELECT 1 FROM sys.databases WHERE name = 'BC_Dev'";
|
|
using (SqlCommand cmd = new SqlCommand(checkDbExistsSql, conn))
|
|
{
|
|
object result = cmd.ExecuteScalar();
|
|
if (result == null || Convert.ToInt32(result) != 1)
|
|
{
|
|
// 資料庫不存在,無需檢查表格
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 資料庫存在,檢查表格中是否有資料
|
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
|
{
|
|
conn.Open();
|
|
|
|
// 檢查ACJID表是否存在並有資料
|
|
string checkTableSql = "IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'ACJID') " +
|
|
"SELECT COUNT(*) FROM ACJID ELSE SELECT 0";
|
|
using (SqlCommand cmd = new SqlCommand(checkTableSql, conn))
|
|
{
|
|
try
|
|
{
|
|
int count = Convert.ToInt32(cmd.ExecuteScalar());
|
|
if (count > 0)
|
|
{
|
|
hasData = true;
|
|
message += $"• ACJID表格中有{count}筆資料\n";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"檢查ACJID表格時發生錯誤: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// 檢查APJID表是否存在並有資料
|
|
checkTableSql = "IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'APJID') " +
|
|
"SELECT COUNT(*) FROM APJID ELSE SELECT 0";
|
|
using (SqlCommand cmd = new SqlCommand(checkTableSql, conn))
|
|
{
|
|
try
|
|
{
|
|
int count = Convert.ToInt32(cmd.ExecuteScalar());
|
|
if (count > 0)
|
|
{
|
|
hasData = true;
|
|
message += $"• APJID表格中有{count}筆資料\n";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"檢查APJID表格時發生錯誤: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果任一表格有資料,顯示警告訊息
|
|
if (hasData)
|
|
{
|
|
string warningMessage = "警告:資料庫中有CJ、PJ的帳,操作可能有風險!\n" +
|
|
"Warning: There are CJ and PJ accounts in the database, operation may be risky!\n\n" +
|
|
message + "\n" +
|
|
"這些操作可能導致資料遺失,建議您先清線後再執行。\n" +
|
|
"These operations may cause data loss. It is recommended to clear the line first before proceeding.\n\n" +
|
|
"您確定要繼續嗎?\n" +
|
|
"Are you sure you want to continue?\n\n" +
|
|
"這些操作可能導致資料遺失。建議您先清除資料後再執行操作。\n" +
|
|
"This operation may cause data loss. It is recommended to clear the data first and then perform the operation.";
|
|
|
|
// 記錄警告日誌
|
|
LogMessage("【警告】發現資料庫中有CJ、PJ的帳號資料:", true);
|
|
LogMessage(message.Replace("• ", "").Replace("\n", " "));
|
|
|
|
MessageBoxResult result = MessageBox.Show(
|
|
warningMessage,
|
|
"資料遺失風險警告 / Data Loss Risk Warning",
|
|
MessageBoxButton.OKCancel,
|
|
MessageBoxImage.Warning);
|
|
|
|
// 記錄用戶的選擇
|
|
if (result == MessageBoxResult.OK)
|
|
{
|
|
LogMessage("使用者選擇繼續操作,即使知道CJ、PJ資料可能遺失的風險");
|
|
}
|
|
else
|
|
{
|
|
LogMessage("使用者因CJ、PJ資料遺失風險而取消操作");
|
|
}
|
|
|
|
return result == MessageBoxResult.OK;
|
|
}
|
|
|
|
// 沒有資料,可以安全執行操作
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error($"檢查帳號表格時發生錯誤: {ex.Message}");
|
|
MessageBox.Show($"檢查資料表時發生錯誤: {ex.Message}\n\nError occurred while checking tables: {ex.Message}", "錯誤 / Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|