You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
456 lines
20 KiB
456 lines
20 KiB
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<CloudDataViewModel> _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<CloudDataViewModel> 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<double>();
|
|
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<string> downloadedPaths)> DownloadAllPackagesSimpleAsync(
|
|
IReadOnlyList<CloudPackageInfo> packages,
|
|
IProgress<double> segmentProgress,
|
|
int maxConcurrency = 2,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (packages.Count == 0)
|
|
{
|
|
segmentProgress?.Report(100.0);
|
|
return (true, new List<string>());
|
|
}
|
|
|
|
var semaphore = new SemaphoreSlim(maxConcurrency);
|
|
var tasks = new List<Task<string>>();
|
|
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<double>(_ =>
|
|
{
|
|
// 单个文件内部不细算,只按完成数算整体进度
|
|
});
|
|
|
|
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<string>());
|
|
}
|
|
}
|
|
|
|
#region 小文件拷贝版
|
|
// public async Task<(bool success, string message)> DeployPackagesWorkflowAsync(
|
|
//IReadOnlyList<CloudPackageInfo> packages,
|
|
//IProgress<double> 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<double>(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<double>(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<double>(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<CloudPackageInfo> packages,
|
|
IProgress<double> 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<double>(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<double>(p => progress.Report(download + p * extractLocal / 100)),
|
|
true,
|
|
cancellationToken);
|
|
|
|
progress.Report(download + extractLocal + 5);
|
|
|
|
// ==============================
|
|
// ✅ 3. 差异对比 + 只写变化文件
|
|
// ==============================
|
|
await _hidCommunicationService.CopyDeltaDirectoryToUdiskAsync(
|
|
localTarget,
|
|
new Progress<double>(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
|
|
}
|
|
}
|
|
|