using AIK.Common.Event; using AIK.Common.Interface; using AIK.Common.Language; using AIK.Common.SysCommon; using AIK.Models.ApiModels.CloudData; using AIK.Models.HidModels; using AIK.Service.Extensions; using AIK.Service.IService; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; namespace AIK.ViewModels.CloudData { public partial class CloudDataViewModel : ObservableObject { private readonly ISettingsService _settingsService; private readonly IConfigurationService _configurationService; private readonly IDataCacheService _dataCacheService; private readonly IModalDialogService _modalDialogService; private readonly IAikDataService _aikDataService; private readonly IFileServiceFactory _fileServiceFactory; private readonly IFileCacheService _fileCacheService; private readonly IWindowService _windowService; private readonly IHidCommunicationService _hidCommunicationService; private readonly IFileDialogService _fileDialogService; private readonly IFileOperationService _fileOperationService; private readonly ILogger _logger; [ObservableProperty] private HidDeviceStatus _hidDeviceStatus = new HidDeviceStatus(); [ObservableProperty] private string diskDrive; [ObservableProperty] private string _statusMessage = string.Empty; [ObservableProperty] private bool _isShowStatusMessage = true; private ApiResponseOssData ossdataResult { get; set; } public CloudDataViewModel(ISettingsService settingsService, IConfigurationService configurationService, IDataCacheService dataCacheService, IModalDialogService modalDialogService, IAikDataService aikDataService, IFileServiceFactory fileServiceFactory, IWindowService windowService, IHidCommunicationService hidCommunicationService, IFileDialogService fileDialogService, IFileOperationService fileOperationService, ILogger logger) { _settingsService = settingsService; _configurationService = configurationService; _dataCacheService = dataCacheService; _modalDialogService = modalDialogService; _aikDataService = aikDataService; _fileServiceFactory = fileServiceFactory; _windowService = windowService; _hidCommunicationService = hidCommunicationService; _hidCommunicationService.ConnectionStateChanged += OnConnectionStateChanged; _fileDialogService = fileDialogService; _fileCacheService = fileServiceFactory.GetService(Common.Enum.FileServiceType.CloudData); _fileOperationService = fileOperationService; _logger = logger; } #region 页面方法 [RelayCommand] private async Task SelectFileAsync() { try { StatusMessage = "正在选择文件..."; var filePath = await _fileDialogService.OpenFileAsync("选择要复制的文件"); if (filePath != null) { await _modalDialogService.ShowProgressAsync( async (progress, ct) => { return await _hidCommunicationService.CopyToUdiskAsync(filePath, isDirectory: false, progress, ct); }); //await _hidCommunicationService.CopyToUdiskAsync(filePath, isDirectory: false, cancellationToken); StatusMessage = "✅ 文件复制成功!"; //MessageBox.Show("文件已复制到 HID U盘!", "成功", MessageBoxButton.OK, MessageBoxImage.Information); } } catch (Exception ex) { StatusMessage = "❌ 操作失败"; MessageBox.Show($"错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } [RelayCommand] private async Task SelectFolderAsync() { try { StatusMessage = "正在选择文件夹..."; var folderPath = await _fileDialogService.SelectFolderAsync("请选择要复制的文件夹"); if (folderPath != null) { await _modalDialogService.ShowProgressAsync( async (progress, ct) => { return await _hidCommunicationService.CopyToUdiskAsync(folderPath, isDirectory: true, progress, ct); }); StatusMessage = "✅ 文件夹复制成功!"; //MessageBox.Show("文件夹已复制到 HID U盘!", "成功", MessageBoxButton.OK, MessageBoxImage.Information); } } catch (Exception ex) { StatusMessage = "❌ 操作失败"; MessageBox.Show($"错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } [RelayCommand] private async Task SyncCloudDataAsync() { try { await GetCloudDataCacheAsync(); } catch (Exception ex) { _logger.LogError(ex, "云数据同步失败"); StatusMessage = "❌ 操作失败"; MessageBox.Show($"错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } [RelayCommand] private async Task OpenUDiskAsync() { try { await _hidCommunicationService.OpenUDiskAndDetectDriveAsync(); } catch (Exception ex) { StatusMessage = "❌ 操作失败"; MessageBox.Show($"错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } [RelayCommand] private async Task CloseUDiskAsync() { try { if (string.IsNullOrEmpty(DiskDrive)) { return; } await _hidCommunicationService.CloseUDiskSafeDriveAsync(DiskDrive); } catch (Exception ex) { StatusMessage = "❌ 操作失败"; MessageBox.Show($"错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } [RelayCommand] private async Task KeepDeviceAliveAsync() { await _hidCommunicationService.KeepDeviceAliveAsync(); } #endregion #region 辅助方法 private async Task GetDiskDrive() { IsShowStatusMessage = true; StatusMessage = AiKLanguage.LanguageKey.GetValueOrDefault("CloudDataDeviceNotFound") ?? string.Empty; if (!string.IsNullOrEmpty(DiskDrive)) { IsShowStatusMessage = false; StatusMessage = AiKLanguage.LanguageKey.GetValueOrDefault("CloudDataInitCompleted") ?? string.Empty; return; } DiskDrive = await _hidCommunicationService.FindAssociatedUsbDisk(); } #endregion #region 连接状态事件处理 private void OnConnectionStateChanged(object? sender, ConnectionStateChangedEventArgs e) { HidDeviceStatus.DeviceStatus = e.IsConnected; if (e.IsConnected) { HidDeviceStatus.DeviceStatusText = AiKLanguage.LanguageKey.GetValueOrDefault("Connected"); HidDeviceStatus.DeviceStatusColor = "#28a745"; HidDeviceStatus.DeviceStatusIcon = "\uE62a"; _ = GetDiskDrive(); } else { HidDeviceStatus.DeviceStatusText = AiKLanguage.LanguageKey.GetValueOrDefault("Disconnected"); ; HidDeviceStatus.DeviceStatusColor = "#FFF44336"; HidDeviceStatus.DeviceStatusIcon = "\uE6f9"; DiskDrive = ""; StatusMessage = AiKLanguage.LanguageKey.GetValueOrDefault("CloudDataDeviceNotFound") ?? string.Empty; IsShowStatusMessage = true; } } #endregion #region 获取云数据缓存并同步数据到U盘 private async Task GetCloudDataCacheAsync() { try { var usertoken = await _dataCacheService.GetLatestUserTokenAsync(); ossdataResult = await _aikDataService.GetOssDataCache(usertoken, AIK.Common.SysCommon.WebApiAddress.OssDataCache); var progress = new Progress(); if (ossdataResult != null && ossdataResult.Data.Count > 0) { _logger.LogInformation("开始同步云数据,共 {Count} 个数据包", ossdataResult.Data.Count); await _modalDialogService.ShowProgressAsync( async (progress, ct) => { return await DeployPackagesWorkflowAsync(ossdataResult.Data, progress, CancellationToken.None); }); } else { _logger.LogWarning("获取云数据缓存为空,无数据可同步"); } } catch (Exception ex) { _logger.LogError(ex, "获取云数据缓存失败"); } } public async Task<(bool, List downloadedPaths)> DownloadAllPackagesSimpleAsync( IReadOnlyList packages, IProgress segmentProgress, int maxConcurrency = 2, CancellationToken cancellationToken = default) { if (packages.Count == 0) { segmentProgress?.Report(100.0); return (true, new List()); } var semaphore = new SemaphoreSlim(maxConcurrency); var tasks = new List>(); int completedCount = 0; object lockObj = new object(); int totalCount = packages.Count; foreach (var item in packages) { tasks.Add(Task.Run(async () => { await semaphore.WaitAsync(cancellationToken); try { var fileProgress = new Progress(_ => { // 单个文件内部不细算,只按完成数算整体进度 }); var (ok, path) = await _fileCacheService.DownloadFileWithProgressAsync( item.EsaUrl, item.Id.ToString(), fileProgress, cancellationToken); if (!ok) { _logger.LogError("云数据包下载失败: {PackageName}, Id: {PackageId}, URL: {PackageUrl}", item.Name, item.Id, item.EsaUrl); throw new Exception($"{AiKLanguage.LanguageKey.GetValueOrDefault("CloudDataDownloadFailed") ?? string.Empty}: {item.Name}"); } lock (lockObj) { completedCount++; double progress = (double)completedCount / totalCount * 100.0; segmentProgress.Report(progress); } return path; } catch (Exception ex) { _logger.LogError(ex, "云数据包下载异常: {PackageName}, Id: {PackageId}, URL: {PackageUrl}", item.Name, item.Id, item.EsaUrl); throw; } finally { semaphore.Release(); } }, cancellationToken)); } try { var downloadedPaths = await Task.WhenAll(tasks); segmentProgress?.Report(100.0); _logger.LogInformation("云数据包下载完成: {SuccessCount}/{TotalCount}", totalCount, totalCount); return (true, downloadedPaths.ToList()); } catch (Exception ex) { _logger.LogError(ex, "云数据包下载批量失败,共 {TotalCount} 个包", totalCount); return (false, new List()); } } #region 小文件拷贝版 // public async Task<(bool success, string message)> DeployPackagesWorkflowAsync( //IReadOnlyList packages, //IProgress progress, //CancellationToken cancellationToken) // { // try // { // // 进度分段(流畅不卡) // const double downloadStart = 0.0; // const double downloadEnd = 20.0; // const double extractStart = 20.0; // const double extractEnd = 30.0; // const double copyStart = 30.0; // const double copyEnd = 100.0; // // ========== 1. 下载:0% → 20% ========== // var downloadProgress = new Progress(p => // { // double globalProgress = downloadStart + (p / 100.0) * (downloadEnd - downloadStart); // progress.Report(globalProgress); // }); // var (dlSuccess, downloadedPaths) = await DownloadAllPackagesSimpleAsync( // packages, downloadProgress, 3, cancellationToken); // if (!dlSuccess) // return (false, "下载失败"); // // ========== 临时目录 ========== // string shortId = Path.GetRandomFileName().Replace(".", ""); // string tempRoot = Path.Combine(Path.GetTempPath(), $"K3_{shortId}"); // string tempDir = Path.Combine(tempRoot, ComStringHelper.K3ToolDirectory); // Directory.CreateDirectory(tempDir); // try // { // // ========== 2. 解压:20% → 30% ========== // var extractProgress = new Progress(p => // { // double globalProgress = extractStart + (p / 100.0) * (extractEnd - extractStart); // progress.Report(globalProgress); // }); // await _fileOperationService.ExtractMultipleArchivesAsync( // downloadedPaths, tempDir, extractProgress,true, cancellationToken); // // ========== 3. 拷贝到U盘:30% → 100% ========== // var copyProgress = new Progress(p => // { // double globalProgress = copyStart + (p / 100.0) * (copyEnd - copyStart); // progress.Report(globalProgress); // }); // var (copySuccess, copyMsg) = await _hidCommunicationService.CopyToUdiskAsync( // tempDir, true, copyProgress, cancellationToken); // return (copySuccess, copyMsg); // } // finally // { // try { Directory.Delete(tempRoot, true); } catch { } // } // } // catch (Exception ex) // { // return (false, $"操作失败:{ex.Message}"); // } // } #endregion #region 单文件压缩解压版 public async Task<(bool success, string message)> DeployPackagesWorkflowAsync( IReadOnlyList packages, IProgress progress, CancellationToken cancellationToken) { var sw = Stopwatch.StartNew(); try { // 最快最合理的进度 const double download = 25; const double extractLocal = 15; const double scanDiff = 10; const double writeToUdisk = 50; // 1. 下载多个 ZIP(并发 2:用户网络较慢(6-8 Mbps),降低并发使每个连接更稳定、减少中途断线) progress.Report(0); var (dlOk, zipPaths) = await DownloadAllPackagesSimpleAsync(packages, new Progress(p => progress.Report(p * download / 100)), 2, cancellationToken); if (!dlOk) { _logger.LogError("云数据同步下载阶段失败,共 {Count} 个包", packages.Count); return (false, AiKLanguage.LanguageKey.GetValueOrDefault("CloudDataDownloadFailed") ?? string.Empty); } // 2. 【本地快速解压】所有 ZIP → 一个完整文件夹 string tempFolder = Path.Combine(Path.GetTempPath(), $"Full_{Guid.NewGuid():N}"); string localTarget = Path.Combine(tempFolder, ComStringHelper.K3ToolDirectory); Directory.CreateDirectory(localTarget); try { // 多压缩包合并解压 await _fileOperationService.ExtractMultipleArchivesAsync( zipPaths, localTarget, new Progress(p => progress.Report(download + p * extractLocal / 100)), true, cancellationToken); progress.Report(download + extractLocal + 5); // ============================== // ✅ 3. 差异对比 + 只写变化文件 // ============================== await _hidCommunicationService.CopyDeltaDirectoryToUdiskAsync( localTarget, new Progress(p => progress.Report(download + extractLocal + scanDiff + p * writeToUdisk / 100)), cancellationToken); progress.Report(100); sw.Stop(); _logger.LogInformation("云数据同步完成,共 {Count} 个包,耗时 {Elapsed}", packages.Count, sw.Elapsed); return (true, AiKLanguage.LanguageKey.GetValueOrDefault("CloudDataSyncCompleted") ?? string.Empty); } finally { // 清理本地临时文件 try { Directory.Delete(tempFolder, true); } catch { } try { await _hidCommunicationService.CloseUDiskSafeDriveAsync(); } catch { } } } catch (Exception ex) { _logger.LogError(ex, "云数据同步部署工作流失败"); return (false, $"{AiKLanguage.LanguageKey.GetValueOrDefault("CloudDataSyncFailed") ?? string.Empty}:{ex.Message}"); } } #endregion #endregion } }