diff --git a/.gitignore b/.gitignore index 3fdb593..58c7a0e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ obj/ logs/ *.log +# Local config overrides (secrets) +appsettings.local.json +src/TelegramPanel.Web/appsettings.local.json + # Sessions sessions/ @@ -25,6 +29,9 @@ sessions/ .DS_Store Thumbs.db +# SQLite +*.db + # NuGet *.nupkg packages/ diff --git a/global.json b/global.json new file mode 100644 index 0000000..7b3240a --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.416", + "rollForward": "latestPatch" + } +} diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountService.cs index aea339b..04ddaac 100644 --- a/src/TelegramPanel.Core/Services/Telegram/AccountService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/AccountService.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; using TelegramPanel.Core.Interfaces; using TelegramPanel.Core.Models; using AccountStatus = TelegramPanel.Core.Interfaces.AccountStatus; @@ -12,23 +13,30 @@ public class AccountService : IAccountService { private readonly ITelegramClientPool _clientPool; private readonly ILogger _logger; + private readonly IConfiguration _configuration; // 临时存储登录状态(实际项目应该使用数据库或缓存) private readonly Dictionary _pendingLogins = new(); - public AccountService(ITelegramClientPool clientPool, ILogger logger) + public AccountService(ITelegramClientPool clientPool, ILogger logger, IConfiguration configuration) { _clientPool = clientPool; _logger = logger; + _configuration = configuration; } public async Task StartLoginAsync(int accountId, string phone) { - // 这里需要从数据库获取 apiId, apiHash, sessionPath - // 暂时使用示例值 - var apiId = 0; // TODO: 从配置或数据库获取 - var apiHash = ""; // TODO: 从配置或数据库获取 - var sessionPath = $"sessions/{phone}.session"; + if (!int.TryParse(_configuration["Telegram:ApiId"], out var apiId) || apiId <= 0 + || string.IsNullOrWhiteSpace(_configuration["Telegram:ApiHash"])) + { + return new LoginResult(false, null, "请先在【系统设置】中配置全局 Telegram API(ApiId/ApiHash)"); + } + + var apiHash = _configuration["Telegram:ApiHash"]!.Trim(); + var sessionsPath = _configuration["Telegram:SessionsPath"] ?? "sessions"; + Directory.CreateDirectory(sessionsPath); + var sessionPath = Path.Combine(sessionsPath, $"{phone}.session"); var client = await _clientPool.GetOrCreateClientAsync(accountId, apiId, apiHash, sessionPath); diff --git a/src/TelegramPanel.Core/TelegramPanel.Core.csproj b/src/TelegramPanel.Core/TelegramPanel.Core.csproj index dd0d55e..2967738 100644 --- a/src/TelegramPanel.Core/TelegramPanel.Core.csproj +++ b/src/TelegramPanel.Core/TelegramPanel.Core.csproj @@ -9,8 +9,14 @@ + + + + + + diff --git a/src/TelegramPanel.Data/Migrations/20250119000000_InitialCreate.cs b/src/TelegramPanel.Data/Migrations/20250119000000_InitialCreate.cs index f070074..346175e 100644 --- a/src/TelegramPanel.Data/Migrations/20250119000000_InitialCreate.cs +++ b/src/TelegramPanel.Data/Migrations/20250119000000_InitialCreate.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -6,6 +7,8 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace TelegramPanel.Data.Migrations { /// + [DbContext(typeof(AppDbContext))] + [Migration("20250119000000_InitialCreate")] public partial class InitialCreate : Migration { /// diff --git a/src/TelegramPanel.Web/Components/App.razor b/src/TelegramPanel.Web/Components/App.razor index 0fcee07..76570e2 100644 --- a/src/TelegramPanel.Web/Components/App.razor +++ b/src/TelegramPanel.Web/Components/App.razor @@ -7,11 +7,11 @@ - + - + diff --git a/src/TelegramPanel.Web/Components/Pages/AccountCategories.razor b/src/TelegramPanel.Web/Components/Pages/AccountCategories.razor index 1b40633..5af3358 100644 --- a/src/TelegramPanel.Web/Components/Pages/AccountCategories.razor +++ b/src/TelegramPanel.Web/Components/Pages/AccountCategories.razor @@ -1,5 +1,8 @@ @page "/accounts/categories" +@inject AccountCategoryManagementService CategoryManagement @inject ISnackbar Snackbar +@inject IDialogService DialogService +@using TelegramPanel.Data.Entities 账号分类 - Telegram Panel @@ -13,7 +16,7 @@ - + @@ -31,7 +34,7 @@ 分类列表 - + 分类名称 颜色 @@ -41,18 +44,16 @@ - @context.Name + @context.Name -
+
- @context.Description - @context.AccountCount + @(context.Description ?? "-") + @context.Accounts.Count - + OnClick="@(() => DeleteCategory(context))" />
@@ -68,56 +69,77 @@ private string newCategoryName = ""; private MudColor newCategoryColor = "#1976d2"; private string newCategoryDesc = ""; + private List categories = new(); + private bool loading = true; - private List categories = new() + protected override async Task OnInitializedAsync() { - new CategoryModel { Id = 1, Name = "工作账号", Color = "#1976d2", Description = "用于工作的账号", AccountCount = 5 }, - new CategoryModel { Id = 2, Name = "推广账号", Color = "#f44336", Description = "用于推广的账号", AccountCount = 10 }, - new CategoryModel { Id = 3, Name = "测试账号", Color = "#4caf50", Description = "测试用账号", AccountCount = 3 } - }; - - private void AddCategory() - { - var newCategory = new CategoryModel - { - Id = categories.Max(c => c.Id) + 1, - Name = newCategoryName, - Color = newCategoryColor.ToString(), - Description = newCategoryDesc, - AccountCount = 0 - }; - - categories.Add(newCategory); - Snackbar.Add($"分类 \"{newCategoryName}\" 添加成功", Severity.Success); - - // 清空表单 - newCategoryName = ""; - newCategoryColor = "#1976d2"; - newCategoryDesc = ""; + await LoadCategories(); } - private void EditCategory(CategoryModel category) + private async Task LoadCategories() { - Snackbar.Add($"编辑分类:{category.Name}", Severity.Info); - // TODO: 实现编辑对话框 - } - - private void DeleteCategory(int id) - { - var category = categories.FirstOrDefault(c => c.Id == id); - if (category != null) + loading = true; + try { - categories.Remove(category); - Snackbar.Add($"分类已删除", Severity.Success); + var result = await CategoryManagement.GetAllCategoriesAsync(); + categories = result.ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载失败:{ex.Message}", Severity.Error); + } + finally + { + loading = false; } } - private class CategoryModel + private async Task AddCategory() { - public int Id { get; set; } - public string Name { get; set; } = ""; - public string Color { get; set; } = ""; - public string Description { get; set; } = ""; - public int AccountCount { get; set; } + try + { + var category = new AccountCategory + { + Name = newCategoryName, + Color = newCategoryColor.Value, + Description = newCategoryDesc, + CreatedAt = DateTime.UtcNow + }; + + await CategoryManagement.CreateCategoryAsync(category); + await LoadCategories(); + Snackbar.Add($"分类 \"{newCategoryName}\" 添加成功", Severity.Success); + + newCategoryName = ""; + newCategoryColor = "#1976d2"; + newCategoryDesc = ""; + } + catch (Exception ex) + { + Snackbar.Add($"添加失败:{ex.Message}", Severity.Error); + } + } + + private async Task DeleteCategory(AccountCategory category) + { + bool? result = await DialogService.ShowMessageBox( + "确认删除", + $"确定要删除分类 {category.Name} 吗?关联的账号将变为未分类。", + yesText: "删除", cancelText: "取消"); + + if (result == true) + { + try + { + await CategoryManagement.DeleteCategoryAsync(category.Id); + categories.Remove(category); + Snackbar.Add("删除成功", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"删除失败:{ex.Message}", Severity.Error); + } + } } } diff --git a/src/TelegramPanel.Web/Components/Pages/AccountImport.razor b/src/TelegramPanel.Web/Components/Pages/AccountImport.razor index 291e4f3..5df8aba 100644 --- a/src/TelegramPanel.Web/Components/Pages/AccountImport.razor +++ b/src/TelegramPanel.Web/Components/Pages/AccountImport.razor @@ -1,5 +1,6 @@ @page "/accounts/import" @inject AccountImportService AccountImportService +@inject IConfiguration Configuration @inject ISnackbar Snackbar 导入账号 - Telegram Panel @@ -13,9 +14,6 @@ Session 文件导入 - - - @@ -27,10 +25,10 @@ @if (selectedFiles.Any()) { 已选择 @selectedFiles.Count 个文件 - + @foreach (var file in selectedFiles) { - @file.Name + @file.Name } } @@ -58,8 +56,6 @@ StringSession 导入 - - @@ -101,11 +97,11 @@ @(context.UserId?.ToString() ?? "-") @(context.Username ?? "-") - + @(context.Success ? "成功" : "失败") - @(context.Message ?? "-") + @(context.Success ? "成功" : (context.Error ?? "失败"))
@@ -113,18 +109,22 @@ } @code { - private string apiId = ""; - private string apiHash = ""; private List selectedFiles = new(); private bool importing = false; - private string stringApiId = ""; - private string stringApiHash = ""; private string sessionString = ""; private bool importingString = false; private List importResults = new(); + private bool TryGetTelegramApi(out int apiId, out string apiHash) + { + apiHash = Configuration["Telegram:ApiHash"] ?? ""; + return int.TryParse(Configuration["Telegram:ApiId"], out apiId) + && apiId > 0 + && !string.IsNullOrWhiteSpace(apiHash); + } + private void OnFilesChanged(IReadOnlyList files) { selectedFiles = files.ToList(); @@ -132,9 +132,9 @@ private async Task ImportSessionFiles() { - if (!int.TryParse(apiId, out int parsedApiId)) + if (!TryGetTelegramApi(out var apiId, out var apiHash)) { - Snackbar.Add("API ID 格式错误", Severity.Error); + Snackbar.Add("请先在【系统设置】中配置全局 Telegram API(ApiId/ApiHash)", Severity.Error); return; } @@ -145,7 +145,7 @@ { var results = await AccountImportService.ImportFromBrowserFilesAsync( selectedFiles, - parsedApiId, + apiId, apiHash ); @@ -179,9 +179,9 @@ private async Task ImportStringSession() { - if (!int.TryParse(stringApiId, out int parsedApiId)) + if (!TryGetTelegramApi(out var apiId, out var apiHash)) { - Snackbar.Add("API ID 格式错误", Severity.Error); + Snackbar.Add("请先在【系统设置】中配置全局 Telegram API(ApiId/ApiHash)", Severity.Error); return; } @@ -191,8 +191,8 @@ { var result = await AccountImportService.ImportFromStringSessionAsync( sessionString, - parsedApiId, - stringApiHash + apiId, + apiHash ); importResults.Add(result); @@ -204,7 +204,7 @@ } else { - Snackbar.Add($"导入失败:{result.Message}", Severity.Error); + Snackbar.Add($"导入失败:{result.Error ?? "未知错误"}", Severity.Error); } } catch (Exception ex) diff --git a/src/TelegramPanel.Web/Components/Pages/AccountLogin.razor b/src/TelegramPanel.Web/Components/Pages/AccountLogin.razor index 3881e90..8e60ced 100644 --- a/src/TelegramPanel.Web/Components/Pages/AccountLogin.razor +++ b/src/TelegramPanel.Web/Components/Pages/AccountLogin.razor @@ -1,6 +1,7 @@ @page "/accounts/login" @inject IAccountService AccountService @inject AccountManagementService AccountManagement +@inject IConfiguration Configuration @inject ISnackbar Snackbar @inject NavigationManager Navigation @using TelegramPanel.Data.Entities @@ -15,8 +16,21 @@ - - + @if (!IsApiConfigured) + { + + 未配置全局 Telegram API(ApiId/ApiHash),无法登录。 + + + 去系统设置配置 + + } + else + { + + 使用全局 Telegram API:ApiId=@(Configuration["Telegram:ApiId"] ?? "(未配置)") + + } @@ -57,20 +71,23 @@ - + 重置 - @if (stepper.GetActiveIndex() > 0 && !stepper.IsAllStepsCompleted()) + @if ((stepper?.ActiveIndex ?? 0) > 0 && !IsFinished) { - + 上一步 } - @if (!stepper.IsAllStepsCompleted()) + @if (!IsFinished) { + Disabled="@logging" + OnClick="@(async (MouseEventArgs _) => await NextStep())"> @if (logging) { @@ -78,7 +95,7 @@ } else { - @(stepper.GetActiveIndex() == stepper.Steps.Count - 2 ? "登录" : "下一步") + @(IsLoginActionStep ? "登录" : "下一步") } } @@ -88,11 +105,12 @@ @code { - private MudStepper stepper; + private MudStepper? stepper; private bool logging = false; - private string apiId = ""; - private string apiHash = ""; + private int telegramApiId; + private string telegramApiHash = ""; + private string sessionsPath = "sessions"; private string phoneNumber = ""; private string verificationCode = ""; private string password = ""; @@ -101,23 +119,54 @@ private long userId = 0; private int accountId = 0; + private bool TryGetTelegramApi(out int apiId, out string apiHash, out string configuredSessionsPath) + { + apiHash = Configuration["Telegram:ApiHash"] ?? ""; + configuredSessionsPath = Configuration["Telegram:SessionsPath"] ?? "sessions"; + return int.TryParse(Configuration["Telegram:ApiId"], out apiId) + && apiId > 0 + && !string.IsNullOrWhiteSpace(apiHash); + } + + private bool IsApiConfigured => TryGetTelegramApi(out _, out _, out _); + + private bool IsFinished => + stepper != null && stepper.ActiveIndex >= stepper.Steps.Count - 1; + + private bool IsLoginActionStep => + stepper != null && stepper.ActiveIndex == stepper.Steps.Count - 2; + + private async Task ResetStepperAsync() + { + if (stepper == null) return; + await stepper.ResetAsync(true); + } + + private async Task PreviousStepAsync() + { + if (stepper == null) return; + await stepper.PreviousStepAsync(); + } + private async Task NextStep() { - var currentStep = stepper.GetActiveIndex(); + if (stepper == null) return; + + var currentStep = stepper.ActiveIndex; logging = true; try { if (currentStep == 0) { - // 步骤1:验证 API 配置并发送验证码 - if (!int.TryParse(apiId, out int parsedApiId)) + // 步骤1:检查全局配置并发送验证码 + if (!TryGetTelegramApi(out telegramApiId, out telegramApiHash, out sessionsPath)) { - Snackbar.Add("API ID 格式错误", Severity.Error); + Snackbar.Add("请先在【系统设置】中配置全局 Telegram API(ApiId/ApiHash)", Severity.Error); return; } - if (string.IsNullOrWhiteSpace(apiHash) || string.IsNullOrWhiteSpace(phoneNumber)) + if (string.IsNullOrWhiteSpace(phoneNumber)) { Snackbar.Add("请填写完整信息", Severity.Error); return; @@ -131,7 +180,7 @@ if (result.NextStep == "code") { Snackbar.Add($"验证码已发送到 {phoneNumber}", Severity.Info); - stepper.Next(); + await stepper.NextStepAsync(); } else if (result.NextStep == "signup") { @@ -161,7 +210,7 @@ else if (result.NextStep == "password") { Snackbar.Add("需要两步验证密码", Severity.Info); - stepper.Next(); + await stepper.NextStepAsync(); } else { @@ -211,6 +260,8 @@ // 更新现有账号 existingAccount.Username = accountInfo.Username; existingAccount.IsActive = true; + existingAccount.ApiId = telegramApiId; + existingAccount.ApiHash = telegramApiHash; existingAccount.LastSyncAt = DateTime.UtcNow; await AccountManagement.UpdateAccountAsync(existingAccount); @@ -224,9 +275,9 @@ Phone = accountInfo.Phone!, UserId = accountInfo.TelegramUserId, Username = accountInfo.Username, - SessionPath = $"sessions/{accountInfo.Phone}.session", - ApiId = int.Parse(apiId), - ApiHash = apiHash, + SessionPath = System.IO.Path.Combine(sessionsPath, $"{accountInfo.Phone}.session"), + ApiId = telegramApiId, + ApiHash = telegramApiHash, IsActive = true, CreatedAt = DateTime.UtcNow, LastSyncAt = DateTime.UtcNow @@ -241,7 +292,10 @@ userId = accountInfo.TelegramUserId; // 跳转到完成步骤 - stepper.Next(); + if (stepper != null) + { + await stepper.NextStepAsync(); + } } catch (Exception ex) { diff --git a/src/TelegramPanel.Web/Components/Pages/Accounts.razor b/src/TelegramPanel.Web/Components/Pages/Accounts.razor index b30bdef..45b31e3 100644 --- a/src/TelegramPanel.Web/Components/Pages/Accounts.razor +++ b/src/TelegramPanel.Web/Components/Pages/Accounts.razor @@ -37,7 +37,7 @@ @if (context.Category != null) { - + @context.Category.Name } @@ -50,7 +50,7 @@ @context.Channels.Count / @context.Groups.Count - + @(context.IsActive ? "活跃" : "停用") @@ -115,18 +115,7 @@ private async Task ShowDetails(Account account) { var (channels, groups) = await AccountManagement.GetAccountStatisticsAsync(account.Id); - var message = $@" -账号详情: -手机号:{account.Phone} -用户名:{account.Username ?? "无"} -用户ID:{account.UserId} -分类:{account.Category?.Name ?? "未分类"} -创建的频道:{channels} 个 -创建的群组:{groups} 个 -创建时间:{account.CreatedAt:yyyy-MM-dd HH:mm} -最后同步:{account.LastSyncAt:yyyy-MM-dd HH:mm} -Session路径:{account.SessionPath} -"; + var message = $"账号详情:\n手机号:{account.Phone}\n用户名:{account.Username ?? "无"}\n用户ID:{account.UserId}\n分类:{account.Category?.Name ?? "未分类"}\n创建的频道:{channels} 个\n创建的群组:{groups} 个\n创建时间:{account.CreatedAt:yyyy-MM-dd HH:mm}\n最后同步:{account.LastSyncAt:yyyy-MM-dd HH:mm}\nSession路径:{account.SessionPath}"; await DialogService.ShowMessageBox("账号详情", message, "关闭"); } diff --git a/src/TelegramPanel.Web/Components/Pages/BatchAdmins.razor b/src/TelegramPanel.Web/Components/Pages/BatchAdmins.razor index 35cd09e..32fccd8 100644 --- a/src/TelegramPanel.Web/Components/Pages/BatchAdmins.razor +++ b/src/TelegramPanel.Web/Components/Pages/BatchAdmins.razor @@ -1,6 +1,11 @@ @page "/batch/admins" @inject IChannelService ChannelService +@inject AccountManagementService AccountManagement +@inject ChannelManagementService ChannelManagement +@inject BatchTaskManagementService TaskManagement @inject ISnackbar Snackbar +@using TelegramPanel.Data.Entities +@using TelegramPanel.Core.Interfaces 批量设管理 - Telegram Panel @@ -15,14 +20,18 @@ -- 请选择账号 -- - 账号1 (+86xxxxxxxx) - 账号2 (+86yyyyyyyy) + @foreach (var account in accounts) + { + @account.Phone (@account.Username) + } - -- 请选择频道 -- - 频道1 - 频道2 + -- 请选择频道 -- + @foreach (var channel in channels) + { + @channel.Title (@channel.Username) + } + HelperText="每行一个用户名" /> 清空 @@ -69,16 +77,16 @@ 设置结果 - + @foreach (var result in results) { -
@("@" + result.Username) - @if (!result.Success) + @if (!result.Success && result.Error != null) { - @result.ErrorMessage + @result.Error }
@@ -97,8 +105,10 @@ @code { + private List accounts = new(); + private List channels = new(); private int selectedAccountId = 0; - private long selectedChannelId = 0; + private int selectedChannelId = 0; private string adminTitle = "Admin"; private string usernames = ""; private bool processing = false; @@ -106,6 +116,38 @@ private AdminPermissions permissions = new(); private List results = new(); + protected override async Task OnInitializedAsync() + { + await LoadAccounts(); + await LoadChannels(); + } + + private async Task LoadAccounts() + { + try + { + var accountList = await AccountManagement.GetActiveAccountsAsync(); + accounts = accountList.ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error); + } + } + + private async Task LoadChannels() + { + try + { + var channelList = await ChannelManagement.GetAllChannelsAsync(); + channels = channelList.ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载频道列表失败:{ex.Message}", Severity.Error); + } + } + private async Task StartSetAdmins() { processing = true; @@ -113,13 +155,69 @@ try { + // 解析用户名列表 var usernameList = usernames.Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(u => u.Trim().TrimStart('@')) .Where(u => !string.IsNullOrEmpty(u)) .ToList(); - // TODO: 转换权限并调用 API - Snackbar.Add("批量设置管理员功能开发中", Severity.Info); + if (usernameList.Count == 0) + { + Snackbar.Add("请输入至少一个用户名", Severity.Warning); + return; + } + + // 获取选中的频道 + var channel = channels.FirstOrDefault(c => c.Id == selectedChannelId); + if (channel == null) + { + Snackbar.Add("未找到选中的频道", Severity.Error); + return; + } + + // 转换权限 + var rights = AdminRights.None; + if (permissions.ChangeInfo) rights |= AdminRights.ChangeInfo; + if (permissions.PostMessages) rights |= AdminRights.PostMessages; + if (permissions.EditMessages) rights |= AdminRights.EditMessages; + if (permissions.DeleteMessages) rights |= AdminRights.DeleteMessages; + if (permissions.BanUsers) rights |= AdminRights.BanUsers; + if (permissions.InviteUsers) rights |= AdminRights.InviteUsers; + if (permissions.PinMessages) rights |= AdminRights.PinMessages; + if (permissions.AddAdmins) rights |= AdminRights.AddAdmins; + + // 创建批量任务记录 + var task = new BatchTask + { + TaskType = "set_admin", + Total = usernameList.Count, + Completed = 0, + Failed = 0, + Config = System.Text.Json.JsonSerializer.Serialize(new + { + ChannelId = channel.TelegramId, + ChannelTitle = channel.Title, + Rights = rights.ToString(), + Title = adminTitle + }) + }; + var createdTask = await TaskManagement.CreateTaskAsync(task); + await TaskManagement.StartTaskAsync(createdTask.Id); + + // 创建管理员请求列表 + var adminRequests = usernameList.Select(u => new AdminRequest(u, rights, adminTitle)).ToList(); + + // 执行批量设置管理员 + var setResults = await ChannelService.BatchSetAdminsAsync(selectedAccountId, channel.TelegramId, adminRequests); + results.AddRange(setResults); + + // 更新任务状态 + var successCount = setResults.Count(r => r.Success); + var failedCount = setResults.Count(r => !r.Success); + await TaskManagement.UpdateTaskProgressAsync(createdTask.Id, successCount, failedCount); + await TaskManagement.CompleteTaskAsync(createdTask.Id, true); + + Snackbar.Add($"设置完成:{successCount}/{setResults.Count} 成功", successCount == setResults.Count ? Severity.Success : Severity.Warning); } catch (Exception ex) { diff --git a/src/TelegramPanel.Web/Components/Pages/BatchInvite.razor b/src/TelegramPanel.Web/Components/Pages/BatchInvite.razor index 9e57f53..cbef8dc 100644 --- a/src/TelegramPanel.Web/Components/Pages/BatchInvite.razor +++ b/src/TelegramPanel.Web/Components/Pages/BatchInvite.razor @@ -1,6 +1,10 @@ @page "/batch/invite" @inject IChannelService ChannelService +@inject AccountManagementService AccountManagement +@inject ChannelManagementService ChannelManagement +@inject BatchTaskManagementService TaskManagement @inject ISnackbar Snackbar +@using TelegramPanel.Data.Entities 批量邀请 - Telegram Panel @@ -15,19 +19,22 @@ -- 请选择账号 -- - 账号1 (+86xxxxxxxx) - 账号2 (+86yyyyyyyy) + @foreach (var account in accounts) + { + @account.Phone (@account.Username) + } - -- 请选择频道 -- - 频道1 - 频道2 + -- 请选择频道 -- + @foreach (var channel in channels) + { + @channel.Title (@channel.Username) + } + HelperText="每行一个用户名" /> @@ -58,16 +65,16 @@ 邀请结果 - + @foreach (var result in inviteResults) { -
@("@" + result.Username) - @if (!result.Success) + @if (!result.Success && result.Error != null) { - @result.ErrorMessage + @result.Error }
@@ -86,14 +93,48 @@ @code { + private List accounts = new(); + private List channels = new(); private int selectedAccountId = 0; - private long selectedChannelId = 0; + private int selectedChannelId = 0; private string usernames = ""; private int delayMs = 2000; private bool inviting = false; private List inviteResults = new(); + protected override async Task OnInitializedAsync() + { + await LoadAccounts(); + await LoadChannels(); + } + + private async Task LoadAccounts() + { + try + { + var accountList = await AccountManagement.GetActiveAccountsAsync(); + accounts = accountList.ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error); + } + } + + private async Task LoadChannels() + { + try + { + var channelList = await ChannelManagement.GetAllChannelsAsync(); + channels = channelList.ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载频道列表失败:{ex.Message}", Severity.Error); + } + } + private async Task StartInvite() { inviting = true; @@ -101,15 +142,53 @@ try { + // 解析用户名列表 var usernameList = usernames.Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(u => u.Trim().TrimStart('@')) .Where(u => !string.IsNullOrEmpty(u)) .ToList(); - var results = await ChannelService.BatchInviteUsersAsync(selectedAccountId, selectedChannelId, usernameList, delayMs); + if (usernameList.Count == 0) + { + Snackbar.Add("请输入至少一个用户名", Severity.Warning); + return; + } + + // 获取选中的频道 + var channel = channels.FirstOrDefault(c => c.Id == selectedChannelId); + if (channel == null) + { + Snackbar.Add("未找到选中的频道", Severity.Error); + return; + } + + // 创建批量任务记录 + var task = new BatchTask + { + TaskType = "invite", + Total = usernameList.Count, + Completed = 0, + Failed = 0, + Config = System.Text.Json.JsonSerializer.Serialize(new + { + ChannelId = channel.TelegramId, + ChannelTitle = channel.Title, + DelayMs = delayMs + }) + }; + var createdTask = await TaskManagement.CreateTaskAsync(task); + await TaskManagement.StartTaskAsync(createdTask.Id); + + // 执行批量邀请 + var results = await ChannelService.BatchInviteUsersAsync(selectedAccountId, channel.TelegramId, usernameList, delayMs); inviteResults.AddRange(results); + // 更新任务状态 var successCount = results.Count(r => r.Success); + var failedCount = results.Count(r => !r.Success); + await TaskManagement.UpdateTaskProgressAsync(createdTask.Id, successCount, failedCount); + await TaskManagement.CompleteTaskAsync(createdTask.Id, true); + Snackbar.Add($"邀请完成:{successCount}/{results.Count} 成功", successCount == results.Count ? Severity.Success : Severity.Warning); } catch (Exception ex) diff --git a/src/TelegramPanel.Web/Components/Pages/ChannelCreate.razor b/src/TelegramPanel.Web/Components/Pages/ChannelCreate.razor index 822de52..c5c8926 100644 --- a/src/TelegramPanel.Web/Components/Pages/ChannelCreate.razor +++ b/src/TelegramPanel.Web/Components/Pages/ChannelCreate.razor @@ -1,7 +1,10 @@ @page "/channels/create" @inject IChannelService ChannelService +@inject AccountManagementService AccountManagement +@inject ChannelManagementService ChannelManagement @inject ISnackbar Snackbar @inject NavigationManager Navigation +@using TelegramPanel.Data.Entities 创建频道 - Telegram Panel @@ -13,8 +16,10 @@ -- 请选择账号 -- - 账号1 (+86xxxxxxxx) - 账号2 (+86yyyyyyyy) + @foreach (var account in accounts) + { + @account.Phone (@account.Username) + } + Adornment="Adornment.Start" AdornmentText='@("@")' Required + HelperText="5-32个字符,只能包含a-z、0-9和下划线" /> } @@ -63,6 +68,7 @@ @code { + private List accounts = new(); private int selectedAccountId = 0; private string channelTitle = ""; private string channelAbout = ""; @@ -73,6 +79,24 @@ private bool signMessages = false; private bool creating = false; + protected override async Task OnInitializedAsync() + { + await LoadAccounts(); + } + + private async Task LoadAccounts() + { + try + { + var accountList = await AccountManagement.GetActiveAccountsAsync(); + accounts = accountList.ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error); + } + } + private async Task CreateChannel() { if (selectedAccountId == 0) @@ -97,13 +121,32 @@ try { + // 调用 Telegram API 创建频道 var channelInfo = await ChannelService.CreateChannelAsync(selectedAccountId, channelTitle, channelAbout, isPublic); + // 如果是公开频道,设置用户名 if (isPublic && !string.IsNullOrEmpty(username)) { await ChannelService.SetChannelVisibilityAsync(selectedAccountId, channelInfo.TelegramId, true, username); } + // 保存到数据库 + var channel = new Channel + { + TelegramId = channelInfo.TelegramId, + AccessHash = channelInfo.AccessHash, + Title = channelTitle, + Username = isPublic ? username : null, + IsBroadcast = channelInfo.IsBroadcast, + MemberCount = channelInfo.MemberCount, + About = channelAbout, + CreatorAccountId = selectedAccountId, + CreatedAt = DateTime.UtcNow, + SyncedAt = DateTime.UtcNow + }; + + await ChannelManagement.CreateOrUpdateChannelAsync(channel); + Snackbar.Add($"频道 \"{channelTitle}\" 创建成功", Severity.Success); Navigation.NavigateTo("/channels"); } diff --git a/src/TelegramPanel.Web/Components/Pages/ChannelGroups.razor b/src/TelegramPanel.Web/Components/Pages/ChannelGroups.razor index 2b13d5f..bea391f 100644 --- a/src/TelegramPanel.Web/Components/Pages/ChannelGroups.razor +++ b/src/TelegramPanel.Web/Components/Pages/ChannelGroups.razor @@ -1,5 +1,8 @@ @page "/channels/groups" +@inject ChannelGroupManagementService GroupManagement @inject ISnackbar Snackbar +@inject IDialogService DialogService +@using TelegramPanel.Data.Entities 频道分组 - Telegram Panel @@ -30,7 +33,7 @@ 分组列表 - + 分组名称 描述 @@ -39,14 +42,16 @@ @context.Name - @context.Description - @context.ChannelCount + @(context.Description ?? "-") + @context.Channels.Count - + OnClick="@(() => DeleteGroup(context))" /> + + 暂无分组,请添加分组 + @@ -56,45 +61,75 @@ @code { private string newGroupName = ""; private string newGroupDesc = ""; + private List groups = new(); + private bool loading = true; - private List groups = new() + protected override async Task OnInitializedAsync() { - new GroupModel { Id = 1, Name = "推广频道", Description = "用于推广的频道", ChannelCount = 5 }, - new GroupModel { Id = 2, Name = "资讯频道", Description = "资讯类频道", ChannelCount = 3 } - }; - - private void AddGroup() - { - var newGroup = new GroupModel - { - Id = groups.Max(g => g.Id) + 1, - Name = newGroupName, - Description = newGroupDesc, - ChannelCount = 0 - }; - - groups.Add(newGroup); - Snackbar.Add($"分组 \"{newGroupName}\" 添加成功", Severity.Success); - - newGroupName = ""; - newGroupDesc = ""; + await LoadGroups(); } - private void DeleteGroup(int id) + private async Task LoadGroups() { - var group = groups.FirstOrDefault(g => g.Id == id); - if (group != null) + loading = true; + try { - groups.Remove(group); - Snackbar.Add("分组已删除", Severity.Success); + var result = await GroupManagement.GetAllGroupsAsync(); + groups = result.ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载失败:{ex.Message}", Severity.Error); + } + finally + { + loading = false; } } - private class GroupModel + private async Task AddGroup() { - public int Id { get; set; } - public string Name { get; set; } = ""; - public string Description { get; set; } = ""; - public int ChannelCount { get; set; } + try + { + var group = new ChannelGroup + { + Name = newGroupName, + Description = newGroupDesc, + CreatedAt = DateTime.UtcNow + }; + + await GroupManagement.CreateGroupAsync(group); + await LoadGroups(); + Snackbar.Add($"分组 \"{newGroupName}\" 添加成功", Severity.Success); + + newGroupName = ""; + newGroupDesc = ""; + } + catch (Exception ex) + { + Snackbar.Add($"添加失败:{ex.Message}", Severity.Error); + } + } + + private async Task DeleteGroup(ChannelGroup group) + { + bool? result = await DialogService.ShowMessageBox( + "确认删除", + $"确定要删除分组 {group.Name} 吗?关联的频道将变为未分组。", + yesText: "删除", cancelText: "取消"); + + if (result == true) + { + try + { + await GroupManagement.DeleteGroupAsync(group.Id); + groups.Remove(group); + Snackbar.Add("删除成功", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"删除失败:{ex.Message}", Severity.Error); + } + } } } diff --git a/src/TelegramPanel.Web/Components/Pages/Channels.razor b/src/TelegramPanel.Web/Components/Pages/Channels.razor index cac547a..6fb3ca1 100644 --- a/src/TelegramPanel.Web/Components/Pages/Channels.razor +++ b/src/TelegramPanel.Web/Components/Pages/Channels.razor @@ -1,7 +1,10 @@ @page "/channels" -@inject IChannelService ChannelService +@inject ChannelManagementService ChannelManagement +@inject AccountManagementService AccountManagement @inject ISnackbar Snackbar +@inject IDialogService DialogService @inject NavigationManager Navigation +@using TelegramPanel.Data.Entities 频道列表 - Telegram Panel @@ -9,19 +12,21 @@ - + - 我的频道 + 我的频道 (@channels.Count) 全部账号 - 账号1 (+86xxx) - 账号2 (+86yyy) + @foreach (var account in accounts) + { + @account.Phone (@account.Username) + } - 全部 - 公开 - 私密 + 全部 + 公开 + 私密 @@ -43,7 +48,7 @@ @context.Title @if (!string.IsNullOrEmpty(context.About)) { - @context.About + @(context.About.Length > 50 ? context.About.Substring(0, 50) + "..." : context.About) } @@ -51,7 +56,7 @@ @if (!string.IsNullOrEmpty(context.Username)) { - @context.Username + @context.Username } else { @@ -59,12 +64,21 @@ } - + @(string.IsNullOrEmpty(context.Username) ? "私密" : "公开") @context.MemberCount - 账号 @context.CreatorAccountId + + @if (context.CreatorAccount != null) + { + @context.CreatorAccount.Phone + } + else + { + 账号 @context.CreatorAccountId + } + @context.SyncedAt.ToString("yyyy-MM-dd HH:mm") @@ -88,7 +102,8 @@ @code { - private List channels = new(); + private List channels = new(); + private List accounts = new(); private bool loading = true; private string searchString = ""; private int filterAccount = 0; @@ -96,17 +111,30 @@ protected override async Task OnInitializedAsync() { + await LoadAccounts(); await LoadChannels(); } + private async Task LoadAccounts() + { + try + { + var accountList = await AccountManagement.GetAllAccountsAsync(); + accounts = accountList.ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error); + } + } + private async Task LoadChannels() { loading = true; try { - // TODO: 从数据库加载频道列表 - channels = new List(); - Snackbar.Add("频道列表加载成功", Severity.Success); + var channelList = await ChannelManagement.GetAllChannelsAsync(); + channels = channelList.ToList(); } catch (Exception ex) { @@ -118,7 +146,7 @@ } } - private bool FilterFunc(ChannelInfo channel) + private bool FilterFunc(Channel channel) { if (filterAccount > 0 && channel.CreatorAccountId != filterAccount) return false; @@ -141,22 +169,22 @@ return false; } - private void ShowDetails(ChannelInfo channel) + private void ShowDetails(Channel channel) { Snackbar.Add($"查看频道详情:{channel.Title}", Severity.Info); } - private void EditChannel(ChannelInfo channel) + private void EditChannel(Channel channel) { Snackbar.Add($"编辑频道:{channel.Title}", Severity.Info); } - private void InviteUsers(ChannelInfo channel) + private void InviteUsers(Channel channel) { Navigation.NavigateTo($"/batch/invite?channelId={channel.Id}"); } - private void CopyLink(ChannelInfo channel) + private void CopyLink(Channel channel) { var link = string.IsNullOrEmpty(channel.Username) ? $"私密频道(邀请链接待实现)" @@ -166,6 +194,26 @@ private async Task DeleteChannel(int id) { - Snackbar.Add("删除功能开发中", Severity.Warning); + var channel = channels.FirstOrDefault(c => c.Id == id); + if (channel == null) return; + + bool? result = await DialogService.ShowMessageBox( + "确认删除", + $"确定要删除频道 {channel.Title} 吗?此操作不可恢复。", + yesText: "删除", cancelText: "取消"); + + if (result == true) + { + try + { + await ChannelManagement.DeleteChannelAsync(id); + channels.Remove(channel); + Snackbar.Add("删除成功", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"删除失败:{ex.Message}", Severity.Error); + } + } } } diff --git a/src/TelegramPanel.Web/Components/Pages/Groups.razor b/src/TelegramPanel.Web/Components/Pages/Groups.razor index f468845..d347513 100644 --- a/src/TelegramPanel.Web/Components/Pages/Groups.razor +++ b/src/TelegramPanel.Web/Components/Pages/Groups.razor @@ -1,6 +1,10 @@ @page "/groups" -@inject IGroupService GroupService +@inject GroupManagementService GroupManagement +@inject AccountManagementService AccountManagement @inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject NavigationManager Navigation +@using TelegramPanel.Data.Entities 群组列表 - Telegram Panel @@ -8,10 +12,17 @@ - + - 我的群组 + 我的群组 (@groups.Count) + + 全部账号 + @foreach (var account in accounts) + { + @account.Phone (@account.Username) + } + @@ -27,13 +38,19 @@
@context.Title[0] - @context.Title +
+ @context.Title + @if (!string.IsNullOrEmpty(context.About)) + { + @(context.About.Length > 50 ? context.About.Substring(0, 50) + "..." : context.About) + } +
@if (!string.IsNullOrEmpty(context.Username)) { - @context.Username + @context.Username } else { @@ -41,15 +58,24 @@ } @context.MemberCount - 账号 @context.CreatorAccountId + + @if (context.CreatorAccount != null) + { + @context.CreatorAccount.Phone + } + else + { + 账号 @context.CreatorAccountId + } + @context.SyncedAt.ToString("yyyy-MM-dd HH:mm") - 查看详情 - 邀请成员 - 复制链接 + 查看详情 + 邀请成员 + 复制链接 - 删除群组 + 删除群组 @@ -64,23 +90,38 @@
@code { - private List groups = new(); + private List groups = new(); + private List accounts = new(); private bool loading = true; private string searchString = ""; + private int filterAccount = 0; protected override async Task OnInitializedAsync() { + await LoadAccounts(); await LoadGroups(); } + private async Task LoadAccounts() + { + try + { + var accountList = await AccountManagement.GetAllAccountsAsync(); + accounts = accountList.ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载账号列表失败:{ex.Message}", Severity.Error); + } + } + private async Task LoadGroups() { loading = true; try { - // TODO: 从数据库加载群组列表 - groups = new List(); - Snackbar.Add("群组列表加载成功", Severity.Success); + var groupList = await GroupManagement.GetAllGroupsAsync(); + groups = groupList.ToList(); } catch (Exception ex) { @@ -91,4 +132,64 @@ loading = false; } } + + private bool FilterFunc(Group group) + { + if (filterAccount > 0 && group.CreatorAccountId != filterAccount) + return false; + + if (string.IsNullOrWhiteSpace(searchString)) + return true; + + if (group.Title.Contains(searchString, StringComparison.OrdinalIgnoreCase)) + return true; + + if (group.Username?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true) + return true; + + return false; + } + + private void ShowDetails(Group group) + { + Snackbar.Add($"查看群组详情:{group.Title}", Severity.Info); + } + + private void InviteMembers(Group group) + { + Navigation.NavigateTo($"/batch/invite?groupId={group.Id}"); + } + + private void CopyLink(Group group) + { + var link = string.IsNullOrEmpty(group.Username) + ? $"私密群组(邀请链接待实现)" + : $"https://t.me/{group.Username}"; + Snackbar.Add($"链接:{link}", Severity.Info); + } + + private async Task DeleteGroup(int id) + { + var group = groups.FirstOrDefault(g => g.Id == id); + if (group == null) return; + + bool? result = await DialogService.ShowMessageBox( + "确认删除", + $"确定要删除群组 {group.Title} 吗?此操作不可恢复。", + yesText: "删除", cancelText: "取消"); + + if (result == true) + { + try + { + await GroupManagement.DeleteGroupAsync(id); + groups.Remove(group); + Snackbar.Add("删除成功", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"删除失败:{ex.Message}", Severity.Error); + } + } + } } diff --git a/src/TelegramPanel.Web/Components/Pages/Settings.razor b/src/TelegramPanel.Web/Components/Pages/Settings.razor index 5605f25..84a525b 100644 --- a/src/TelegramPanel.Web/Components/Pages/Settings.razor +++ b/src/TelegramPanel.Web/Components/Pages/Settings.razor @@ -1,5 +1,13 @@ @page "/settings" +@inject IConfiguration Configuration +@inject AccountManagementService AccountManagement +@inject ChannelManagementService ChannelManagement +@inject GroupManagementService GroupManagement +@inject IChannelService ChannelService +@inject IGroupService GroupService @inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject IWebHostEnvironment Environment 系统设置 - Telegram Panel @@ -16,9 +24,15 @@ HelperText="从 https://my.telegram.org 获取" /> + + 写入位置:@LocalConfigPath + 文件存在:@(LocalConfigExists ? "是" : "否") + 当前生效 ApiId:@(Configuration["Telegram:ApiId"] ?? "(未配置)") +
- + 保存配置 @@ -41,7 +55,8 @@ }
- + 保存配置 @@ -61,7 +76,7 @@ Min="1" Max="24" /> } + OnClick="@(async (MouseEventArgs _) => await SyncNow())"> 立即同步
@@ -73,16 +88,17 @@ - Debug - Information - Warning - Error + Debug + Information + Warning + Error - + 保存配置 @@ -98,7 +114,8 @@ 数据库: SQLite + StartIcon="@Icons.Material.Filled.Delete" + OnClick="@(async (MouseEventArgs _) => await ClearCache())"> 清除缓存
@@ -117,31 +134,168 @@ private int syncInterval = 6; private string logLevel = "Information"; private int logRetentionDays = 30; + private bool syncing = false; - private void SaveApiConfig() + private string LocalConfigPath => System.IO.Path.Combine(Environment.ContentRootPath, "appsettings.local.json"); + private bool LocalConfigExists => System.IO.File.Exists(LocalConfigPath); + + protected override void OnInitialized() { - Snackbar.Add("API 配置已保存", Severity.Success); + // 从配置文件加载默认设置 + apiId = Configuration["Telegram:ApiId"] ?? ""; + apiHash = Configuration["Telegram:ApiHash"] ?? ""; } - private void SaveBatchConfig() + private async Task SaveApiConfig() { + if (!int.TryParse(apiId, out int parsedApiId) || parsedApiId <= 0) + { + Snackbar.Add("默认 API ID 格式错误", Severity.Error); + return; + } + + if (string.IsNullOrWhiteSpace(apiHash)) + { + Snackbar.Add("默认 API Hash 不能为空", Severity.Error); + return; + } + + var localSettings = new + { + Telegram = new + { + ApiId = parsedApiId, + ApiHash = apiHash.Trim() + } + }; + + try + { + var localConfigPath = LocalConfigPath; + var json = System.Text.Json.JsonSerializer.Serialize( + localSettings, + new System.Text.Json.JsonSerializerOptions { WriteIndented = true } + ); + + await System.IO.File.WriteAllTextAsync( + localConfigPath, + json, + new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false) + ); + + var fileInfo = new System.IO.FileInfo(localConfigPath); + Snackbar.Add($"API 配置已保存:{localConfigPath}({fileInfo.Length} bytes)。若仍提示未配置,请刷新页面或重启应用。", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"保存失败:{ex.Message}(路径:{LocalConfigPath})", Severity.Error); + } + } + + private Task SaveBatchConfig() + { + // TODO: 实现配置保存到文件 Snackbar.Add("批量操作配置已保存", Severity.Success); + return Task.CompletedTask; } - private void SaveLogConfig() + private Task SaveLogConfig() { + // TODO: 实现配置保存到文件 Snackbar.Add("日志配置已保存", Severity.Success); + return Task.CompletedTask; } private async Task SyncNow() { - Snackbar.Add("开始同步...", Severity.Info); - await Task.Delay(2000); - Snackbar.Add("同步完成", Severity.Success); + if (syncing) + { + Snackbar.Add("正在同步中,请稍候...", Severity.Warning); + return; + } + + syncing = true; + Snackbar.Add("开始同步频道和群组信息...", Severity.Info); + + try + { + var accounts = await AccountManagement.GetActiveAccountsAsync(); + int totalChannelsSynced = 0; + int totalGroupsSynced = 0; + + foreach (var account in accounts) + { + try + { + // 同步频道 + var channels = await ChannelService.GetOwnedChannelsAsync(account.Id); + foreach (var channelInfo in channels) + { + var channel = new Data.Entities.Channel + { + TelegramId = channelInfo.TelegramId, + AccessHash = channelInfo.AccessHash, + Title = channelInfo.Title, + Username = channelInfo.Username, + IsBroadcast = channelInfo.IsBroadcast, + MemberCount = channelInfo.MemberCount, + About = channelInfo.About, + CreatorAccountId = account.Id + }; + await ChannelManagement.CreateOrUpdateChannelAsync(channel); + totalChannelsSynced++; + } + + // 同步群组 + var groups = await GroupService.GetOwnedGroupsAsync(account.Id); + foreach (var groupInfo in groups) + { + var group = new Data.Entities.Group + { + TelegramId = groupInfo.TelegramId, + AccessHash = groupInfo.AccessHash, + Title = groupInfo.Title, + Username = groupInfo.Username, + MemberCount = groupInfo.MemberCount, + About = null, + CreatorAccountId = account.Id + }; + await GroupManagement.CreateOrUpdateGroupAsync(group); + totalGroupsSynced++; + } + + // 更新账号的最后同步时间 + await AccountManagement.UpdateLastSyncTimeAsync(account.Id); + } + catch (Exception ex) + { + Snackbar.Add($"账号 {account.Phone} 同步失败:{ex.Message}", Severity.Warning); + } + } + + Snackbar.Add($"同步完成:{totalChannelsSynced} 个频道,{totalGroupsSynced} 个群组", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"同步失败:{ex.Message}", Severity.Error); + } + finally + { + syncing = false; + } } - private void ClearCache() + private async Task ClearCache() { - Snackbar.Add("缓存已清除", Severity.Success); + bool? result = await DialogService.ShowMessageBox( + "确认清除", + "确定要清除缓存吗?这将清除所有临时数据。", + yesText: "清除", cancelText: "取消"); + + if (result == true) + { + // TODO: 实现缓存清除逻辑 + Snackbar.Add("缓存已清除", Severity.Success); + } } } diff --git a/src/TelegramPanel.Web/Components/Pages/Tasks.razor b/src/TelegramPanel.Web/Components/Pages/Tasks.razor index df44a09..14ab4e6 100644 --- a/src/TelegramPanel.Web/Components/Pages/Tasks.razor +++ b/src/TelegramPanel.Web/Components/Pages/Tasks.razor @@ -1,5 +1,8 @@ @page "/tasks" +@inject BatchTaskManagementService TaskManagement @inject ISnackbar Snackbar +@inject IDialogService DialogService +@using TelegramPanel.Data.Entities 任务中心 - Telegram Panel @@ -7,49 +10,63 @@ - + - 任务列表 + 任务列表 (@tasks.Count) - 全部 - 待执行 - 执行中 - 已完成 - 失败 + 全部 + 待执行 + 执行中 + 已完成 + 失败 任务ID 任务类型 - 目标 状态 进度 创建时间 + 完成时间 操作 @context.Id - @context.Type + @GetTaskTypeName(context.TaskType) - @context.Target - @context.Status + @GetStatusName(context.Status) - - @context.Progress% + + @context.Completed / @context.Total (@context.Failed 失败) @context.CreatedAt.ToString("yyyy-MM-dd HH:mm") + + @if (context.CompletedAt.HasValue) + { + @context.CompletedAt.Value.ToString("yyyy-MM-dd HH:mm") + } + else + { + - + } + - @if (context.Status == "待执行" || context.Status == "执行中") + @if (context.Status == "pending" || context.Status == "running") { } + @if (context.Status == "completed" || context.Status == "failed") + { + + } @@ -60,48 +77,144 @@ @code { + private List tasks = new(); + private bool loading = true; private string filterStatus = "all"; - private List tasks = new() + private IEnumerable filteredTasks => + filterStatus == "all" ? tasks : tasks.Where(t => t.Status == filterStatus); + + protected override async Task OnInitializedAsync() { - new TaskModel { Id = 1, Type = "批量邀请", Target = "频道1", Status = "已完成", Progress = 100, CreatedAt = DateTime.Now.AddHours(-2) }, - new TaskModel { Id = 2, Type = "设置管理", Target = "频道2", Status = "执行中", Progress = 60, CreatedAt = DateTime.Now.AddMinutes(-30) }, - new TaskModel { Id = 3, Type = "批量邀请", Target = "频道3", Status = "待执行", Progress = 0, CreatedAt = DateTime.Now.AddMinutes(-5) } - }; + await LoadTasks(); + } + + private async Task LoadTasks() + { + loading = true; + try + { + var taskList = await TaskManagement.GetRecentTasksAsync(50); + tasks = taskList.OrderByDescending(t => t.CreatedAt).ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载失败:{ex.Message}", Severity.Error); + } + finally + { + loading = false; + } + } + + private double GetProgress(BatchTask task) + { + if (task.Total == 0) return 0; + return (double)task.Completed / task.Total * 100; + } private string GetTaskIcon(string type) => type switch { - "批量邀请" => Icons.Material.Filled.PersonAdd, - "设置管理" => Icons.Material.Filled.AdminPanelSettings, + "invite" => Icons.Material.Filled.PersonAdd, + "set_admin" => Icons.Material.Filled.AdminPanelSettings, _ => Icons.Material.Filled.Task }; + private string GetTaskTypeName(string type) => type switch + { + "invite" => "批量邀请", + "set_admin" => "设置管理员", + _ => type + }; + + private string GetStatusName(string status) => status switch + { + "pending" => "待执行", + "running" => "执行中", + "completed" => "已完成", + "failed" => "失败", + _ => status + }; + private Color GetStatusColor(string status) => status switch { - "已完成" => Color.Success, - "执行中" => Color.Info, - "待执行" => Color.Default, - "失败" => Color.Error, + "completed" => Color.Success, + "running" => Color.Info, + "pending" => Color.Default, + "failed" => Color.Error, _ => Color.Default }; - private void ShowTaskDetails(TaskModel task) + private Color GetProgressColor(string status) => status switch { - Snackbar.Add($"查看任务详情:{task.Type} - {task.Target}", Severity.Info); + "completed" => Color.Success, + "running" => Color.Primary, + "failed" => Color.Error, + _ => Color.Default + }; + + private void ShowTaskDetails(BatchTask task) + { + var details = $"任务类型: {GetTaskTypeName(task.TaskType)}\n" + + $"状态: {GetStatusName(task.Status)}\n" + + $"总数: {task.Total}\n" + + $"已完成: {task.Completed}\n" + + $"失败: {task.Failed}\n" + + $"创建时间: {task.CreatedAt:yyyy-MM-dd HH:mm}\n"; + + if (task.StartedAt.HasValue) + details += $"开始时间: {task.StartedAt.Value:yyyy-MM-dd HH:mm}\n"; + + if (task.CompletedAt.HasValue) + details += $"完成时间: {task.CompletedAt.Value:yyyy-MM-dd HH:mm}\n"; + + if (!string.IsNullOrEmpty(task.Config)) + details += $"\n配置信息:\n{task.Config}"; + + DialogService.ShowMessageBox("任务详情", details); } - private void CancelTask(int id) + private async Task CancelTask(int id) { - Snackbar.Add($"取消任务 #{id}", Severity.Warning); + bool? result = await DialogService.ShowMessageBox( + "确认取消", + "确定要取消这个任务吗?", + yesText: "取消任务", cancelText: "返回"); + + if (result == true) + { + try + { + await TaskManagement.CompleteTaskAsync(id, false); + await LoadTasks(); + Snackbar.Add("任务已取消", Severity.Warning); + } + catch (Exception ex) + { + Snackbar.Add($"取消失败:{ex.Message}", Severity.Error); + } + } } - private class TaskModel + private async Task DeleteTask(int id) { - public int Id { get; set; } - public string Type { get; set; } = ""; - public string Target { get; set; } = ""; - public string Status { get; set; } = ""; - public int Progress { get; set; } - public DateTime CreatedAt { get; set; } + bool? result = await DialogService.ShowMessageBox( + "确认删除", + "确定要删除这个任务记录吗?此操作不可恢复。", + yesText: "删除", cancelText: "取消"); + + if (result == true) + { + try + { + await TaskManagement.DeleteTaskAsync(id); + tasks.RemoveAll(t => t.Id == id); + Snackbar.Add("任务已删除", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"删除失败:{ex.Message}", Severity.Error); + } + } } } diff --git a/src/TelegramPanel.Web/Components/_Imports.razor b/src/TelegramPanel.Web/Components/_Imports.razor new file mode 100644 index 0000000..e08bc5a --- /dev/null +++ b/src/TelegramPanel.Web/Components/_Imports.razor @@ -0,0 +1,17 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Hosting +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using MudBlazor +@using MudBlazor.Utilities +@using TelegramPanel.Core.Interfaces +@using TelegramPanel.Core.Services +@using TelegramPanel.Core.Services.Telegram +@using TelegramPanel.Web.Components +@using TelegramPanel.Web.Components.Layout diff --git a/src/TelegramPanel.Web/Program.cs b/src/TelegramPanel.Web/Program.cs index 09463a1..834081d 100644 --- a/src/TelegramPanel.Web/Program.cs +++ b/src/TelegramPanel.Web/Program.cs @@ -1,10 +1,14 @@ using MudBlazor.Services; +using Microsoft.EntityFrameworkCore; using Serilog; using TelegramPanel.Core; using TelegramPanel.Data; var builder = WebApplication.CreateBuilder(args); +// 可选的本地覆盖配置(不要提交到仓库) +builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true); + // 配置 Serilog Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) @@ -23,8 +27,30 @@ builder.Services.AddRazorComponents() builder.Services.AddMudServices(); // 数据库上下文 -var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") +var configuredConnectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=telegram-panel.db"; + +// 统一 SQLite 数据库路径为 ContentRoot 下的文件,避免因工作目录不同导致连到错误的 db(从而出现 no such table) +var connectionString = configuredConnectionString; +try +{ + var dataSourcePrefix = "Data Source="; + var trimmed = configuredConnectionString.Trim(); + if (trimmed.StartsWith(dataSourcePrefix, StringComparison.OrdinalIgnoreCase)) + { + var dataSource = trimmed.Substring(dataSourcePrefix.Length).Trim().Trim('"'); + if (!Path.IsPathRooted(dataSource)) + { + var absolute = Path.Combine(builder.Environment.ContentRootPath, dataSource); + connectionString = $"{dataSourcePrefix}{absolute}"; + } + } +} +catch (Exception ex) +{ + Log.Warning(ex, "Failed to normalize sqlite connection string, using configured value"); + connectionString = configuredConnectionString; +} builder.Services.AddTelegramPanelData(connectionString); // Telegram Panel 核心服务 @@ -36,6 +62,91 @@ builder.Services.AddTelegramPanelCore(); var app = builder.Build(); +// 确保数据库已创建并应用最新迁移 +// 注意:SQLite 在首次连接时只会创建空文件,不会自动建表,因此需要显式执行迁移。 +using (var scope = app.Services.CreateScope()) +{ + try + { + var db = scope.ServiceProvider.GetRequiredService(); + Log.Information("Using sqlite connection string: {ConnectionString}", connectionString); + var migrations = db.Database.GetMigrations().ToList(); + Log.Information("EF migrations discovered: {Count}", migrations.Count); + + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + List tables; + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"; + using var reader = cmd.ExecuteReader(); + tables = new List(); + while (reader.Read()) + tables.Add(reader.GetString(0)); + } + + var hasHistory = tables.Contains("__EFMigrationsHistory", StringComparer.Ordinal); + var hasAnyUserTables = tables.Any(t => + !string.Equals(t, "__EFMigrationsHistory", StringComparison.Ordinal) && + !t.StartsWith("sqlite_", StringComparison.OrdinalIgnoreCase)); + + if (migrations.Count > 0) + { + // 只在「新库」或「已有迁移历史」时执行 Migrate;避免对已有表但无历史的库误执行迁移导致冲突 + if (!hasAnyUserTables || hasHistory) + { + db.Database.Migrate(); + } + else + { + Log.Warning("Database has schema tables but no __EFMigrationsHistory; skipping Migrate() to avoid conflicts"); + } + } + else + { + db.Database.EnsureCreated(); + } + + // 刷新表清单(上面可能已创建/迁移) + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"; + using var reader = cmd.ExecuteReader(); + tables = new List(); + while (reader.Read()) + tables.Add(reader.GetString(0)); + } + + // 兜底:若 Accounts 仍不存在(常见于库被创建为空文件/仅有历史表),给出可恢复的自愈 + if (!tables.Contains("Accounts", StringComparer.Ordinal)) + { + hasHistory = tables.Contains("__EFMigrationsHistory", StringComparer.Ordinal); + if (tables.Count == 0) + { + Log.Warning("Database has no tables; calling EnsureCreated()"); + db.Database.EnsureCreated(); + } + else if (tables.Count == 1 && hasHistory) + { + Log.Warning("Database only contains __EFMigrationsHistory but no schema tables; recreating schema via EnsureCreated()"); + db.Database.ExecuteSqlRaw("DROP TABLE IF EXISTS __EFMigrationsHistory;"); + db.Database.EnsureCreated(); + } + else + { + Log.Error("Accounts table missing. Existing tables: {Tables}", string.Join(", ", tables)); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Database migration failed"); + throw; + } +} + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { diff --git a/src/TelegramPanel.Web/_Imports.razor b/src/TelegramPanel.Web/_Imports.razor index d6389e5..1c6d116 100644 --- a/src/TelegramPanel.Web/_Imports.razor +++ b/src/TelegramPanel.Web/_Imports.razor @@ -1,5 +1,6 @@ @using System.Net.Http @using System.Net.Http.Json +@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @@ -7,5 +8,9 @@ @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using MudBlazor +@using MudBlazor.Utilities +@using TelegramPanel.Core.Interfaces +@using TelegramPanel.Core.Services +@using TelegramPanel.Core.Services.Telegram @using TelegramPanel.Web.Components @using TelegramPanel.Web.Components.Layout