浏览代码

VisionPro文件安全读写与备份机制重构

新增VisionProFileHelper,支持.vpp文件的安全保存、加载、自动备份与恢复,提升文件损坏容错能力。FileHelper全面增强,支持通用文件/JSON的安全写入、自动恢复、批量修复等。业务代码统一切换为新文件操作接口。修正文件名校验,升级版本号。整体提升视觉项目文件的安全性与可维护性。
孝锋 徐 7 月之前
父节点
当前提交
0363a66e57

+ 796 - 54
TeamAAS-VM/Core/FileHelper.cs

@@ -1,5 +1,7 @@
 using ControlzEx.Standard;
 using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using NPOI.Util;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
@@ -8,118 +10,858 @@ using System.Linq;
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;
-using TeamAAS_VP.Models;
 using TeamAAS_VP;
-using static Org.BouncyCastle.Math.EC.ECCurve;
+using TeamAAS_VP.Models;
+using TeamAAS_VP.Resources.Languages;
 
 namespace TeamAAS_VP.Core
 {
+    /// <summary>
+    /// 提供文件读写、备份与恢复相关的静态辅助方法。
+    /// 功能包括:安全写入(临时文件 + 原子替换)、JSON 读写(含格式校验)、备份版本管理、临时/备份清理与恢复等。
+    /// 该类内部包含可配置的选项,通过 <see cref="Configure"/> 进行设置。
+    /// </summary>
     public static class FileHelper
     {
+        #region 配置选项
         /// <summary>
-        /// 写入Json文件
+        /// 文件操作辅助类的配置项集合。
+        /// 可通过 <see cref="Configure"/> 修改。
         /// </summary>
-        /// <param name="obj">对象</param>
-        /// <param name="path">文件路径</param>
+        public class FileHelperOptions
+        {
+            /// <summary>
+            /// 是否启用自动恢复(读取失败时尝试从备份恢复)。默认值:true。
+            /// </summary>
+            public bool EnableAutoRecovery { get; set; } = true;
+
+            /// <summary>
+            /// 是否保留每次写入时生成的 .bak 备份文件。默认值:false。
+            /// </summary>
+            public bool KeepBackupFiles { get; set; } = false;
+
+            /// <summary>
+            /// 保留的版本化备份最大数量(超过会清理)。默认值:3。
+            /// </summary>
+            public int MaxBackupVersions { get; set; } = 3;
+
+            /// <summary>
+            /// 是否启用内部日志记录(通过 LogHelper)。默认值:false。
+            /// </summary>
+            public bool EnableLogging { get; set; } = false;
+
+            /// <summary>
+            /// 版本化备份存放目录名称(相对于原文件目录)。默认值:"Backups"。
+            /// </summary>
+            public string BackupDirectory { get; set; } = "Backups";
+
+            /// <summary>
+            /// 文件读写所使用的编码。默认值:UTF8。
+            /// </summary>
+            public Encoding FileEncoding { get; set; } = Encoding.UTF8;
+        }
+
+        private static FileHelperOptions _options = new FileHelperOptions();
+
+        /// <summary>
+        /// 配置 FileHelper 的运行时选项。
+        /// </summary>
+        /// <param name="configure">接收并修改 <see cref="FileHelperOptions"/> 的委托;如果为 null 则保持默认配置。</param>
+        public static void Configure(Action<FileHelperOptions> configure)
+        {
+            configure?.Invoke(_options);
+        }
+        #endregion
+
+        #region Json文件操作
+        /// <summary>
+        /// 将对象序列化为格式化 JSON 并安全写入指定路径。
+        /// 写入使用临时文件 + 原子替换以减少损坏风险,并根据配置创建版本化备份。
+        /// </summary>
+        /// <param name="obj">要序列化并写入的对象。</param>
+        /// <param name="path">目标文件完整路径。</param>
         public static void WriteJsonFile(object obj, string path)
         {
-            if (!Directory.Exists(Path.GetDirectoryName(path)))
+            ValidatePath(path);
+            string json = JsonConvert.SerializeObject(obj, Formatting.Indented);
+            WriteFileInternal(json, path);
+        }
+
+        /// <summary>
+        /// 读取 JSON 文件并反序列化为指定类型,支持自动从备份恢复。
+        /// 如果读取失败且启用了自动恢复,将尝试从 .bak 或版本化备份恢复。
+        /// </summary>
+        /// <typeparam name="T">反序列化的目标类型。</typeparam>
+        /// <param name="path">JSON 文件路径。</param>
+        /// <param name="autoRecover">可选:覆盖默认的自动恢复行为(null 表示使用配置项)。</param>
+        /// <returns>反序列化后的对象。</returns>
+        /// <exception cref="FileNotFoundException">当文件不存在且无法恢复时抛出。</exception>
+        public static T ReadJsonFile<T>(string path, bool? autoRecover = null)
+        {
+            ValidatePath(path);
+
+            bool shouldRecover = autoRecover ?? _options.EnableAutoRecovery;
+
+            // 检查并清理临时文件
+            CheckAndCleanTempFile(path);
+
+            // 尝试读取主文件
+            Exception lastException = null;
+
+            try
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(path));
+                if (File.Exists(path))
+                {
+                    string json = ReadAndValidateJsonFile(path);
+                    return JsonConvert.DeserializeObject<T>(json);
+                }
             }
-            using (StreamWriter sw = new StreamWriter(path))
+            catch (Exception ex)
             {
-                string json = JsonConvert.SerializeObject(obj, Formatting.Indented);
-                sw.Write(JsonConvert.SerializeObject(obj, Newtonsoft.Json.Formatting.Indented));
+                lastException = ex;
+                Log($"读取主文件失败: {ex.Message}");
+
+                if (shouldRecover)
+                {
+                    try
+                    {
+                        // 尝试从备份恢复
+                        T recovered = TryRecoverJsonFile<T>(path);
+                        if (recovered != null)
+                            return recovered;
+                    }
+                    catch (Exception recoveryEx)
+                    {
+                        Log($"恢复尝试失败: {recoveryEx.Message}");
+                    }
+                }
             }
+
+            // 如果文件不存在且允许恢复,尝试从默认备份位置恢复
+            if (shouldRecover && !File.Exists(path))
+            {
+                T recovered = TryFindAndRecoverJsonFile<T>(path);
+                if (recovered != null)
+                    return recovered;
+            }
+
+            throw new FileNotFoundException($"文件 {path} 不存在且无法恢复", lastException);
+        }
+        #endregion
+
+        #region 普通文件操作
+        /// <summary>
+        /// 安全写入文本文件(支持目录创建、临时写入、原子替换与可选版本化备份)。
+        /// </summary>
+        /// <param name="content">要写入的文本内容。</param>
+        /// <param name="path">目标文件完整路径。</param>
+        public static void WriteFile(string content, string path)
+        {
+            ValidatePath(path);
+            WriteFileInternal(content, path);
         }
 
         /// <summary>
-        /// 读取Json文件
+        /// 读取文本文件,支持自动恢复(从 .bak 或版本化备份恢复)。
         /// </summary>
-        /// <typeparam name="T">对象</typeparam>
-        /// <param name="path">文件路径</param>
-        /// <returns></returns>
-        /// <exception cref="Exception"></exception>
-        public static T ReadJsonFile<T>(string path)
+        /// <param name="path">文件路径。</param>
+        /// <param name="autoRecover">可选:覆盖默认的自动恢复行为(null 表示使用配置项)。</param>
+        /// <returns>文件内容字符串。</returns>
+        /// <exception cref="FileNotFoundException">当文件不存在且无法恢复时抛出。</exception>
+        public static string ReadFile(string path, bool? autoRecover = null)
         {
-            if (File.Exists(path))
+            ValidatePath(path);
+
+            bool shouldRecover = autoRecover ?? _options.EnableAutoRecovery;
+
+            // 检查并清理临时文件
+            CheckAndCleanTempFile(path);
+
+            // 尝试读取主文件
+            Exception lastException = null;
+
+            try
             {
-                string buffer;
-                using (StreamReader sr = new StreamReader(path))
+                if (File.Exists(path))
                 {
-                    buffer = sr.ReadToEnd();
+                    return ReadFileWithValidation(path);
                 }
-                return JsonConvert.DeserializeObject<T>(buffer);
             }
-            else
+            catch (Exception ex)
             {
-                throw new Exception($"文件不存在:[{path}]");
+                lastException = ex;
+                Log($"读取主文件失败: {ex.Message}");
+
+                if (shouldRecover)
+                {
+                    try
+                    {
+                        // 尝试从备份恢复
+                        string recovered = TryRecoverFile(path);
+                        if (recovered != null)
+                            return recovered;
+                    }
+                    catch (Exception recoveryEx)
+                    {
+                        Log($"恢复尝试失败: {recoveryEx.Message}");
+                    }
+                }
             }
+
+            // 如果文件不存在且允许恢复,尝试从默认备份位置恢复
+            if (shouldRecover && !File.Exists(path))
+            {
+                string recovered = TryFindAndRecoverFile(path);
+                if (recovered != null)
+                    return recovered;
+            }
+
+            throw new FileNotFoundException($"文件 {path} 不存在且无法恢复", lastException);
         }
 
         /// <summary>
-        /// 写入文件
+        /// 在不抛出异常的情况下安全读取文件内容,失败时返回提供的默认值。
         /// </summary>
-        /// <param name="content">对象</param>
-        /// <param name="path">文件路径</param>
-        public static void WriteFile(string content, string path)
+        /// <param name="path">文件路径。</param>
+        /// <param name="defaultValue">读取失败时返回的默认值(可为 null)。</param>
+        /// <returns>文件内容或默认值。</returns>
+        public static string SafeReadFile(string path, string defaultValue = null)
+        {
+            try
+            {
+                return ReadFile(path, true);
+            }
+            catch
+            {
+                return defaultValue;
+            }
+        }
+        #endregion
+
+        #region 文件恢复相关
+        /// <summary>
+        /// 检查并修复指定目录下的所有相关临时文件与备份文件。
+        /// - 恢复或清理 .tmp 文件
+        /// - 清理孤立或过旧的 .bak 文件
+        /// - 清理版本化备份超过保留数量的旧版本
+        /// </summary>
+        /// <param name="directoryPath">要检查的目录路径。</param>
+        public static void CheckAndRepairDirectory(string directoryPath)
         {
-            if (!Directory.Exists(Path.GetDirectoryName(path)))
+            if (!Directory.Exists(directoryPath))
+                return;
+
+            // 处理临时文件
+            var tempFiles = Directory.GetFiles(directoryPath, "*.tmp", SearchOption.AllDirectories);
+            foreach (var tempFile in tempFiles)
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(path));
+                TryRecoverFromTempFile(tempFile);
             }
-            using (StreamWriter sw = new StreamWriter(path))
+
+            // 处理备份文件
+            var backupFiles = Directory.GetFiles(directoryPath, "*.bak", SearchOption.AllDirectories);
+            foreach (var backupFile in backupFiles)
             {
-                sw.Write(content);
+                TryCleanOrphanedBackup(backupFile);
             }
+
+            // 处理版本化备份
+            CleanupOldBackupVersions(directoryPath);
         }
 
         /// <summary>
-        /// 读取文件
+        /// 强制从备份恢复主文件。优先使用直接的 .bak,其次尝试版本化备份目录中的最新文件。
         /// </summary>
-        /// <param name="path"></param>
-        /// <returns></returns>
-        /// <exception cref="Exception"></exception>
-        public static string ReadFile(string path)
+        /// <param name="originalPath">原文件路径。</param>
+        /// <returns>成功返回 true,失败返回 false。</returns>
+        public static bool ForceRecoverFile(string originalPath)
         {
-            if (File.Exists(path))
+            try
             {
-                string buffer;
-                using (StreamReader sr = new StreamReader(path))
+                // 尝试从直接备份恢复
+                string backupPath = originalPath + ".bak";
+                if (File.Exists(backupPath))
                 {
-                    buffer = sr.ReadToEnd();
+                    File.Copy(backupPath, originalPath, true);
+                    Log($"从备份强制恢复: {originalPath}");
+                    return true;
                 }
-                return buffer;
+
+                // 尝试从版本化备份恢复
+                var recovered = TryFindLatestBackup(originalPath);
+                if (recovered != null)
+                {
+                    File.Copy(recovered, originalPath, true);
+                    Log($"从版本备份强制恢复: {originalPath}");
+                    return true;
+                }
+
+                return false;
             }
-            else
+            catch (Exception ex)
             {
-                throw new Exception($"文件不存在:[{path}]");
+                Log($"强制恢复失败: {ex.Message}");
+                return false;
             }
         }
+        #endregion
 
+        #region 辅助方法
         /// <summary>
-        /// 检测文件名是否合规
+        /// 检测给定的文件名是否合规(不包含 Windows 文件名禁止字符)。
         /// </summary>
-        /// <param name="filename"></param>
-        /// <returns></returns>
+        /// <param name="filename">仅文件名部分(不含路径)。</param>
+        /// <returns>文件名合法返回 true,否则返回 false。</returns>
         public static bool CheckFileName(string filename)
         {
-            //
+            if (string.IsNullOrWhiteSpace(filename))
+                return false;
+
             // 定义文件名合法性的正则表达式
-            string pattern = @"^[^-\\/:*?""<>|\x00-\x1F]*$"; // 匹配不包含非法字符的文件名
+            string pattern = @"^[^\\/:*?""<>|\x00-\x1F]*$";
+            return Regex.IsMatch(filename, pattern);
+        }
 
-            // 使用正则表达式进行匹配
-            if (Regex.IsMatch(filename, pattern))
+        /// <summary>
+        /// 检查文件是否存在且有效(可读且长度大于 0)。
+        /// </summary>
+        /// <param name="path">文件完整路径。</param>
+        /// <returns>文件存在且有效返回 true,否则返回 false。</returns>
+        public static bool IsFileValid(string path)
+        {
+            try
             {
-                //Console.WriteLine("输入的名称符合文件名命名规则,并且不包含非法字符。");
-                return true;
+                if (!File.Exists(path))
+                    return false;
+
+                // 尝试读取一小部分内容来验证文件可访问性
+                using (var fs = File.OpenRead(path))
+                {
+                    return fs.CanRead && fs.Length > 0;
+                }
             }
-            else
+            catch
             {
-                //Console.WriteLine("输入的名称不符合文件名命名规则,或者包含非法字符。");
                 return false;
             }
         }
+
+        /// <summary>
+        /// 为给定原始路径生成一个安全的临时文件路径(放在系统临时目录),包含随机 GUID 前缀。
+        /// </summary>
+        /// <param name="originalPath">原文件完整路径,用于生成可识别的临时文件名。</param>
+        /// <returns>生成的临时文件完整路径。</returns>
+        public static string GetTempFilePath(string originalPath)
+        {
+            // 尝试放在目标目录(确保同一卷)
+            string dir = Path.GetDirectoryName(originalPath);
+            try
+            {
+                if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
+                {
+                    string safeName = Path.GetFileName(originalPath).Replace(" ", "_").Replace(":", "_");
+                    return Path.Combine(dir, $"{Guid.NewGuid():N}_{safeName}.tmp");
+                }
+            }
+            catch
+            {
+                // 忽略并回退到系统临时目录
+            }
+
+            // 回退(极少用到)
+            string tempDir = Path.GetTempPath();
+            string fallbackName = Path.GetFileName(originalPath).Replace(" ", "_").Replace(":", "_");
+            return Path.Combine(tempDir, $"{Guid.NewGuid():N}_{fallbackName}.tmp");
+        }
+        #endregion
+
+        #region 私有实现方法
+        /// <summary>
+        /// 验证路径与文件名合法性(非空且文件名不包含非法字符)。
+        /// </summary>
+        /// <param name="path">要验证的文件路径。</param>
+        private static void ValidatePath(string path)
+        {
+            if (string.IsNullOrWhiteSpace(path))
+                throw new ArgumentException("路径不能为空", nameof(path));
+
+            string fileName = Path.GetFileName(path);
+            if (!CheckFileName(fileName))
+                throw new ArgumentException($"文件名 '{fileName}' 包含非法字符", nameof(path));
+        }
+
+        /// <summary>
+        /// 内部写文件实现:创建目录、可选创建版本化备份、写入临时文件并以原子方式替换目标文件。
+        /// 发生异常时会尝试清理临时文件。
+        /// </summary>
+        /// <param name="content">要写入的文本内容。</param>
+        /// <param name="path">目标文件完整路径。</param>
+        private static void WriteFileInternal(string content, string path)
+        {
+            // 创建目录
+            string directory = Path.GetDirectoryName(path);
+            if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+            {
+                Directory.CreateDirectory(directory);
+            }
+
+            // 创建版本化备份
+            if (_options.KeepBackupFiles && File.Exists(path))
+            {
+                CreateVersionedBackup(path);
+            }
+
+            // 临时文件路径
+            string tempPath = path + ".tmp"; //GetTempFilePath(path);
+
+            try
+            {
+                // 写入临时文件
+                using (var fs = new FileStream(tempPath,
+                    FileMode.Create,
+                    FileAccess.Write,
+                    FileShare.None,
+                    bufferSize: 4096,
+                    useAsync: false))
+                using (var sw = new StreamWriter(fs, _options.FileEncoding))
+                {
+                    sw.Write(content);
+                    sw.Flush();
+                    fs.Flush(true); // 强制刷新到磁盘
+                }
+
+                // 原子性替换
+                if (File.Exists(path))
+                {
+                    string backupPath = path + ".bak";
+                    File.Replace(tempPath, path, backupPath, true);
+
+                    // 如果不保留备份文件,删除它
+                    if (!_options.KeepBackupFiles && File.Exists(backupPath))
+                    {
+                        try { File.Delete(backupPath); } catch { }
+                    }
+                }
+                else
+                {
+                    File.Move(tempPath, path);
+                }
+            }
+            catch
+            {
+                // 清理临时文件
+                SafeDelete(tempPath);
+                throw;
+            }
+            finally
+            {
+                // 确保临时文件被清理
+                SafeDelete(tempPath);
+            }
+        }
+
+        /// <summary>
+        /// 创建版本化备份:将原文件复制到同目录下的备份目录,并带时间戳后缀。
+        /// 发生异常时仅记录日志,不抛出。
+        /// </summary>
+        /// <param name="originalPath">要备份的原文件路径。</param>
+        private static void CreateVersionedBackup(string originalPath)
+        {
+            try
+            {
+                string backupDir = Path.Combine(Path.GetDirectoryName(originalPath), _options.BackupDirectory);
+                if (!Directory.Exists(backupDir))
+                    Directory.CreateDirectory(backupDir);
+
+                string fileName = Path.GetFileNameWithoutExtension(originalPath);
+                string extension = Path.GetExtension(originalPath);
+                string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
+                string backupName = $"{fileName}_{timestamp}{extension}";
+                string backupPath = Path.Combine(backupDir, backupName);
+
+                File.Copy(originalPath, backupPath, true);
+                Log($"创建版本备份: {backupPath}");
+            }
+            catch (Exception ex)
+            {
+                Log($"创建版本备份失败: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// 检查并清理与原文件同名的临时文件(使用 .tmp 后缀的临时文件),并在可能的情况下记录发现的信息。
+        /// </summary>
+        /// <param name="originalPath">原文件完整路径。</param>
+        private static void CheckAndCleanTempFile(string originalPath)
+        {
+            string tempPath = originalPath + ".tmp";
+            if (File.Exists(tempPath))
+            {
+                try
+                {
+                    // 检查临时文件是否较新(可能是中断的写入)
+                    if (File.Exists(originalPath))
+                    {
+                        var originalTime = File.GetLastWriteTime(originalPath);
+                        var tempTime = File.GetLastWriteTime(tempPath);
+
+                        if (tempTime > originalTime)
+                        {
+                            Log($"发现较新的临时文件,可能上次写入未完成: {tempPath}");
+                        }
+                    }
+
+                    File.Delete(tempPath);
+                    Log($"清理临时文件: {tempPath}");
+                }
+                catch (Exception ex)
+                {
+                    Log($"清理临时文件失败: {ex.Message}");
+                }
+            }
+        }
+
+        /// <summary>
+        /// 尝试从直接的 .bak 文件恢复 JSON 文件并返回反序列化的对象;恢复成功后尝试覆盖主文件。
+        /// </summary>
+        /// <typeparam name="T">目标类型。</typeparam>
+        /// <param name="originalPath">原 JSON 文件路径。</param>
+        /// <returns>恢复并反序列化后的对象,失败返回 default(T)。</returns>
+        private static T TryRecoverJsonFile<T>(string originalPath)
+        {
+            // 尝试从直接备份恢复
+            string backupPath = originalPath + ".bak";
+            if (File.Exists(backupPath))
+            {
+                try
+                {
+                    string json = ReadAndValidateJsonFile(backupPath);
+                    T result = JsonConvert.DeserializeObject<T>(json);
+
+                    // 尝试恢复主文件
+                    TryRestoreMainFile(backupPath, originalPath);
+
+                    Log($"从备份恢复Json文件: {originalPath}");
+                    return result;
+                }
+                catch (Exception ex)
+                {
+                    Log($"从备份恢复Json文件失败: {ex.Message}");
+                }
+            }
+
+            return default;
+        }
+
+        /// <summary>
+        /// 尝试从直接的 .bak 文件恢复文本文件并返回内容;恢复成功后尝试覆盖主文件。
+        /// </summary>
+        /// <param name="originalPath">原文件路径。</param>
+        /// <returns>恢复后的内容,失败返回 null。</returns>
+        private static string TryRecoverFile(string originalPath)
+        {
+            // 尝试从直接备份恢复
+            string backupPath = originalPath + ".bak";
+            if (File.Exists(backupPath))
+            {
+                try
+                {
+                    string content = ReadFileWithValidation(backupPath);
+
+                    // 尝试恢复主文件
+                    TryRestoreMainFile(backupPath, originalPath);
+
+                    Log($"从备份恢复文件: {originalPath}");
+                    return content;
+                }
+                catch (Exception ex)
+                {
+                    Log($"从备份恢复文件失败: {ex.Message}");
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// 在版本化备份目录中查找最新的备份并尝试恢复 JSON 文件,成功返回反序列化对象并复制到主文件。
+        /// </summary>
+        /// <typeparam name="T">目标类型。</typeparam>
+        /// <param name="originalPath">原 JSON 文件路径。</param>
+        /// <returns>恢复并反序列化后的对象,失败返回 default(T)。</returns>
+        private static T TryFindAndRecoverJsonFile<T>(string originalPath)
+        {
+            string backup = TryFindLatestBackup(originalPath);
+            if (backup != null)
+            {
+                try
+                {
+                    string json = ReadAndValidateJsonFile(backup);
+                    T result = JsonConvert.DeserializeObject<T>(json);
+
+                    File.Copy(backup, originalPath, true);
+                    Log($"从版本备份恢复Json文件: {originalPath}");
+                    return result;
+                }
+                catch (Exception ex)
+                {
+                    Log($"从版本备份恢复Json文件失败: {ex.Message}");
+                }
+            }
+
+            return default;
+        }
+
+        /// <summary>
+        /// 在版本化备份目录中查找最新的备份并尝试恢复文本文件,成功后复制到主文件并返回内容。
+        /// </summary>
+        /// <param name="originalPath">原文件路径。</param>
+        /// <returns>恢复后的内容,失败返回 null。</returns>
+        private static string TryFindAndRecoverFile(string originalPath)
+        {
+            string backup = TryFindLatestBackup(originalPath);
+            if (backup != null)
+            {
+                try
+                {
+                    string content = ReadFileWithValidation(backup);
+                    File.Copy(backup, originalPath, true);
+                    Log($"从版本备份恢复文件: {originalPath}");
+                    return content;
+                }
+                catch (Exception ex)
+                {
+                    Log($"从版本备份恢复文件失败: {ex.Message}");
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// 查找最新的版本化备份文件路径。优先检查指定的备份目录,其次检查原目录下同名带时间戳的文件。
+        /// </summary>
+        /// <param name="originalPath">原始文件路径。</param>
+        /// <returns>最新备份文件的完整路径或 null(未找到)。</returns>
+        private static string TryFindLatestBackup(string originalPath)
+        {
+            string directory = Path.GetDirectoryName(originalPath);
+            string fileName = Path.GetFileNameWithoutExtension(originalPath);
+            string extension = Path.GetExtension(originalPath);
+
+            if (!Directory.Exists(directory))
+                return null;
+
+            // 检查备份目录
+            string backupDir = Path.Combine(directory, _options.BackupDirectory);
+            if (Directory.Exists(backupDir))
+            {
+                var backups = Directory.GetFiles(backupDir, $"{fileName}_*{extension}")
+                    .OrderByDescending(f => f)
+                    .ToList();
+
+                if (backups.Any())
+                    return backups.First();
+            }
+
+            // 检查当前目录的备份文件
+            var localBackups = Directory.GetFiles(directory, $"{fileName}_*{extension}")
+                .OrderByDescending(f => f)
+                .ToList();
+
+            if (localBackups.Any())
+                return localBackups.First();
+
+            return null;
+        }
+
+        /// <summary>
+        /// 读取文件并在读取后进行 JSON 格式校验(适用于 JSON 文件)。如果格式无效则抛出异常。
+        /// </summary>
+        /// <param name="filePath">文件路径。</param>
+        /// <returns>文件文本内容。</returns>
+        /// <exception cref="InvalidDataException">当 JSON 格式无效时抛出。</exception>
+        private static string ReadAndValidateJsonFile(string filePath)
+        {
+            string json = ReadFileWithValidation(filePath);
+
+            // 验证JSON格式
+            if (!string.IsNullOrWhiteSpace(json))
+            {
+                try
+                {
+                    JToken.Parse(json);
+                }
+                catch (JsonException ex)
+                {
+                    throw new InvalidDataException($"文件 {filePath} 包含无效的JSON格式: {ex.Message}", ex);
+                }
+            }
+
+            return json;
+        }
+
+        /// <summary>
+        /// 读取文件并进行基本验证(文件存在且读取结果非 null)。
+        /// </summary>
+        /// <param name="filePath">文件路径。</param>
+        /// <returns>文件内容字符串。</returns>
+        /// <exception cref="FileNotFoundException">文件不存在时抛出。</exception>
+        /// <exception cref="InvalidDataException">读取结果为 null 时抛出。</exception>
+        private static string ReadFileWithValidation(string filePath)
+        {
+            if (!File.Exists(filePath))
+                throw new FileNotFoundException($"文件不存在: {filePath}");
+
+            string content = File.ReadAllText(filePath, _options.FileEncoding);
+
+            if (content == null)
+                throw new InvalidDataException($"文件 {filePath} 内容为null");
+
+            return content;
+        }
+
+        /// <summary>
+        /// 将备份文件复制回主文件路径以进行恢复,失败时记录日志但不抛出。
+        /// </summary>
+        /// <param name="backupPath">备份文件路径。</param>
+        /// <param name="originalPath">目标主文件路径。</param>
+        private static void TryRestoreMainFile(string backupPath, string originalPath)
+        {
+            try
+            {
+                File.Copy(backupPath, originalPath, true);
+                Log($"恢复主文件: {originalPath}");
+            }
+            catch (Exception ex)
+            {
+                Log($"恢复主文件失败: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// 尝试根据 .tmp 临时文件恢复原文件:如果临时文件较新或原文件不存在则移动临时文件覆盖原文件,否则删除临时文件。
+        /// 出错时尝试安全删除临时文件。
+        /// </summary>
+        /// <param name="tempPath">临时文件路径。</param>
+        private static void TryRecoverFromTempFile(string tempPath)
+        {
+            try
+            {
+                // 获取原文件路径(假设临时文件是 .tmp 扩展名)
+                string originalPath = tempPath.EndsWith(".tmp")
+                    ? tempPath.Substring(0, tempPath.Length - 4)
+                    : tempPath;
+
+                // 如果原文件不存在或临时文件更新,则恢复
+                if (!File.Exists(originalPath) ||
+                    File.GetLastWriteTime(tempPath) > File.GetLastWriteTime(originalPath))
+                {
+                    File.Move(tempPath, originalPath);
+                    Log($"从临时文件恢复: {originalPath}");
+                }
+                else
+                {
+                    File.Delete(tempPath);
+                }
+            }
+            catch
+            {
+                SafeDelete(tempPath);
+            }
+        }
+
+        /// <summary>
+        /// 清理孤立或过旧的 .bak 备份:如果主文件存在且备份比主文件早超过一定时间,则删除备份。
+        /// </summary>
+        /// <param name="backupPath">备份文件路径(通常以 .bak 结尾)。</param>
+        private static void TryCleanOrphanedBackup(string backupPath)
+        {
+            try
+            {
+                string originalPath = backupPath.EndsWith(".bak")
+                    ? backupPath.Substring(0, backupPath.Length - 4)
+                    : backupPath;
+
+                // 如果主文件存在且备份较旧,删除备份
+                if (File.Exists(originalPath))
+                {
+                    var originalTime = File.GetLastWriteTime(originalPath);
+                    var backupTime = File.GetLastWriteTime(backupPath);
+
+                    if (originalTime > backupTime.AddMinutes(5))
+                    {
+                        File.Delete(backupPath);
+                        Log($"清理旧备份: {backupPath}");
+                    }
+                }
+            }
+            catch
+            {
+                // 忽略错误
+            }
+        }
+
+        /// <summary>
+        /// 清理版本化备份目录中超过保留数量的旧版本(按名称分组并按字符串降序保留最新的若干个)。
+        /// </summary>
+        /// <param name="directory">原文件所在目录,该目录下可能包含版本化备份子目录。</param>
+        private static void CleanupOldBackupVersions(string directory)
+        {
+            try
+            {
+                string backupDir = Path.Combine(directory, _options.BackupDirectory);
+                if (!Directory.Exists(backupDir))
+                    return;
+
+                var fileGroups = Directory.GetFiles(backupDir)
+                    .GroupBy(f => Path.GetFileNameWithoutExtension(f).Split('_')[0])
+                    .ToList();
+
+                foreach (var group in fileGroups)
+                {
+                    var backups = group.OrderByDescending(f => f).ToList();
+                    for (int i = _options.MaxBackupVersions; i < backups.Count; i++)
+                    {
+                        SafeDelete(backups[i]);
+                    }
+                }
+            }
+            catch
+            {
+                // 忽略错误
+            }
+        }
+
+        /// <summary>
+        /// 尝试安全删除指定文件,删除失败则忽略异常。
+        /// </summary>
+        /// <param name="filePath">要删除的文件路径。</param>
+        private static void SafeDelete(string filePath)
+        {
+            try
+            {
+                if (File.Exists(filePath))
+                    File.Delete(filePath);
+            }
+            catch
+            {
+                // 忽略删除错误
+            }
+        }
+
+        /// <summary>
+        /// 内部日志方法,只有在配置启用时才会调用 LogHelper 记录调试日志。
+        /// </summary>
+        /// <param name="message">日志消息正文。</param>
+        private static void Log(string message)
+        {
+            if (_options.EnableLogging)
+            {
+                //Console.WriteLine($"[FileHelper] {DateTime.Now:HH:mm:ss} - {message}");
+                LogHelper.WriteLogDebug($"[FileHelper] {DateTime.Now:HH:mm:ss} - {message}");
+            }
+        }
+        #endregion
     }
 }

+ 687 - 0
TeamAAS-VM/Core/VisionProFileHelper.cs

@@ -0,0 +1,687 @@
+using Cognex.VisionPro;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TeamAAS_VP.Core
+{
+    /// <summary>
+    /// VisionPro 文件操作辅助类:提供安全保存、加载、备份、恢复与验证等功能。
+    /// 该类旨在通过临时文件、备份与验证机制减少文件损坏风险并支持自动恢复。
+    /// </summary>
+    public static class VisionProFileHelper
+    {
+        #region 配置选项
+        /// <summary>
+        /// VisionPro 文件处理的配置项。
+        /// - EnableBackup: 是否启用备份机制
+        /// - KeepBackupFiles: 是否保留版本化备份文件(否则仅保留 .bak 临时备份)
+        /// - MaxBackupVersions: 保留的最大版本备份数量(超过则删除最旧)
+        /// - BackupDirectory: 存放版本备份的目录名(相对于原文件所在目录)
+        /// - ValidateAfterSave: 保存后是否验证文件完整性
+        /// </summary>
+        public class VisionProOptions
+        {
+            public bool EnableBackup { get; set; } = true;
+            public bool KeepBackupFiles { get; set; } = false;
+            public int MaxBackupVersions { get; set; } = 3;
+            public string BackupDirectory { get; set; } = "VisionProBackups";
+            public bool ValidateAfterSave { get; set; } = true;
+        }
+
+        private static VisionProOptions _options = new VisionProOptions();
+
+        /// <summary>
+        /// 配置 VisionPro 文件处理选项。
+        /// </summary>
+        /// <param name="configure">用于配置 VisionProOptions 的委托(可为 null)</param>
+        public static void Configure(Action<VisionProOptions> configure)
+        {
+            configure?.Invoke(_options);
+        }
+        #endregion
+
+        #region 安全保存方法
+        /// <summary>
+        /// 安全保存 VisionPro 对象到指定文件路径。
+        /// 实现要点:
+        /// - 支持在保存前根据配置创建版本化备份或 .bak 备份
+        /// - 先保存到临时文件,再进行原子替换(File.Replace 或 File.Move)
+        /// - 可选的保存后验证,验证失败会抛出异常并尝试从备份恢复
+        /// </summary>
+        /// <param name="obj">要保存的 VisionPro 对象(不能为空)</param>
+        /// <param name="filePath">目标文件路径(不能为空或空白)</param>
+        /// <param name="backupEnabled">可选:覆盖全局备份配置</param>
+        /// <returns>成功返回 true,如发生异常会抛出 IOException</returns>
+        public static bool SaveObjectToFileSafe(object obj, string filePath, bool? backupEnabled = null)
+        {
+            if (obj == null)
+                throw new ArgumentNullException(nameof(obj));
+            if (string.IsNullOrWhiteSpace(filePath))
+                throw new ArgumentException("文件路径不能为空", nameof(filePath));
+
+            bool useBackup = backupEnabled ?? _options.EnableBackup;
+
+            // 创建目录(若不存在)
+            string directory = Path.GetDirectoryName(filePath);
+            if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+            {
+                Directory.CreateDirectory(directory);
+            }
+
+            // 生成临时文件路径用于先写入
+            string tempFilePath = GetVisionProTempFilePath(filePath);
+
+            try
+            {
+                // 如果启用备份且目标文件已存在,则创建备份(版本化或延续 .bak 机制)
+                if (useBackup && File.Exists(filePath))
+                {
+                    CreateVisionProBackup(filePath);
+                }
+
+                // 将对象保存到临时文件
+                CogSerializer.SaveObjectToFile(obj, tempFilePath);
+
+                // 可选:验证临时文件的完整性
+                if (_options.ValidateAfterSave && !ValidateVisionProFile(tempFilePath))
+                {
+                    throw new InvalidOperationException("保存的VisionPro文件验证失败");
+                }
+
+                // 原子性替换:如果目标存在使用 File.Replace(可生成 .bak),否则直接移动临时文件
+                if (File.Exists(filePath))
+                {
+                    string backupPath = filePath + ".bak";
+                    File.Replace(tempFilePath, filePath, backupPath, true);
+
+                    // 若配置不保留备份,删除 .bak 文件
+                    if (!_options.KeepBackupFiles && File.Exists(backupPath))
+                    {
+                        SafeDelete(backupPath);
+                    }
+                }
+                else
+                {
+                    File.Move(tempFilePath, filePath);
+                }
+
+                Log($"成功保存VisionPro对象到: {filePath}");
+                return true;
+            }
+            catch (Exception ex)
+            {
+                Log($"保存VisionPro对象失败: {ex.Message}");
+
+                // 尝试从备份恢复(若启用了备份)
+                if (useBackup)
+                {
+                    Log("尝试从备份恢复...");
+                    TryRestoreVisionProFile(filePath);
+                }
+
+                throw new IOException($"保存VisionPro文件失败: {ex.Message}", ex);
+            }
+            finally
+            {
+                // 无论成功与否,尝试清理临时文件
+                SafeDelete(tempFilePath);
+            }
+        }
+
+        /// <summary>
+        /// 安全加载 VisionPro 对象(带自动恢复机制)。
+        /// 尝试加载主文件,若失败并且允许自动恢复则尝试从 .bak 或版本备份恢复。
+        /// </summary>
+        /// <typeparam name="T">目标对象类型</typeparam>
+        /// <param name="filePath">文件路径</param>
+        /// <param name="autoRecover">是否启用自动恢复</param>
+        /// <returns>加载得到的对象(成功),否则抛出 FileNotFoundException</returns>
+        public static T LoadObjectFromFileSafe<T>(string filePath, bool autoRecover = true) where T : class
+        {
+            if (string.IsNullOrWhiteSpace(filePath))
+                throw new ArgumentException("文件路径不能为空", nameof(filePath));
+
+            // 清理可能残留的临时文件
+            CleanVisionProTempFile(filePath);
+
+            // 记录最后一次异常用于抛出上下文
+            Exception lastException = null;
+
+            try
+            {
+                if (File.Exists(filePath))
+                {
+                    var obj = CogSerializer.LoadObjectFromFile(filePath) as T;
+                    if (obj == null)
+                        throw new InvalidCastException($"文件 {filePath} 不是有效的 {typeof(T).Name} 类型");
+
+                    return obj;
+                }
+            }
+            catch (Exception ex)
+            {
+                lastException = ex;
+                Log($"加载主文件失败: {ex.Message}");
+
+                if (autoRecover)
+                {
+                    try
+                    {
+                        // 尝试从 .bak 或其他备份恢复并返回对象
+                        T recovered = TryRecoverVisionProFile<T>(filePath);
+                        if (recovered != null)
+                            return recovered;
+                    }
+                    catch (Exception recoveryEx)
+                    {
+                        Log($"恢复尝试失败: {recoveryEx.Message}");
+                    }
+                }
+            }
+
+            // 如果主文件不存在且允许恢复,尝试查找版本化备份进行恢复
+            if (autoRecover && !File.Exists(filePath))
+            {
+                T recovered = TryFindAndRecoverVisionProFile<T>(filePath);
+                if (recovered != null)
+                    return recovered;
+            }
+
+            throw new FileNotFoundException($"VisionPro文件 {filePath} 不存在且无法恢复", lastException);
+        }
+
+        /// <summary>
+        /// 简化版安全加载:遇到任何异常时返回提供的默认值(不抛出)。
+        /// </summary>
+        public static T SafeLoadObjectFromFile<T>(string filePath, T defaultValue = null) where T : class
+        {
+            try
+            {
+                return LoadObjectFromFileSafe<T>(filePath, true);
+            }
+            catch
+            {
+                return defaultValue;
+            }
+        }
+        #endregion
+
+        #region 恢复和验证方法
+        /// <summary>
+        /// 检查并修复单个 VisionPro 文件:
+        /// - 检查并尝试从临时文件恢复
+        /// - 检查 .bak 备份并在合理时恢复
+        /// - 检查版本化备份并恢复最新的可用版本
+        /// </summary>
+        /// <param name="filePath">要检查的文件路径</param>
+        /// <returns>如果执行了恢复操作返回 true,否则返回 false</returns>
+        public static bool CheckAndRepairVisionProFile(string filePath)
+        {
+            try
+            {
+                // 检查临时文件并尝试恢复
+                string tempPath = filePath + ".vp_tmp";
+                if (File.Exists(tempPath))
+                {
+                    TryRecoverFromVisionProTemp(tempPath);
+                }
+
+                // 检查 .bak 备份(File.Replace 可能产生)
+                string backupPath = filePath + ".bak";
+                if (File.Exists(backupPath))
+                {
+                    // 验证备份文件并在主文件不存在或备份更新时恢复
+                    if (ValidateVisionProFile(backupPath))
+                    {
+                        if (!File.Exists(filePath) ||
+                            File.GetLastWriteTime(backupPath) > File.GetLastWriteTime(filePath))
+                        {
+                            File.Copy(backupPath, filePath, true);
+                            Log($"从备份恢复VisionPro文件: {filePath}");
+                            return true;
+                        }
+                    }
+                }
+
+                // 查找并恢复版本化备份(BackupDirectory 或其他模式)
+                var latestBackup = FindLatestVisionProBackup(filePath);
+                if (latestBackup != null)
+                {
+                    File.Copy(latestBackup, filePath, true);
+                    Log($"从版本备份恢复VisionPro文件: {filePath}");
+                    return true;
+                }
+
+                return false;
+            }
+            catch (Exception ex)
+            {
+                Log($"修复VisionPro文件失败: {ex.Message}");
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// 强制从备份恢复(优先 .bak,然后版本化备份)。
+        /// </summary>
+        /// <param name="filePath">目标文件路径</param>
+        /// <returns>恢复成功返回 true,否则 false</returns>
+        public static bool ForceRecoverVisionProFile(string filePath)
+        {
+            try
+            {
+                // 优先使用 .bak 恢复(如存在且验证通过)
+                string backupPath = filePath + ".bak";
+                if (File.Exists(backupPath) && ValidateVisionProFile(backupPath))
+                {
+                    File.Copy(backupPath, filePath, true);
+                    Log($"强制从备份恢复: {filePath}");
+                    return true;
+                }
+
+                // 否则查找版本化备份并恢复最新可用的
+                var latestBackup = FindLatestVisionProBackup(filePath);
+                if (latestBackup != null)
+                {
+                    File.Copy(latestBackup, filePath, true);
+                    Log($"强制从版本备份恢复: {filePath}");
+                    return true;
+                }
+
+                return false;
+            }
+            catch (Exception ex)
+            {
+                Log($"强制恢复失败: {ex.Message}");
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// 尝试从已知备份恢复主文件(单参数便捷版本)。
+        /// 会检查 .bak 与版本化备份并在找到有效备份时覆盖主文件。
+        /// </summary>
+        private static bool TryRestoreVisionProFile(string filePath)
+        {
+            try
+            {
+                string backupPath = filePath + ".bak";
+                if (File.Exists(backupPath) && ValidateVisionProFile(backupPath))
+                {
+                    File.Copy(backupPath, filePath, true);
+                    Log($"从备份恢复文件: {filePath}");
+                    return true;
+                }
+
+                // 若 .bak 不可用,尝试版本化备份
+                var latestBackup = FindLatestVisionProBackup(filePath);
+                if (latestBackup != null)
+                {
+                    File.Copy(latestBackup, filePath, true);
+                    Log($"从版本备份恢复文件: {filePath}");
+                    return true;
+                }
+
+                return false;
+            }
+            catch (Exception ex)
+            {
+                Log($"恢复文件失败: {ex.Message}");
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// 批量检查并修复目录下所有 .vpp 文件。
+        /// </summary>
+        /// <param name="directoryPath">根目录路径</param>
+        public static void CheckAndRepairDirectory(string directoryPath)
+        {
+            if (!Directory.Exists(directoryPath))
+                return;
+
+            // 查找所有 .vpp 文件(包含子目录)
+            var vppFiles = Directory.GetFiles(directoryPath, "*.vpp", SearchOption.AllDirectories);
+            Log($"开始检查和修复 {vppFiles.Length} 个VisionPro文件...");
+
+            int repairedCount = 0;
+            foreach (var file in vppFiles)
+            {
+                if (CheckAndRepairVisionProFile(file))
+                {
+                    repairedCount++;
+                }
+            }
+
+            Log($"完成检查和修复,共修复 {repairedCount} 个文件");
+        }
+        #endregion
+
+        #region 私有实现方法
+        /// <summary>
+        /// 根据配置创建备份:
+        /// - 若 KeepBackupFiles 为 true,则在指定 BackupDirectory 中创建带时间戳的版本备份并清理旧版本;
+        /// - 若为 false,则不在此方法中处理 .bak(.bak 由 File.Replace 在保存时产生)。
+        /// </summary>
+        /// <param name="originalPath">原文件路径</param>
+        private static void CreateVisionProBackup(string originalPath)
+        {
+            try
+            {
+                if (_options.KeepBackupFiles)
+                {
+                    // 在原文件目录下创建 backupDir(如果不存在)
+                    string backupDir = Path.Combine(Path.GetDirectoryName(originalPath), _options.BackupDirectory);
+                    if (!Directory.Exists(backupDir))
+                        Directory.CreateDirectory(backupDir);
+
+                    string fileName = Path.GetFileNameWithoutExtension(originalPath);
+                    string extension = Path.GetExtension(originalPath);
+                    string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
+                    string backupName = $"{fileName}_{timestamp}{extension}";
+                    string backupPath = Path.Combine(backupDir, backupName);
+
+                    File.Copy(originalPath, backupPath, true);
+                    Log($"创建VisionPro版本备份: {backupPath}");
+
+                    // 清理超过 MaxBackupVersions 的旧备份
+                    CleanupOldVisionProBackups(backupDir, fileName);
+                }
+            }
+            catch (Exception ex)
+            {
+                Log($"创建VisionPro备份失败: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// 生成一个用于临时保存的唯一文件路径(位于系统临时目录)。
+        /// </summary>
+        /// <param name="originalPath">与原文件关联的文件名(用于可读性)</param>
+        /// <returns>临时文件路径</returns>
+        private static string GetVisionProTempFilePath(string originalPath)
+        {
+            return originalPath + ".tmp";
+            //string tempDir = Path.GetTempPath();
+            //string safeName = Path.GetFileName(originalPath)
+            //    .Replace(" ", "_")
+            //    .Replace(":", "_");
+            //return Path.Combine(tempDir, $"vp_{Guid.NewGuid():N}_{safeName}");
+        }
+
+        /// <summary>
+        /// 尝试通过加载文件来验证 VisionPro 文件的完整性(加载失败视为无效)。
+        /// </summary>
+        /// <param name="filePath">要验证的文件路径</param>
+        /// <returns>若能成功加载返回 true,否则 false</returns>
+        private static bool ValidateVisionProFile(string filePath)
+        {
+            try
+            {
+                // 使用 Cognex 的序列化加载验证完整性
+                var obj = CogSerializer.LoadObjectFromFile(filePath);
+                return obj != null;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// 尝试从 .bak 备份恢复并返回加载的对象(泛型)。
+        /// 若恢复成功同时尝试将备份复制回主文件以保持一致性。
+        /// </summary>
+        private static T TryRecoverVisionProFile<T>(string originalPath) where T : class
+        {
+            string backupPath = originalPath + ".bak";
+            if (File.Exists(backupPath))
+            {
+                try
+                {
+                    var obj = CogSerializer.LoadObjectFromFile(backupPath) as T;
+                    if (obj != null)
+                    {
+                        // 当备份可用时,尝试恢复主文件
+                        TryRestoreVisionProFile(originalPath);
+                        Log($"从备份恢复VisionPro对象: {originalPath}");
+                        return obj;
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Log($"从备份恢复VisionPro对象失败: {ex.Message}");
+                }
+            }
+
+            return default;
+        }
+
+        /// <summary>
+        /// 查找版本化备份并尝试恢复为指定类型,成功时会将备份复制为主文件并返回对象。
+        /// </summary>
+        private static T TryFindAndRecoverVisionProFile<T>(string originalPath) where T : class
+        {
+            var backup = FindLatestVisionProBackup(originalPath);
+            if (backup != null)
+            {
+                try
+                {
+                    var obj = CogSerializer.LoadObjectFromFile(backup) as T;
+                    if (obj != null)
+                    {
+                        File.Copy(backup, originalPath, true);
+                        Log($"从版本备份恢复VisionPro对象: {originalPath}");
+                        return obj;
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Log($"从版本备份恢复VisionPro对象失败: {ex.Message}");
+                }
+            }
+
+            return default;
+        }
+
+        /// <summary>
+        /// 在备份目录和当前目录下查找与原文件对应的最新备份文件路径(按写入时间排序)。
+        /// 支持多种命名模式以提高鲁棒性。
+        /// </summary>
+        private static string FindLatestVisionProBackup(string originalPath)
+        {
+            string directory = Path.GetDirectoryName(originalPath);
+            string fileName = Path.GetFileNameWithoutExtension(originalPath);
+            string extension = Path.GetExtension(originalPath);
+
+            if (!Directory.Exists(directory))
+                return null;
+
+            // 优先检查专用备份目录
+            string backupDir = Path.Combine(directory, _options.BackupDirectory);
+            if (Directory.Exists(backupDir))
+            {
+                var backups = Directory.GetFiles(backupDir, $"{fileName}_*{extension}")
+                    .OrderByDescending(f => File.GetLastWriteTime(f))
+                    .ToList();
+
+                if (backups.Any())
+                    return backups.First();
+            }
+
+            // 检查当前目录中常见的备份命名模式
+            var backupPatterns = new[]
+            {
+                $"{fileName}.*.bak",
+                $"{fileName}_*{extension}",
+                $"{fileName}.backup.*"
+            };
+
+            foreach (var pattern in backupPatterns)
+            {
+                try
+                {
+                    var backups = Directory.GetFiles(directory, pattern)
+                        .OrderByDescending(f => File.GetLastWriteTime(f))
+                        .ToList();
+
+                    if (backups.Any())
+                        return backups.First();
+                }
+                catch
+                {
+                    continue;
+                }
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// 恢复主文件的重载版本(从指定备份路径复制到原文件路径)。
+        /// </summary>
+        private static void TryRestoreVisionProFile(string backupPath, string originalPath)
+        {
+            try
+            {
+                File.Copy(backupPath, originalPath, true);
+                Log($"恢复VisionPro主文件: {originalPath}");
+            }
+            catch (Exception ex)
+            {
+                Log($"恢复VisionPro主文件失败: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// 清理与指定原文件可能残留的各种临时文件(本进程或其他异常中断时产生)。
+        /// 支持单个文件名与通配符模式(在系统临时目录下)。
+        /// </summary>
+        /// <param name="originalPath">原文件路径</param>
+        private static void CleanVisionProTempFile(string originalPath)
+        {
+            // 可能的临时文件模式集合
+            var tempPatterns = new[]
+            {
+                originalPath + ".vp_tmp",
+                originalPath + ".tmp",
+                Path.GetTempPath() + $"vp_*_{Path.GetFileName(originalPath)}"
+            };
+
+            foreach (var pattern in tempPatterns)
+            {
+                try
+                {
+                    if (File.Exists(pattern))
+                    {
+                        File.Delete(pattern);
+                        Log($"清理VisionPro临时文件: {pattern}");
+                    }
+                    else if (pattern.Contains("*"))
+                    {
+                        // 通配符处理:删除系统临时目录下匹配的文件
+                        var files = Directory.GetFiles(Path.GetTempPath(), $"vp_*_{Path.GetFileName(originalPath)}");
+                        foreach (var file in files)
+                        {
+                            File.Delete(file);
+                            Log($"清理VisionPro临时文件: {file}");
+                        }
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Log($"清理VisionPro临时文件失败: {ex.Message}");
+                }
+            }
+        }
+
+        /// <summary>
+        /// 尝试从临时文件恢复到主文件(当临时文件完整且主文件缺失或临时文件更可靠时)。
+        /// 若临时文件无效则删除该临时文件。
+        /// </summary>
+        /// <param name="tempPath">临时文件路径(通常以 .vp_tmp 结尾)</param>
+        private static void TryRecoverFromVisionProTemp(string tempPath)
+        {
+            try
+            {
+                string originalPath = tempPath.Replace(".vp_tmp", "");
+
+                // 当主文件不存在或者临时文件验证通过时,尝试恢复
+                if (!File.Exists(originalPath) || ValidateVisionProFile(tempPath))
+                {
+                    if (ValidateVisionProFile(tempPath))
+                    {
+                        File.Move(tempPath, originalPath);
+                        Log($"从临时文件恢复VisionPro文件: {originalPath}");
+                    }
+                    else
+                    {
+                        File.Delete(tempPath);
+                    }
+                }
+            }
+            catch
+            {
+                SafeDelete(tempPath);
+            }
+        }
+
+        /// <summary>
+        /// 清理备份目录中过多的旧版本,保留最新的 _options.MaxBackupVersions 个版本。
+        /// </summary>
+        private static void CleanupOldVisionProBackups(string backupDir, string baseName)
+        {
+            try
+            {
+                var files = Directory.GetFiles(backupDir)
+                    .Where(f => Path.GetFileName(f).StartsWith(baseName + "_"))
+                    .OrderByDescending(f => File.GetLastWriteTime(f))
+                    .ToList();
+
+                for (int i = _options.MaxBackupVersions; i < files.Count; i++)
+                {
+                    SafeDelete(files[i]);
+                }
+            }
+            catch
+            {
+                // 忽略清理错误以避免影响主流程
+            }
+        }
+
+        /// <summary>
+        /// 安全删除文件(吞掉异常),用于清理临时/备份文件时保证不抛出影响主逻辑的异常。
+        /// </summary>
+        private static void SafeDelete(string filePath)
+        {
+            try
+            {
+                if (File.Exists(filePath))
+                    File.Delete(filePath);
+            }
+            catch
+            {
+                // 忽略删除错误
+            }
+        }
+
+        /// <summary>
+        /// 日志记录适配器(当前使用 LogHelper.WriteLogDebug)。
+        /// 可根据项目替换为其它日志实现。
+        /// </summary>
+        /// <param name="message">日志消息</param>
+        private static void Log(string message)
+        {
+            // 这里可以使用你喜欢的日志系统
+            //Console.WriteLine($"[VisionProFileHelper] {DateTime.Now:HH:mm:ss} - {message}");
+            LogHelper.WriteLogDebug($"[VisionProFileHelper] {DateTime.Now:HH:mm:ss} - {message}");
+            // 或者使用:Debug.WriteLine(message);
+            // 或者使用:Logger.Log(message);
+        }
+        #endregion
+    }
+}

+ 2 - 2
TeamAAS-VM/Properties/AssemblyInfo.cs

@@ -51,5 +51,5 @@ using System.Windows;
 //可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
 //通过使用 "*",如下所示:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.2.1.1")]
-[assembly: AssemblyFileVersion("1.2.1.1")]
+[assembly: AssemblyVersion("1.2.1.2")]
+[assembly: AssemblyFileVersion("1.2.1.2")]

+ 6 - 6
TeamAAS-VM/Services/CalibrationService.cs

@@ -130,19 +130,19 @@ namespace TeamAAS_VP.Services
             if (calib.ToolBlock != null)
             {
                 // 已有 ToolBlock,直接保存
-                CogSerializer.SaveObjectToFile(calib.ToolBlock, prcPath);
+                VisionProFileHelper.SaveObjectToFileSafe(calib.ToolBlock, prcPath);
             }
             else
             {
                 // 优先从当前目录加载已有 .vpp 文件,否则尝试从模板加载并保存
                 if (File.Exists(prcPath))
                 {
-                    calib.ToolBlock = CogSerializer.LoadObjectFromFile(prcPath) as CogToolBlock;
+                    calib.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(prcPath);
                 }
                 else if (File.Exists("..//Vision Template//Calibration.vpp"))
                 {
-                    calib.ToolBlock = CogSerializer.LoadObjectFromFile("..//Vision Template//Calibration.vpp") as CogToolBlock;
-                    CogSerializer.SaveObjectToFile(calib.ToolBlock, prcPath);
+                    calib.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>("..//Vision Template//Calibration.vpp");
+                    VisionProFileHelper.SaveObjectToFileSafe(calib.ToolBlock, prcPath);
                 }
             }
 
@@ -235,7 +235,7 @@ namespace TeamAAS_VP.Services
                             string prcPath = Path.Combine(FilePath.CalibrationPath, calib.Name, calib.Name + ".vpp");
                             if (File.Exists(prcPath))
                             {
-                                calib.ToolBlock = CogSerializer.LoadObjectFromFile(prcPath) as CogToolBlock;
+                                calib.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(prcPath);
                             }
                             _calibrations.Add(calib);
                         }
@@ -269,7 +269,7 @@ namespace TeamAAS_VP.Services
                     if (item.ToolBlock != null)
                     {
                         string prcPath = Path.Combine(folder, item.Name + ".vpp");
-                        CogSerializer.SaveObjectToFile(item.ToolBlock, prcPath);
+                        VisionProFileHelper.SaveObjectToFileSafe(item.ToolBlock, prcPath);
                     }
                 }
                 FileHelper.WriteJsonFile(list, Paths.CalibrationListFilePath);

+ 8 - 8
TeamAAS-VM/Services/ProductService.cs

@@ -460,22 +460,22 @@ namespace TeamAAS_VP.Services
                     }
                     if (item1.ToolBlock != null)
                     {
-                        CogSerializer.SaveObjectToFile(item1.ToolBlock, prcPath);
+                        VisionProFileHelper.SaveObjectToFileSafe(item1.ToolBlock, prcPath);
                     }
                     else
                     {
                         //判断路径下是否存在toolblock
                         if (File.Exists(prcPath))
                         {
-                            item1.ToolBlock = CogSerializer.LoadObjectFromFile(prcPath) as CogToolBlock;
+                            item1.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(prcPath);
                         }
                         else
                         {
                             if (File.Exists(TemplateToolBlock))
                             {
                                 //保存模板
-                                item1.ToolBlock = CogSerializer.LoadObjectFromFile(TemplateToolBlock) as CogToolBlock;
-                                CogSerializer.SaveObjectToFile(item1.ToolBlock, prcPath);
+                                item1.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(TemplateToolBlock);
+                                VisionProFileHelper.SaveObjectToFileSafe(item1.ToolBlock, prcPath);
                             }
 
                         }
@@ -526,7 +526,7 @@ namespace TeamAAS_VP.Services
                 foreach (var prc in camera.ProcedureModels)
                 {
                     string prcPath = dir + "//" + camera.Camera + "//" + prc.Name + ".vpp";
-                    prc.ToolBlock = CogSerializer.LoadObjectFromFile(prcPath) as CogToolBlock;
+                    prc.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(prcPath);
                 }
             }
 
@@ -1015,16 +1015,16 @@ namespace TeamAAS_VP.Services
             string vpppath= Path.Combine(ProductsRootPath, product.Name, cameraProcedure.Camera, prcname + ".vpp");
             if (File.Exists(vpppath))
             {
-                procedure.ToolBlock = CogSerializer.LoadObjectFromFile(vpppath) as CogToolBlock;
+                procedure.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(vpppath);
             }
             else
             {
                 //如果不存在,则加载一个模板工具
                 if (File.Exists(TemplateToolBlock))
                 {
-                    procedure.ToolBlock = CogSerializer.LoadObjectFromFile(TemplateToolBlock) as CogToolBlock;
+                    procedure.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(TemplateToolBlock);
                     //保存到指定路径
-                    CogSerializer.SaveObjectToFile(procedure.ToolBlock, vpppath);
+                    VisionProFileHelper.SaveObjectToFileSafe(procedure.ToolBlock, vpppath);
                 }
                 else
                 {

+ 1 - 0
TeamAAS-VM/TeamAAS-VP.csproj

@@ -534,6 +534,7 @@
     </Compile>
     <Compile Include="Core\RectangleCenterCalculator.cs" />
     <Compile Include="Core\StabilityAnalyzer.cs" />
+    <Compile Include="Core\VisionProFileHelper.cs" />
     <Compile Include="Events\MainTabSwitchNotification.cs" />
     <Compile Include="Interfaces\IMesService.cs" />
     <Compile Include="Models\AssemblyRecord.cs" />

+ 3 - 3
TeamAAS-VM/ViewModels/Calibration/CalibrationCameraParamsViewModel.cs

@@ -336,18 +336,18 @@ namespace TeamAAS_VP.ViewModels.Calibration
             }
             if (SelectCalibration.IsCreate && SelectCalibration.ToolBlock == null)
             {
-                SelectCalibration.ToolBlock = CogSerializer.LoadObjectFromFile(FilePath.CalibrationToolblockPath) as CogToolBlock;
+                SelectCalibration.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(FilePath.CalibrationToolblockPath);
             }
             else if (SelectCalibration.ToolBlock == null)
             {
                 string prcPath = FilePath.CalibrationPath + "//" + SelectCalibration.Name + "//" + SelectCalibration.Name + ".vpp";
                 if (File.Exists(prcPath))
                 {
-                    SelectCalibration.ToolBlock = CogSerializer.LoadObjectFromFile(prcPath) as CogToolBlock;
+                    SelectCalibration.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(prcPath);
                 }
                 else
                 {
-                    SelectCalibration.ToolBlock = CogSerializer.LoadObjectFromFile(FilePath.CalibrationToolblockPath) as CogToolBlock;
+                    SelectCalibration.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(FilePath.CalibrationToolblockPath);
                 }
             }
             if (SelectCalibration.RobotId != Guid.Empty)

+ 2 - 2
TeamAAS-VM/ViewModels/Calibration/CalibrationDistortionViewModel.cs

@@ -131,7 +131,7 @@ namespace TeamAAS_VP.ViewModels.Calibration
                 }
                 SelectCalibration.CalibCheckerboardTool = CalibCheckerboardEdit.Subject;
                 //保存校准工具
-                CogSerializer.SaveObjectToFile(CalibCheckerboardEdit.Subject, calibToolPath);
+                VisionProFileHelper.SaveObjectToFileSafe(CalibCheckerboardEdit.Subject, calibToolPath);
                 
                 NavigationParameters param = new NavigationParameters();
                 param.Add("SelectCalibration", SelectCalibration);
@@ -206,7 +206,7 @@ namespace TeamAAS_VP.ViewModels.Calibration
                 string calibToolPath = FilePath.CalibrationPath + "//" + SelectCalibration.Name + "//" + "CalibCheckerboard.vpp";
                 if (System.IO.File.Exists(calibToolPath))
                 {
-                    SelectCalibration.CalibCheckerboardTool = CogSerializer.LoadObjectFromFile(calibToolPath) as CogCalibCheckerboardTool;
+                    SelectCalibration.CalibCheckerboardTool = VisionProFileHelper.LoadObjectFromFileSafe<CogCalibCheckerboardTool>(calibToolPath);
                     CalibCheckerboardEdit.Subject = SelectCalibration.CalibCheckerboardTool;
                 }
                 else

+ 1 - 1
TeamAAS-VM/ViewModels/Calibration/CalibrationEditVisionViewModel.cs

@@ -145,7 +145,7 @@ namespace TeamAAS_VP.ViewModels.Calibration
                     Directory.CreateDirectory(Path.GetDirectoryName(toolBlockPath));
                 }
                 //保存校准工具
-                CogSerializer.SaveObjectToFile(ToolBlockEdit.Subject, toolBlockPath);
+                VisionProFileHelper.SaveObjectToFileSafe(ToolBlockEdit.Subject, toolBlockPath);
                 NavigationParameters param = new NavigationParameters();
                 param.Add("SelectCalibration", SelectCalibration);
                 _regionManager.RequestNavigate("CalibrationRegionContext", "CalibrationTeachCenterPoint", param);

+ 1 - 1
TeamAAS-VM/ViewModels/Calibration/CalibrationHomeViewModel.cs

@@ -184,7 +184,7 @@ namespace TeamAAS_VP.ViewModels.Calibration
                         WaitPhoto = 200,
                         WaitSuction = 200,
                         IsCreate = true,
-                        ToolBlock = CogSerializer.LoadObjectFromFile(FilePath.CalibrationToolblockPath) as CogToolBlock,
+                        ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(FilePath.CalibrationToolblockPath),
                     };
                     NavigationParameters param= new NavigationParameters();
                     param.Add("SelectCalibration", SelectCalibration);

+ 4 - 4
TeamAAS-VM/ViewModels/Calibration/FixedUpCameraFindP0ViewModel.cs

@@ -268,7 +268,7 @@ namespace TeamAAS_VP.ViewModels.Calibration
                 {
                     Directory.CreateDirectory(System.IO.Path.GetDirectoryName(prcPath));
                 }
-                CogSerializer.SaveObjectToFile(ToolBlockEdit.Subject, prcPath);
+                VisionProFileHelper.SaveObjectToFileSafe(ToolBlockEdit.Subject, prcPath);
                 NavigationParameters param = new NavigationParameters();
                 param.Add("SelectCalibration", SelectCalibration);
                 _regionManager.RequestNavigate("CalibrationRegionContext", "CalibrationAuto", param);
@@ -580,7 +580,7 @@ namespace TeamAAS_VP.ViewModels.Calibration
                         string prcPath = FilePath.CalibrationPath + "//" + SelectedCalibration.Name + "//" + "CalibCheckerboardTool.vpp";
                         if (File.Exists(prcPath))
                         {
-                            SelectedCalibration.CalibCheckerboardTool = CogSerializer.LoadObjectFromFile(prcPath) as Cognex.VisionPro.CalibFix.CogCalibCheckerboardTool;
+                            SelectedCalibration.CalibCheckerboardTool = VisionProFileHelper.LoadObjectFromFileSafe<Cognex.VisionPro.CalibFix.CogCalibCheckerboardTool>(prcPath);
                             SelectedCalibration.CalibCheckerboardTool.InputImage = image;
                             SelectedCalibration.CalibCheckerboardTool.Run();
                             image = SelectedCalibration.CalibCheckerboardTool.OutputImage;
@@ -643,11 +643,11 @@ namespace TeamAAS_VP.ViewModels.Calibration
             string prcPath = FilePath.CalibrationPath + "//" + SelectCalibration.Name + "//" + "FixedUpCameraTool.vpp";
             if (File.Exists(prcPath))
             {
-                ToolBlockEdit.Subject = CogSerializer.LoadObjectFromFile(prcPath) as CogToolBlock;
+                ToolBlockEdit.Subject = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(prcPath);
             }
             else
             {
-                ToolBlockEdit.Subject = CogSerializer.LoadObjectFromFile(FilePath.CalibrationToolblockPath) as CogToolBlock;
+                ToolBlockEdit.Subject = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(FilePath.CalibrationToolblockPath);
             }
 
             if (SelectCalibration.RobotId != Guid.Empty)

+ 3 - 3
TeamAAS-VM/ViewModels/Product/ProcessBasicsViewModel.cs

@@ -316,18 +316,18 @@ namespace TeamAAS_VP.ViewModels.Product
 
                 //if (SelectProduct.IsCreate && SelectProcedure.ToolBlock == null)
                 //{
-                //    SelectProcedure.ToolBlock = CogSerializer.LoadObjectFromFile(FilePath.SimpleProcedurePath) as CogToolBlock; 
+                //    SelectProcedure.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(FilePath.SimpleProcedurePath) as CogToolBlock; 
                 //}
                 //else if (SelectProcedure.ToolBlock == null)
                 //{
                 //    string prcPath = FilePath.ProductsPath + "//" + SelectProduct.Name + "//" + SelectProcedure.CameraName + "//" + SelectProcedure.Name + ".vpp";
                 //    if (File.Exists(prcPath))
                 //    {
-                //        SelectProcedure.ToolBlock = CogSerializer.LoadObjectFromFile(prcPath) as CogToolBlock;
+                //        SelectProcedure.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(prcPath) as CogToolBlock;
                 //    }
                 //    else
                 //    {
-                //        SelectProcedure.ToolBlock = CogSerializer.LoadObjectFromFile(FilePath.SimpleProcedurePath) as CogToolBlock;
+                //        SelectProcedure.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(FilePath.SimpleProcedurePath) as CogToolBlock;
                 //    }
                 //}
 

+ 2 - 2
TeamAAS-VM/ViewModels/Product/ProductProcessManageViewModel.cs

@@ -217,7 +217,7 @@ namespace TeamAAS_VP.ViewModels.Product
                                 IsExcludeNearPoint=false,
                                 ExcludeNearPointThreshol=1,
                                 OutputSeparator=";",
-                                ToolBlock = CogSerializer.LoadObjectFromFile(FilePath.SimpleProcedurePath) as CogToolBlock,
+                                ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(FilePath.SimpleProcedurePath),
                                 FeederFindFrequency = new FeederFrequencyModel(11)
                             };
                             var waiting = new WaitingControl();
@@ -225,7 +225,7 @@ namespace TeamAAS_VP.ViewModels.Product
                             var task = DialogHost.Show(waiting, "RootDialog", null, null, null);
                             await Task.Run(() =>
                             {
-                                procedureModel.ToolBlock = CogSerializer.LoadObjectFromFile(ProductService.TemplateToolBlock) as CogToolBlock;
+                                procedureModel.ToolBlock = VisionProFileHelper.LoadObjectFromFileSafe<CogToolBlock>(ProductService.TemplateToolBlock);
                             });
                             if (DialogHost.IsDialogOpen("RootDialog"))
                             {