feat: Luban.Core初步实现,提供能力:

- RegistService 装饰器实现服务注册
- 下载服务
- 文件服务
- 多线程方法
- 解压缩服务
This commit is contained in:
jackqqq123
2025-09-24 12:07:03 +08:00
parent 6ec2a804ba
commit 369fcbf403
29 changed files with 2024 additions and 11 deletions

View File

@@ -3,6 +3,52 @@ applyTo: '**'
---
- 说中文
- 这是一个基于Avalonia的项目管理工程目标是让非程序员也能轻松使用luban管理配置表。
- 命令行是PowerShell使用分号作为命令分隔符
## 架构说明
### 🏗️ LubanHub.Core 核心能力库
LubanHub.Core 提供了项目的底层核心能力,**优先使用这些服务**来实现功能,避免重复造轮子:
#### 📥 下载服务 (ICoreDownloadService)
- **默认下载目录**: 系统用户数据目录的LubanHub子目录 (`%AppData%\LubanHub\Downloads`)
- **目录管理**: 获取/设置下载目录
- **进度追踪**: 实时下载进度、速度、文件名显示
- **取消支持**: 支持CancellationToken取消下载
- **使用场景**: Luban版本下载、模板下载、资源文件下载
#### 📁 文件管理服务 (ICoreFileService)
- **文件操作**: 创建、删除、复制、移动文件
- **目录操作**: 创建、删除、遍历目录
- **异步读写**: ReadAllTextAsync/WriteAllTextAsync
- **信息获取**: FileInfo/DirectoryInfo
- **使用场景**: 配置文件操作、项目文件管理、模板文件处理
#### ⚙️ 进程调用服务 (ICoreProcessService)
- **同步执行**: 等待进程完成并获取输出
- **异步执行**: 实时获取stdout/stderr输出
- **后台进程**: 启动但不等待的进程
- **进程管理**: 检查进程状态、杀死进程
- **使用场景**: 调用Luban命令行、Git操作、外部工具集成
#### 🗜️ 解压缩服务 (ICoreCompressionService)
- **ZIP支持**: 完整的ZIP解压缩功能
- **进度追踪**: 解压进度回调
- **格式检测**: 自动识别压缩格式
- **扩展支持**: 预留RAR/7Z接口需第三方库
- **使用场景**: Luban安装包解压、模板包解压、资源包处理
#### 🔧 依赖注入集成
```csharp
// 在App.axaml.cs中已配置
services.AddCoreServices(); // 自动注册所有Core服务
```
#### ⚠️ 扩展原则
- **优先使用**: 实现新功能时首先检查Core是否已提供相关服务
- **必要扩展**: 只有在Core无法满足需求时才考虑扩展Core
- **接口设计**: 新增Core功能需要定义接口保持架构一致性
- **日志集成**: 所有Core服务已集成Microsoft.Extensions.Logging
## UI设计规范
@@ -62,3 +108,40 @@ applyTo: '**'
- 输入框支持焦点状态
- 列表项支持选中和悬停状态
- 过渡动画让交互更流畅
## 开发指导
### 📋 功能实现流程
1. **需求分析**: 明确功能需求和用户场景
2. **Core检查**: 检查LubanHub.Core是否已提供相关服务
3. **服务利用**: 优先使用Core服务实现功能逻辑
4. **UI设计**: 按照设计规范创建用户界面
5. **进度显示**: 长时间操作使用底部进度条显示状态
6. **错误处理**: 合理的异常处理和用户提示
### 🎯 常见场景示例
- **文件下载**: 使用ICoreDownloadService进度显示在底部
- **Luban调用**: 使用ICoreProcessService执行命令行
- **配置管理**: 使用ICoreFileService读写配置文件
- **安装包处理**: 使用ICoreCompressionService解压
- **目录选择**: 集成系统文件对话框需要时扩展Core
### 🔄 服务扩展指南
当需要新功能且Core无法满足时
1.`LubanHub.Core/Interfaces`添加接口定义
2.`LubanHub.Core/Services`添加实现类
3.`ServiceCollectionExtensions`中注册服务
4. 更新本文档说明新增能力
5. 在App层通过依赖注入使用新服务
### 📊 UI状态管理
- **ViewModelBase**: 所有ViewModel继承此基类
- **属性绑定**: 使用SetProperty方法通知UI更新
- **命令模式**: 使用RelayCommand处理用户交互
- **进度显示**: 使用DownloadProgressViewModel显示长时间操作
### 🎨 主题和样式
- **主题切换**: 使用ThemeManager管理深色/浅色主题
- **资源绑定**: 所有颜色使用DynamicResource绑定
- **样式复用**: 在Styles.axaml中定义通用样式
- **响应式**: 支持运行时主题切换

View File

@@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{06401A04-D86
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.App", "src\LubanHub.App\LubanHub.App.csproj", "{9A66E728-EA8A-4644-9CC2-2C056479AF5A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LubanHub.Core", "src\LubanHub.Core\LubanHub.Core.csproj", "{B8F5E9A2-1234-4567-890A-BCDEF0123456}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -20,8 +22,13 @@ Global
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A66E728-EA8A-4644-9CC2-2C056479AF5A}.Release|Any CPU.Build.0 = Release|Any CPU
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8F5E9A2-1234-4567-890A-BCDEF0123456}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9A66E728-EA8A-4644-9CC2-2C056479AF5A} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
{B8F5E9A2-1234-4567-890A-BCDEF0123456} = {06401A04-D861-4FAC-988F-C06E2D5AC553}
EndGlobalSection
EndGlobal

View File

@@ -82,9 +82,37 @@ UI层
---
## 五、开发建议
## 五、架构原则
### 1. 服务注册原则
- **优先使用装饰器注册服务**:所有服务类使用 `[RegistService]` 特性标记,实现自动发现和注册
- **避免手工注册**:减少在 DI 容器中手动添加服务,降低耦合度和维护成本
- **支持生命周期配置**通过装饰器参数指定服务生命周期Singleton/Scoped/Transient
### 2. 依赖方向原则
- **LubanHub.Core 不能引用任何其他服务**作为基础能力库Core 必须保持纯净,只能依赖系统库和第三方基础库
- **其他服务可以引用 Core**:业务服务层可以依赖 Core 提供的基础能力
- **业务服务间避免相互引用**:各业务服务应保持独立,通过事件或接口解耦
### 3. 接口设计原则
- **Core 服务必须定义接口**:如 `ICoreFileService``ICoreDownloadService`
- **业务服务建议定义接口**:便于测试和扩展
- **接口与实现分离**:接口放在 Interfaces 目录,实现放在 Services 目录
### 4. 装饰器使用示例
```csharp
[RegistService(ServiceLifetime.Singleton, typeof(ICoreFileService))]
public class CoreFileService : ICoreFileService
{
// 服务实现
}
```
---
## 六、开发建议
- 每个服务建议定义接口(如 IFileService便于测试和扩展
- Core层实现时注意跨平台兼容
- 业务服务只依赖Core不直接操作系统API
- UI层只做展示和交互所有逻辑下沉到服务
- 使用装饰器模式实现服务的自动发现和注册

View File

@@ -1,17 +1,29 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using LubanHub.App.Services;
using LubanHub.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace LubanHub.App;
public partial class App : Application
{
public static ServiceProvider? ServiceProvider { get; private set; }
public override void Initialize()
{
try
{
Console.WriteLine("正在初始化应用程序...");
// 配置服务
var services = new ServiceCollection();
ConfigureServices(services);
ServiceProvider = services.BuildServiceProvider();
AvaloniaXamlLoader.Load(this);
Console.WriteLine("XAML加载完成。");
}
@@ -23,6 +35,27 @@ public partial class App : Application
}
}
private static void ConfigureServices(ServiceCollection services)
{
// 添加日志
services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Debug);
});
// 添加Core服务
services.AddCoreServices();
// 添加App层服务
services.AddSingleton<IDownloadProgressService, DownloadProgressService>();
services.AddSingleton<IAppDownloadService, AppDownloadService>();
services.AddSingleton<IExampleModuleService, ExampleModuleService>();
// 添加ViewModels等其他服务
// services.AddSingleton<MainWindowViewModel>();
}
public override void OnFrameworkInitializationCompleted()
{
try
@@ -31,6 +64,7 @@ public partial class App : Application
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
desktop.Exit += (s, e) => ServiceProvider?.Dispose();
Console.WriteLine("主窗口已创建。");
}

View File

@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
<RootNamespace>LubanHub.App</RootNamespace>
<AssemblyName>LubanHub.App</AssemblyName>
</PropertyGroup>
@@ -15,10 +15,17 @@
<PackageReference Include="Avalonia.Desktop" Version="11.3.6" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.6">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LubanHub.Core\LubanHub.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -2,12 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:LubanHub.App.Views"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="LubanHub.App.MainWindow"
Title="LubanHub"
MinWidth="1000" MinHeight="600">
<Grid>
<Grid RowDefinitions="*,Auto">
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
@@ -227,7 +229,7 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Text="C:\Users\Downloads\LubanHub"
Name="DownloadDirectoryTextBox"
Background="{DynamicResource InputBackgroundBrush}"
Foreground="{DynamicResource PrimaryTextBrush}"
BorderBrush="{DynamicResource BorderBrush}"
@@ -235,8 +237,13 @@
<Button Grid.Column="1"
Content="浏览..."
Classes="primary"
Padding="{DynamicResource ButtonPadding}"/>
Padding="{DynamicResource ButtonPadding}"
Click="OnBrowseDownloadDirectoryClick"/>
</Grid>
<Button Content="🧪 测试其他模块下载"
Margin="0,10,0,0"
Padding="10,5"
Click="OnTestModuleDownloadClick"/>
</StackPanel>
<StackPanel>
@@ -260,4 +267,8 @@
</Grid>
</Border>
</Grid>
<!-- 底部下载进度区域 -->
<views:DownloadProgressView Grid.Row="1" DataContext="{Binding DownloadProgress}" />
</Grid>
</Window>

View File

@@ -2,6 +2,10 @@ using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Controls.Selection;
using LubanHub.App.Services;
using LubanHub.App.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using LubanHub.Core.Interfaces;
using System;
namespace LubanHub.App;
@@ -19,11 +23,15 @@ public partial class MainWindow : Window
private Button? _installButton;
private Button? _settingsButton;
private ComboBox? _themeComboBox;
private TextBox? _downloadDirectoryTextBox;
private MainWindowViewModel? _viewModel;
public MainWindow()
{
InitializeComponent();
InitializeComponents();
InitializeViewModel();
// 订阅主题变化事件
ThemeManager.ThemeChanged += OnThemeChanged;
@@ -33,6 +41,31 @@ public partial class MainWindow : Window
UpdateThemeComboBox();
}
private void InitializeViewModel()
{
try
{
_viewModel = new MainWindowViewModel();
DataContext = _viewModel;
// 更新下载目录文本框
UpdateDownloadDirectoryTextBox();
}
catch (Exception ex)
{
// 如果ViewModel初始化失败记录错误但不中断应用启动
Console.WriteLine($"初始化ViewModel时出错: {ex.Message}");
}
}
private void UpdateDownloadDirectoryTextBox()
{
if (_downloadDirectoryTextBox != null && _viewModel != null)
{
_downloadDirectoryTextBox.Text = _viewModel.DownloadDirectory;
}
}
private void InitializeComponents()
{
// 获取面板引用
@@ -48,7 +81,10 @@ public partial class MainWindow : Window
_projectButton = this.FindControl<Button>("ProjectButton");
_installButton = this.FindControl<Button>("InstallButton");
_settingsButton = this.FindControl<Button>("SettingsButton");
// 获取其他控件引用
_themeComboBox = this.FindControl<ComboBox>("ThemeComboBox");
_downloadDirectoryTextBox = this.FindControl<TextBox>("DownloadDirectoryTextBox");
}
private void ShowPanel(Grid? targetPanel)
@@ -130,4 +166,36 @@ public partial class MainWindow : Window
_themeComboBox.SelectedIndex = ThemeManager.CurrentTheme == ThemeVariant.Dark ? 0 : 1;
}
}
private void OnBrowseDownloadDirectoryClick(object? sender, RoutedEventArgs e)
{
try
{
// 简单的目录选择实现,实际项目中应该使用文件对话框
if (_viewModel != null)
{
_viewModel.ChangeDownloadDirectoryCommand.Execute(null);
UpdateDownloadDirectoryTextBox();
}
}
catch (Exception ex)
{
Console.WriteLine($"浏览下载目录时出错: {ex.Message}");
}
}
private void OnTestModuleDownloadClick(object? sender, RoutedEventArgs e)
{
try
{
if (_viewModel != null)
{
_viewModel.TestModuleDownloadCommand.Execute(null);
}
}
catch (Exception ex)
{
Console.WriteLine($"测试模块下载时出错: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,118 @@
using LubanHub.Core.Interfaces;
using LubanHub.Core.Models;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LubanHub.App.Services;
/// <summary>
/// 应用层下载服务接口
/// </summary>
public interface IAppDownloadService
{
/// <summary>
/// 下载文件并自动显示进度
/// </summary>
Task<string> DownloadFileAsync(string url, string? fileName = null, CancellationToken cancellationToken = default);
/// <summary>
/// 下载文件到指定目录并自动显示进度
/// </summary>
Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, CancellationToken cancellationToken = default);
/// <summary>
/// 获取当前下载目录
/// </summary>
string GetDownloadDirectory();
/// <summary>
/// 设置下载目录
/// </summary>
void SetDownloadDirectory(string path);
}
/// <summary>
/// 应用层下载服务实现
/// </summary>
public class AppDownloadService : IAppDownloadService
{
private readonly ICoreDownloadService _coreDownloadService;
private readonly IDownloadProgressService _progressService;
private readonly ILogger<AppDownloadService> _logger;
public AppDownloadService(
ICoreDownloadService coreDownloadService,
IDownloadProgressService progressService,
ILogger<AppDownloadService> logger)
{
_coreDownloadService = coreDownloadService;
_progressService = progressService;
_logger = logger;
}
public async Task<string> DownloadFileAsync(string url, string? fileName = null, CancellationToken cancellationToken = default)
{
var progress = new Progress<DownloadProgressInfo>(progressInfo =>
{
_progressService.ReportProgress(progressInfo);
});
try
{
_logger.LogInformation("开始下载文件: {Url}", url);
var filePath = await _coreDownloadService.DownloadFileAsync(url, fileName, progress, cancellationToken);
// 下载完成后延迟隐藏进度条
_ = Task.Run(async () =>
{
await Task.Delay(2000, CancellationToken.None); // 2秒后隐藏
_progressService.HideProgress();
});
_logger.LogInformation("文件下载完成: {FilePath}", filePath);
return filePath;
}
catch (Exception ex)
{
_logger.LogError(ex, "下载文件时出错: {Url}", url);
_progressService.HideProgress();
throw;
}
}
public async Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, CancellationToken cancellationToken = default)
{
var progress = new Progress<DownloadProgressInfo>(progressInfo =>
{
_progressService.ReportProgress(progressInfo);
});
try
{
_logger.LogInformation("开始下载文件到指定目录: {Url} -> {Directory}", url, targetDirectory);
var filePath = await _coreDownloadService.DownloadFileAsync(url, targetDirectory, fileName, progress, cancellationToken);
// 下载完成后延迟隐藏进度条
_ = Task.Run(async () =>
{
await Task.Delay(2000, CancellationToken.None); // 2秒后隐藏
_progressService.HideProgress();
});
_logger.LogInformation("文件下载完成: {FilePath}", filePath);
return filePath;
}
catch (Exception ex)
{
_logger.LogError(ex, "下载文件时出错: {Url} -> {Directory}", url, targetDirectory);
_progressService.HideProgress();
throw;
}
}
public string GetDownloadDirectory() => _coreDownloadService.GetDownloadDirectory();
public void SetDownloadDirectory(string path) => _coreDownloadService.SetDownloadDirectory(path);
}

View File

@@ -0,0 +1,28 @@
using LubanHub.Core.Models;
using System;
namespace LubanHub.App.Services;
/// <summary>
/// 全局下载进度服务实现
/// </summary>
public class DownloadProgressService : IDownloadProgressService
{
public event Action<DownloadProgressInfo>? ProgressChanged;
public void ReportProgress(DownloadProgressInfo progressInfo)
{
ProgressChanged?.Invoke(progressInfo);
}
public void HideProgress()
{
var hideInfo = new DownloadProgressInfo
{
IsCompleted = true,
Progress = 0,
FileName = string.Empty
};
ProgressChanged?.Invoke(hideInfo);
}
}

View File

@@ -0,0 +1,55 @@
using LubanHub.App.Services;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LubanHub.App.Services;
/// <summary>
/// 示例模块服务,演示如何在其他模块中使用下载功能
/// </summary>
public interface IExampleModuleService
{
/// <summary>
/// 下载Luban示例文件
/// </summary>
Task DownloadLubanExampleAsync();
}
/// <summary>
/// 示例模块服务实现
/// </summary>
public class ExampleModuleService : IExampleModuleService
{
private readonly IAppDownloadService _downloadService;
private readonly ILogger<ExampleModuleService> _logger;
public ExampleModuleService(IAppDownloadService downloadService, ILogger<ExampleModuleService> logger)
{
_downloadService = downloadService;
_logger = logger;
}
public async Task DownloadLubanExampleAsync()
{
try
{
_logger.LogInformation("示例模块开始下载Luban示例文件");
// 模拟下载一个示例文件
var testUrl = "https://httpbin.org/json";
var fileName = "luban_example.json";
// 调用下载服务进度会自动显示在UI上
var filePath = await _downloadService.DownloadFileAsync(testUrl, fileName, CancellationToken.None);
_logger.LogInformation("示例模块下载完成: {FilePath}", filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "示例模块下载失败");
throw;
}
}
}

View File

@@ -0,0 +1,25 @@
using LubanHub.Core.Models;
using System;
namespace LubanHub.App.Services;
/// <summary>
/// 全局下载进度服务接口
/// </summary>
public interface IDownloadProgressService
{
/// <summary>
/// 下载进度变更事件
/// </summary>
event Action<DownloadProgressInfo>? ProgressChanged;
/// <summary>
/// 报告下载进度
/// </summary>
void ReportProgress(DownloadProgressInfo progressInfo);
/// <summary>
/// 隐藏下载进度
/// </summary>
void HideProgress();
}

View File

@@ -0,0 +1,104 @@
using LubanHub.Core.Models;
using System;
namespace LubanHub.App.ViewModels;
/// <summary>
/// 下载进度ViewModel
/// </summary>
public class DownloadProgressViewModel : ViewModelBase
{
private string _fileName = string.Empty;
private double _progress;
private bool _isVisible;
private string _statusText = string.Empty;
private string _speedText = string.Empty;
public string FileName
{
get => _fileName;
set => SetProperty(ref _fileName, value);
}
public double Progress
{
get => _progress;
set => SetProperty(ref _progress, value);
}
public bool IsVisible
{
get => _isVisible;
set => SetProperty(ref _isVisible, value);
}
public string StatusText
{
get => _statusText;
set => SetProperty(ref _statusText, value);
}
public string SpeedText
{
get => _speedText;
set => SetProperty(ref _speedText, value);
}
public void UpdateProgress(DownloadProgressInfo progressInfo)
{
FileName = progressInfo.FileName;
Progress = progressInfo.Progress;
IsVisible = true;
if (progressInfo.IsCompleted)
{
StatusText = "下载完成";
SpeedText = string.Empty;
}
else if (!string.IsNullOrEmpty(progressInfo.ErrorMessage))
{
StatusText = $"下载出错: {progressInfo.ErrorMessage}";
SpeedText = string.Empty;
}
else
{
var downloadedMB = progressInfo.DownloadedBytes / 1024.0 / 1024.0;
var totalMB = progressInfo.TotalBytes / 1024.0 / 1024.0;
if (progressInfo.TotalBytes > 0)
{
StatusText = $"下载中: {downloadedMB:F1}MB / {totalMB:F1}MB";
}
else
{
StatusText = $"下载中: {downloadedMB:F1}MB";
}
if (progressInfo.Speed > 0)
{
var speedKB = progressInfo.Speed / 1024.0;
if (speedKB >= 1024)
{
SpeedText = $"{speedKB / 1024:F1}MB/s";
}
else
{
SpeedText = $"{speedKB:F1}KB/s";
}
}
else
{
SpeedText = string.Empty;
}
}
}
public void Hide()
{
IsVisible = false;
Progress = 0;
StatusText = string.Empty;
SpeedText = string.Empty;
FileName = string.Empty;
}
}

View File

@@ -0,0 +1,154 @@
using LubanHub.App.Services;
using LubanHub.Core.Interfaces;
using LubanHub.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace LubanHub.App.ViewModels;
/// <summary>
/// 主窗口ViewModel
/// </summary>
public class MainWindowViewModel : ViewModelBase
{
private readonly IAppDownloadService _downloadService;
private readonly IDownloadProgressService _progressService;
private readonly IExampleModuleService _exampleModuleService;
private readonly ILogger<MainWindowViewModel> _logger;
private string _downloadDirectory = string.Empty;
public MainWindowViewModel()
{
// 从依赖注入容器获取服务
if (App.ServiceProvider != null)
{
_downloadService = App.ServiceProvider.GetRequiredService<IAppDownloadService>();
_progressService = App.ServiceProvider.GetRequiredService<IDownloadProgressService>();
_exampleModuleService = App.ServiceProvider.GetRequiredService<IExampleModuleService>();
_logger = App.ServiceProvider.GetRequiredService<ILogger<MainWindowViewModel>>();
}
else
{
throw new InvalidOperationException("ServiceProvider 未初始化");
}
DownloadProgress = new DownloadProgressViewModel();
// 订阅全局下载进度事件
_progressService.ProgressChanged += OnProgressChanged;
// 初始化下载目录
_downloadDirectory = _downloadService.GetDownloadDirectory();
// 初始化命令
ChangeDownloadDirectoryCommand = new RelayCommand(async () => await ChangeDownloadDirectoryAsync());
TestModuleDownloadCommand = new RelayCommand(async () => await TestModuleDownloadAsync());
}
public DownloadProgressViewModel DownloadProgress { get; }
public string DownloadDirectory
{
get => _downloadDirectory;
set => SetProperty(ref _downloadDirectory, value);
}
public ICommand ChangeDownloadDirectoryCommand { get; }
public ICommand TestModuleDownloadCommand { get; }
private void OnProgressChanged(DownloadProgressInfo progressInfo)
{
// 在UI线程中更新进度
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
{
if (progressInfo.IsCompleted && string.IsNullOrEmpty(progressInfo.FileName))
{
// 隐藏进度条
DownloadProgress.Hide();
}
else
{
DownloadProgress.UpdateProgress(progressInfo);
}
});
}
private Task ChangeDownloadDirectoryAsync()
{
try
{
// TODO: 实现目录选择对话框
// 这里先用一个简单的示例
var newPath = @"C:\temp\LubanHub"; // 示例路径
_downloadService.SetDownloadDirectory(newPath);
DownloadDirectory = _downloadService.GetDownloadDirectory();
_logger.LogInformation("下载目录已更改为: {NewPath}", DownloadDirectory);
return Task.CompletedTask;
}
catch (Exception ex)
{
_logger.LogError(ex, "更改下载目录时出错");
return Task.FromException(ex);
}
}
private async Task TestModuleDownloadAsync()
{
try
{
_logger.LogInformation("测试其他模块下载功能");
await _exampleModuleService.DownloadLubanExampleAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "测试其他模块下载时出错");
}
}
}
/// <summary>
/// 简单的命令实现
/// </summary>
public class RelayCommand : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool>? _canExecute;
private bool _isExecuting;
public RelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
return !_isExecuting && (_canExecute?.Invoke() ?? true);
}
public async void Execute(object? parameter)
{
if (!CanExecute(parameter))
return;
_isExecuting = true;
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
try
{
await _execute();
}
finally
{
_isExecuting = false;
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace LubanHub.App.ViewModels;
/// <summary>
/// ViewModel基类
/// </summary>
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}

View File

@@ -0,0 +1,46 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LubanHub.App.Views.DownloadProgressView">
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="0,1,0,0"
Padding="16,8"
IsVisible="{Binding IsVisible}">
<Grid RowDefinitions="Auto,4,Auto,4,Auto">
<!-- 第一行:文件名和速度 -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0"
Text="{Binding FileName}"
FontWeight="Medium"
TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="1"
Text="{Binding SpeedText}"
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
FontSize="12" />
</Grid>
<!-- 第二行:间隔 -->
<!-- 第三行:进度条 -->
<ProgressBar Grid.Row="2"
Value="{Binding Progress}"
Minimum="0"
Maximum="100"
Height="4" />
<!-- 第四行:间隔 -->
<!-- 第五行:状态文本 -->
<TextBlock Grid.Row="4"
Text="{Binding StatusText}"
FontSize="11"
Foreground="{DynamicResource SystemControlForegroundBaseMediumBrush}"
HorizontalAlignment="Left" />
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace LubanHub.App.Views;
public partial class DownloadProgressView : UserControl
{
public DownloadProgressView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.DependencyInjection;
using System;
namespace LubanHub.Core.Attributes;
/// <summary>
/// 服务注册特性用于标记需要自动注册到DI容器的服务
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class RegistServiceAttribute : Attribute
{
/// <summary>
/// 服务生命周期
/// </summary>
public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Singleton;
/// <summary>
/// 服务接口类型,如果不指定则自动推断
/// </summary>
public Type? ServiceType { get; set; }
/// <summary>
/// 服务注册优先级,数值越小优先级越高
/// </summary>
public int Priority { get; set; } = 0;
/// <summary>
/// 初始化服务注册特性
/// </summary>
/// <param name="lifetime">服务生命周期</param>
/// <param name="serviceType">服务接口类型</param>
public RegistServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Singleton, Type? serviceType = null)
{
Lifetime = lifetime;
ServiceType = serviceType;
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LubanHub.Core.Interfaces;
/// <summary>
/// 解压缩服务接口
/// </summary>
public interface ICoreCompressionService
{
/// <summary>
/// 解压ZIP文件
/// </summary>
Task ExtractZipAsync(string zipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
/// <summary>
/// 创建ZIP文件
/// </summary>
Task CreateZipAsync(string sourcePath, string zipFilePath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
/// <summary>
/// 解压RAR文件 (如果支持)
/// </summary>
Task ExtractRarAsync(string rarFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
/// <summary>
/// 解压7Z文件 (如果支持)
/// </summary>
Task Extract7ZAsync(string sevenZipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
/// <summary>
/// 检查是否支持指定格式
/// </summary>
bool IsFormatSupported(string fileExtension);
/// <summary>
/// 自动检测格式并解压
/// </summary>
Task ExtractAsync(string archiveFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,37 @@
using LubanHub.Core.Models;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LubanHub.Core.Interfaces;
/// <summary>
/// 下载服务接口
/// </summary>
public interface ICoreDownloadService
{
/// <summary>
/// 获取默认下载目录
/// </summary>
string GetDefaultDownloadDirectory();
/// <summary>
/// 设置下载目录
/// </summary>
void SetDownloadDirectory(string path);
/// <summary>
/// 获取当前下载目录
/// </summary>
string GetDownloadDirectory();
/// <summary>
/// 下载文件
/// </summary>
Task<string> DownloadFileAsync(string url, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default);
/// <summary>
/// 下载文件到指定目录
/// </summary>
Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,76 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace LubanHub.Core.Interfaces;
/// <summary>
/// 文件管理服务接口
/// </summary>
public interface ICoreFileService
{
/// <summary>
/// 检查文件是否存在
/// </summary>
bool FileExists(string path);
/// <summary>
/// 检查目录是否存在
/// </summary>
bool DirectoryExists(string path);
/// <summary>
/// 创建目录
/// </summary>
void CreateDirectory(string path);
/// <summary>
/// 删除文件
/// </summary>
void DeleteFile(string path);
/// <summary>
/// 删除目录
/// </summary>
void DeleteDirectory(string path, bool recursive = false);
/// <summary>
/// 复制文件
/// </summary>
void CopyFile(string sourceFile, string destFile, bool overwrite = false);
/// <summary>
/// 移动文件
/// </summary>
void MoveFile(string sourceFile, string destFile);
/// <summary>
/// 读取文件文本
/// </summary>
Task<string> ReadAllTextAsync(string path);
/// <summary>
/// 写入文件文本
/// </summary>
Task WriteAllTextAsync(string path, string content);
/// <summary>
/// 获取文件信息
/// </summary>
FileInfo GetFileInfo(string path);
/// <summary>
/// 获取目录信息
/// </summary>
DirectoryInfo GetDirectoryInfo(string path);
/// <summary>
/// 获取目录下的文件列表
/// </summary>
string[] GetFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly);
/// <summary>
/// 获取目录下的子目录列表
/// </summary>
string[] GetDirectories(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly);
}

View File

@@ -0,0 +1,38 @@
using LubanHub.Core.Models;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LubanHub.Core.Interfaces;
/// <summary>
/// 进程调用服务接口
/// </summary>
public interface ICoreProcessService
{
/// <summary>
/// 执行进程并等待完成
/// </summary>
Task<ProcessResult> ExecuteAsync(string fileName, string? arguments = null, string? workingDirectory = null, CancellationToken cancellationToken = default);
/// <summary>
/// 执行进程并实时获取输出
/// </summary>
Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
Action<string>? onOutputReceived, Action<string>? onErrorReceived, CancellationToken cancellationToken = default);
/// <summary>
/// 启动进程但不等待完成
/// </summary>
System.Diagnostics.Process StartProcess(string fileName, string? arguments = null, string? workingDirectory = null);
/// <summary>
/// 检查进程是否正在运行
/// </summary>
bool IsProcessRunning(string processName);
/// <summary>
/// 杀死进程
/// </summary>
void KillProcess(string processName);
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>LubanHub.Core</RootNamespace>
<AssemblyName>LubanHub.Core</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,42 @@
namespace LubanHub.Core.Models;
/// <summary>
/// 下载进度信息
/// </summary>
public class DownloadProgressInfo
{
/// <summary>
/// 文件名
/// </summary>
public string FileName { get; set; } = string.Empty;
/// <summary>
/// 下载进度 (0-100)
/// </summary>
public double Progress { get; set; }
/// <summary>
/// 已下载字节数
/// </summary>
public long DownloadedBytes { get; set; }
/// <summary>
/// 总字节数
/// </summary>
public long TotalBytes { get; set; }
/// <summary>
/// 下载速度 (bytes/s)
/// </summary>
public long Speed { get; set; }
/// <summary>
/// 是否完成
/// </summary>
public bool IsCompleted { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string? ErrorMessage { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace LubanHub.Core.Models;
/// <summary>
/// 进程执行结果
/// </summary>
public class ProcessResult
{
/// <summary>
/// 退出代码
/// </summary>
public int ExitCode { get; set; }
/// <summary>
/// 标准输出
/// </summary>
public string StandardOutput { get; set; } = string.Empty;
/// <summary>
/// 标准错误
/// </summary>
public string StandardError { get; set; } = string.Empty;
/// <summary>
/// 是否成功执行
/// </summary>
public bool IsSuccess => ExitCode == 0;
}

View File

@@ -0,0 +1,80 @@
using LubanHub.Core.Attributes;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Net.Http;
using System.Reflection;
namespace LubanHub.Core;
/// <summary>
/// Core服务依赖注入扩展
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加Core服务 - 通过反射自动发现并注册带有RegistService特性的服务
/// </summary>
public static IServiceCollection AddCoreServices(this IServiceCollection services)
{
// 注册HttpClientCore服务的依赖
services.AddSingleton<HttpClient>();
// 获取当前程序集
var assembly = Assembly.GetExecutingAssembly();
// 发现所有带有RegistService特性的类型
var serviceTypes = assembly.GetTypes()
.Where(type => type.IsClass && !type.IsAbstract && type.GetCustomAttribute<RegistServiceAttribute>() != null)
.Select(type => new
{
Type = type,
Attribute = type.GetCustomAttribute<RegistServiceAttribute>()!
})
.OrderBy(x => x.Attribute.Priority) // 按优先级排序
.ToList();
// 注册服务
foreach (var serviceInfo in serviceTypes)
{
var implementationType = serviceInfo.Type;
var attribute = serviceInfo.Attribute;
// 确定服务接口类型
Type serviceType;
if (attribute.ServiceType != null)
{
// 使用特性指定的服务类型
serviceType = attribute.ServiceType;
}
else
{
// 自动推断:查找实现的第一个接口
var interfaceType = implementationType.GetInterfaces().FirstOrDefault();
if (interfaceType == null)
{
throw new InvalidOperationException($"服务 {implementationType.Name} 没有实现任何接口且未在RegistService特性中指定ServiceType");
}
serviceType = interfaceType;
}
// 根据生命周期注册服务
switch (attribute.Lifetime)
{
case ServiceLifetime.Singleton:
services.AddSingleton(serviceType, implementationType);
break;
case ServiceLifetime.Scoped:
services.AddScoped(serviceType, implementationType);
break;
case ServiceLifetime.Transient:
services.AddTransient(serviceType, implementationType);
break;
default:
throw new ArgumentOutOfRangeException(nameof(attribute.Lifetime), attribute.Lifetime, "不支持的服务生命周期");
}
}
return services;
}
}

View File

@@ -0,0 +1,178 @@
using LubanHub.Core.Attributes;
using LubanHub.Core.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace LubanHub.Core.Services;
/// <summary>
/// 解压缩服务实现
/// </summary>
[RegistService(ServiceLifetime.Singleton, typeof(ICoreCompressionService))]
public class CoreCompressionService : ICoreCompressionService
{
private readonly ILogger<CoreCompressionService> _logger;
private readonly ICoreFileService _fileService;
public CoreCompressionService(ILogger<CoreCompressionService> logger, ICoreFileService fileService)
{
_logger = logger;
_fileService = fileService;
}
public Task ExtractZipAsync(string zipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
{
return Task.Run(() =>
{
try
{
if (!_fileService.FileExists(zipFilePath))
throw new FileNotFoundException($"ZIP文件不存在: {zipFilePath}");
_logger.LogInformation("开始解压ZIP文件: {ZipPath} -> {ExtractPath}", zipFilePath, extractPath);
// 确保提取目录存在
if (!_fileService.DirectoryExists(extractPath))
{
_fileService.CreateDirectory(extractPath);
}
using var archive = ZipFile.OpenRead(zipFilePath);
var totalEntries = archive.Entries.Count;
var extractedEntries = 0;
foreach (var entry in archive.Entries)
{
cancellationToken.ThrowIfCancellationRequested();
// 跳过目录条目
if (string.IsNullOrEmpty(entry.Name))
{
extractedEntries++;
continue;
}
var destinationPath = Path.Combine(extractPath, entry.FullName);
var destinationDir = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrEmpty(destinationDir) && !_fileService.DirectoryExists(destinationDir))
{
_fileService.CreateDirectory(destinationDir);
}
try
{
entry.ExtractToFile(destinationPath, overwrite: true);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "解压文件时出错: {EntryName}", entry.FullName);
}
extractedEntries++;
var progressPercent = (double)extractedEntries / totalEntries * 100;
progress?.Report(progressPercent);
}
_logger.LogInformation("ZIP文件解压完成: {ZipPath}, 提取了 {Count} 个文件", zipFilePath, extractedEntries);
}
catch (Exception ex)
{
_logger.LogError(ex, "解压ZIP文件时出错: {ZipPath}", zipFilePath);
throw;
}
}, cancellationToken);
}
public async Task CreateZipAsync(string sourcePath, string zipFilePath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
{
try
{
if (!_fileService.DirectoryExists(sourcePath) && !_fileService.FileExists(sourcePath))
throw new DirectoryNotFoundException($"源路径不存在: {sourcePath}");
_logger.LogInformation("开始创建ZIP文件: {SourcePath} -> {ZipPath}", sourcePath, zipFilePath);
var zipDir = Path.GetDirectoryName(zipFilePath);
if (!string.IsNullOrEmpty(zipDir) && !_fileService.DirectoryExists(zipDir))
{
_fileService.CreateDirectory(zipDir);
}
await Task.Run(() =>
{
if (_fileService.FileExists(sourcePath))
{
// 压缩单个文件
using var archive = ZipFile.Open(zipFilePath, ZipArchiveMode.Create);
archive.CreateEntryFromFile(sourcePath, Path.GetFileName(sourcePath));
}
else
{
// 压缩目录
ZipFile.CreateFromDirectory(sourcePath, zipFilePath);
}
}, cancellationToken);
progress?.Report(100);
_logger.LogInformation("ZIP文件创建完成: {ZipPath}", zipFilePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "创建ZIP文件时出错: {SourcePath} -> {ZipPath}", sourcePath, zipFilePath);
throw;
}
}
public Task ExtractRarAsync(string rarFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
{
_logger.LogWarning("RAR格式暂不支持请使用第三方库实现");
throw new NotSupportedException("RAR格式暂不支持");
}
public Task Extract7ZAsync(string sevenZipFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
{
_logger.LogWarning("7Z格式暂不支持请使用第三方库实现");
throw new NotSupportedException("7Z格式暂不支持");
}
public bool IsFormatSupported(string fileExtension)
{
var supportedFormats = new[] { ".zip" };
return supportedFormats.Contains(fileExtension.ToLowerInvariant());
}
public async Task ExtractAsync(string archiveFilePath, string extractPath, IProgress<double>? progress = null, CancellationToken cancellationToken = default)
{
try
{
var extension = Path.GetExtension(archiveFilePath).ToLowerInvariant();
switch (extension)
{
case ".zip":
await ExtractZipAsync(archiveFilePath, extractPath, progress, cancellationToken);
break;
case ".rar":
await ExtractRarAsync(archiveFilePath, extractPath, progress, cancellationToken);
break;
case ".7z":
await Extract7ZAsync(archiveFilePath, extractPath, progress, cancellationToken);
break;
default:
throw new NotSupportedException($"不支持的压缩格式: {extension}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "自动解压文件时出错: {ArchivePath}", archiveFilePath);
throw;
}
}
}

View File

@@ -0,0 +1,200 @@
using LubanHub.Core.Attributes;
using LubanHub.Core.Interfaces;
using LubanHub.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace LubanHub.Core.Services;
/// <summary>
/// 下载服务实现
/// </summary>
[RegistService(ServiceLifetime.Singleton, typeof(ICoreDownloadService))]
public class CoreDownloadService : ICoreDownloadService
{
private readonly ILogger<CoreDownloadService> _logger;
private readonly ICoreFileService _fileService;
private readonly HttpClient _httpClient;
private string _downloadDirectory;
public CoreDownloadService(ILogger<CoreDownloadService> logger, ICoreFileService fileService, HttpClient httpClient)
{
_logger = logger;
_fileService = fileService;
_httpClient = httpClient;
_downloadDirectory = GetDefaultDownloadDirectory();
}
public string GetDefaultDownloadDirectory()
{
// 使用系统用户数据目录的LubanHub子目录
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
return Path.Combine(appDataPath, "LubanHub", "Downloads");
}
public void SetDownloadDirectory(string path)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("下载目录路径不能为空", nameof(path));
_downloadDirectory = path;
// 确保目录存在
if (!_fileService.DirectoryExists(_downloadDirectory))
{
_fileService.CreateDirectory(_downloadDirectory);
}
_logger.LogDebug("设置下载目录: {Path}", _downloadDirectory);
}
public string GetDownloadDirectory()
{
// 确保目录存在
if (!_fileService.DirectoryExists(_downloadDirectory))
{
_fileService.CreateDirectory(_downloadDirectory);
}
return _downloadDirectory;
}
public Task<string> DownloadFileAsync(string url, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default)
{
return DownloadFileAsync(url, GetDownloadDirectory(), fileName, progress, cancellationToken);
}
public async Task<string> DownloadFileAsync(string url, string targetDirectory, string? fileName = null, IProgress<DownloadProgressInfo>? progress = null, CancellationToken cancellationToken = default)
{
try
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("URL不能为空", nameof(url));
if (string.IsNullOrWhiteSpace(targetDirectory))
throw new ArgumentException("目标目录不能为空", nameof(targetDirectory));
// 确保目标目录存在
if (!_fileService.DirectoryExists(targetDirectory))
{
_fileService.CreateDirectory(targetDirectory);
}
// 确定文件名
if (string.IsNullOrWhiteSpace(fileName))
{
fileName = GetFileNameFromUrl(url);
}
var targetFilePath = Path.Combine(targetDirectory, fileName);
_logger.LogInformation("开始下载文件: {Url} -> {TargetPath}", url, targetFilePath);
var stopwatch = Stopwatch.StartNew();
var lastBytesRead = 0L;
var lastTime = stopwatch.Elapsed;
var progressInfo = new DownloadProgressInfo
{
FileName = fileName
};
try
{
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength ?? -1;
progressInfo.TotalBytes = totalBytes;
using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var fileStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
var buffer = new byte[8192];
var totalBytesRead = 0L;
int bytesRead;
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
totalBytesRead += bytesRead;
// 计算进度和速度
var currentTime = stopwatch.Elapsed;
var timeDiff = currentTime - lastTime;
if (timeDiff.TotalMilliseconds >= 200) // 每200ms更新一次进度
{
var bytesDiff = totalBytesRead - lastBytesRead;
var speed = timeDiff.TotalSeconds > 0 ? (long)(bytesDiff / timeDiff.TotalSeconds) : 0;
progressInfo.DownloadedBytes = totalBytesRead;
progressInfo.Speed = speed;
progressInfo.Progress = totalBytes > 0 ? (double)totalBytesRead / totalBytes * 100 : 0;
progress?.Report(progressInfo);
lastBytesRead = totalBytesRead;
lastTime = currentTime;
}
}
// 完成
progressInfo.DownloadedBytes = totalBytesRead;
progressInfo.Progress = 100;
progressInfo.IsCompleted = true;
progressInfo.Speed = 0;
progress?.Report(progressInfo);
_logger.LogInformation("文件下载完成: {TargetPath}, 大小: {Size} 字节, 耗时: {Duration:F2}s",
targetFilePath, totalBytesRead, stopwatch.Elapsed.TotalSeconds);
return targetFilePath;
}
catch (Exception ex)
{
progressInfo.ErrorMessage = ex.Message;
progress?.Report(progressInfo);
throw;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "下载文件时出错: {Url}", url);
throw;
}
}
private static string GetFileNameFromUrl(string url)
{
try
{
var uri = new Uri(url);
var fileName = Path.GetFileName(uri.LocalPath);
if (string.IsNullOrWhiteSpace(fileName) || fileName == "/")
{
fileName = $"download_{DateTime.Now:yyyyMMdd_HHmmss}";
}
// 移除查询参数
var queryIndex = fileName.IndexOf('?');
if (queryIndex >= 0)
{
fileName = fileName.Substring(0, queryIndex);
}
return fileName;
}
catch
{
return $"download_{DateTime.Now:yyyyMMdd_HHmmss}";
}
}
}

View File

@@ -0,0 +1,206 @@
using LubanHub.Core.Attributes;
using LubanHub.Core.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Threading.Tasks;
namespace LubanHub.Core.Services;
/// <summary>
/// 文件管理服务实现
/// </summary>
[RegistService(ServiceLifetime.Singleton, typeof(ICoreFileService))]
public class CoreFileService : ICoreFileService
{
private readonly ILogger<CoreFileService> _logger;
public CoreFileService(ILogger<CoreFileService> logger)
{
_logger = logger;
}
public bool FileExists(string path)
{
try
{
return File.Exists(path);
}
catch (Exception ex)
{
_logger.LogError(ex, "检查文件是否存在时出错: {Path}", path);
return false;
}
}
public bool DirectoryExists(string path)
{
try
{
return Directory.Exists(path);
}
catch (Exception ex)
{
_logger.LogError(ex, "检查目录是否存在时出错: {Path}", path);
return false;
}
}
public void CreateDirectory(string path)
{
try
{
Directory.CreateDirectory(path);
_logger.LogDebug("创建目录: {Path}", path);
}
catch (Exception ex)
{
_logger.LogError(ex, "创建目录时出错: {Path}", path);
throw;
}
}
public void DeleteFile(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
_logger.LogDebug("删除文件: {Path}", path);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "删除文件时出错: {Path}", path);
throw;
}
}
public void DeleteDirectory(string path, bool recursive = false)
{
try
{
if (Directory.Exists(path))
{
Directory.Delete(path, recursive);
_logger.LogDebug("删除目录: {Path}, 递归: {Recursive}", path, recursive);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "删除目录时出错: {Path}", path);
throw;
}
}
public void CopyFile(string sourceFile, string destFile, bool overwrite = false)
{
try
{
File.Copy(sourceFile, destFile, overwrite);
_logger.LogDebug("复制文件: {Source} -> {Dest}", sourceFile, destFile);
}
catch (Exception ex)
{
_logger.LogError(ex, "复制文件时出错: {Source} -> {Dest}", sourceFile, destFile);
throw;
}
}
public void MoveFile(string sourceFile, string destFile)
{
try
{
File.Move(sourceFile, destFile);
_logger.LogDebug("移动文件: {Source} -> {Dest}", sourceFile, destFile);
}
catch (Exception ex)
{
_logger.LogError(ex, "移动文件时出错: {Source} -> {Dest}", sourceFile, destFile);
throw;
}
}
public async Task<string> ReadAllTextAsync(string path)
{
try
{
var content = await File.ReadAllTextAsync(path);
_logger.LogDebug("读取文件: {Path}, 大小: {Size} 字符", path, content.Length);
return content;
}
catch (Exception ex)
{
_logger.LogError(ex, "读取文件时出错: {Path}", path);
throw;
}
}
public async Task WriteAllTextAsync(string path, string content)
{
try
{
await File.WriteAllTextAsync(path, content);
_logger.LogDebug("写入文件: {Path}, 大小: {Size} 字符", path, content.Length);
}
catch (Exception ex)
{
_logger.LogError(ex, "写入文件时出错: {Path}", path);
throw;
}
}
public FileInfo GetFileInfo(string path)
{
try
{
return new FileInfo(path);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取文件信息时出错: {Path}", path);
throw;
}
}
public DirectoryInfo GetDirectoryInfo(string path)
{
try
{
return new DirectoryInfo(path);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取目录信息时出错: {Path}", path);
throw;
}
}
public string[] GetFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
try
{
return Directory.GetFiles(path, searchPattern, searchOption);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取文件列表时出错: {Path}", path);
throw;
}
}
public string[] GetDirectories(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
try
{
return Directory.GetDirectories(path, searchPattern, searchOption);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取目录列表时出错: {Path}", path);
throw;
}
}
}

View File

@@ -0,0 +1,229 @@
using LubanHub.Core.Attributes;
using LubanHub.Core.Interfaces;
using LubanHub.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LubanHub.Core.Services;
/// <summary>
/// 进程调用服务实现
/// </summary>
[RegistService(ServiceLifetime.Singleton, typeof(ICoreProcessService))]
public class CoreProcessService : ICoreProcessService
{
private readonly ILogger<CoreProcessService> _logger;
public CoreProcessService(ILogger<CoreProcessService> logger)
{
_logger = logger;
}
public async Task<ProcessResult> ExecuteAsync(string fileName, string? arguments = null, string? workingDirectory = null, CancellationToken cancellationToken = default)
{
try
{
_logger.LogDebug("执行进程: {FileName} {Arguments}", fileName, arguments);
var processStartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments ?? string.Empty,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using var process = new Process { StartInfo = processStartInfo };
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
process.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
outputBuilder.AppendLine(args.Data);
}
};
process.ErrorDataReceived += (sender, args) =>
{
if (args.Data != null)
{
errorBuilder.AppendLine(args.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken);
var result = new ProcessResult
{
ExitCode = process.ExitCode,
StandardOutput = outputBuilder.ToString(),
StandardError = errorBuilder.ToString()
};
_logger.LogDebug("进程执行完成: {FileName}, 退出代码: {ExitCode}", fileName, result.ExitCode);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "执行进程时出错: {FileName} {Arguments}", fileName, arguments);
throw;
}
}
public async Task<ProcessResult> ExecuteAsync(string fileName, string? arguments, string? workingDirectory,
Action<string>? onOutputReceived, Action<string>? onErrorReceived, CancellationToken cancellationToken = default)
{
try
{
_logger.LogDebug("执行进程(实时输出): {FileName} {Arguments}", fileName, arguments);
var processStartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments ?? string.Empty,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
using var process = new Process { StartInfo = processStartInfo };
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
process.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
outputBuilder.AppendLine(args.Data);
onOutputReceived?.Invoke(args.Data);
}
};
process.ErrorDataReceived += (sender, args) =>
{
if (args.Data != null)
{
errorBuilder.AppendLine(args.Data);
onErrorReceived?.Invoke(args.Data);
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken);
var result = new ProcessResult
{
ExitCode = process.ExitCode,
StandardOutput = outputBuilder.ToString(),
StandardError = errorBuilder.ToString()
};
_logger.LogDebug("进程执行完成(实时输出): {FileName}, 退出代码: {ExitCode}", fileName, result.ExitCode);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "执行进程时出错(实时输出): {FileName} {Arguments}", fileName, arguments);
throw;
}
}
public Process StartProcess(string fileName, string? arguments = null, string? workingDirectory = null)
{
try
{
_logger.LogDebug("启动进程: {FileName} {Arguments}", fileName, arguments);
var processStartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments ?? string.Empty,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
UseShellExecute = true
};
var process = Process.Start(processStartInfo);
if (process == null)
{
throw new InvalidOperationException($"无法启动进程: {fileName}");
}
return process;
}
catch (Exception ex)
{
_logger.LogError(ex, "启动进程时出错: {FileName} {Arguments}", fileName, arguments);
throw;
}
}
public bool IsProcessRunning(string processName)
{
try
{
var processes = Process.GetProcessesByName(processName);
return processes.Length > 0;
}
catch (Exception ex)
{
_logger.LogError(ex, "检查进程是否运行时出错: {ProcessName}", processName);
return false;
}
}
public void KillProcess(string processName)
{
try
{
var processes = Process.GetProcessesByName(processName);
foreach (var process in processes)
{
try
{
process.Kill();
_logger.LogDebug("杀死进程: {ProcessName} (PID: {ProcessId})", processName, process.Id);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "杀死进程时出错: {ProcessName} (PID: {ProcessId})", processName, process.Id);
}
finally
{
process.Dispose();
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "杀死进程时出错: {ProcessName}", processName);
throw;
}
}
}