diff --git a/src/TelegramPanel.Core/BatchTasks/BatchTaskTypes.cs b/src/TelegramPanel.Core/BatchTasks/BatchTaskTypes.cs
new file mode 100644
index 0000000..7de486d
--- /dev/null
+++ b/src/TelegramPanel.Core/BatchTasks/BatchTaskTypes.cs
@@ -0,0 +1,15 @@
+namespace TelegramPanel.Core.BatchTasks;
+
+///
+/// 批量任务类型常量(数据库中 BatchTask.TaskType 的取值)。
+///
+public static class BatchTaskTypes
+{
+ // Bot 任务(现有)
+ public const string Invite = "invite";
+ public const string SetAdmin = "set_admin";
+
+ // User 任务(新增)
+ public const string UserJoinSubscribe = "user_join_subscribe";
+}
+
diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs
index fcc8cc4..f9b3564 100644
--- a/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs
+++ b/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs
@@ -635,6 +635,86 @@ public class AccountTelegramToolsService
}
}
+ ///
+ /// 通过链接/用户名加入群组或订阅频道(支持 https://t.me/xxx、t.me/+hash、@username、username、tg://join?invite=hash 等)。
+ ///
+ public async Task<(bool Success, string? Error, string? JoinedTitle)> JoinChatOrChannelAsync(
+ int accountId,
+ string linkOrUsername,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var raw = (linkOrUsername ?? string.Empty).Trim();
+ if (string.IsNullOrWhiteSpace(raw))
+ return (false, "链接/用户名为空", null);
+
+ var url = NormalizeTelegramJoinUrl(raw);
+
+ var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var chat = await client.AnalyzeInviteLink(url, join: true);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var title = chat switch
+ {
+ Channel c => c.title,
+ Chat c => c.title,
+ _ => null
+ };
+
+ return (true, null, title);
+ }
+ catch (RpcException ex) when (ex.Code == 400 && string.Equals(ex.Message, "USER_ALREADY_PARTICIPANT", StringComparison.OrdinalIgnoreCase))
+ {
+ return (true, null, "已在群组/频道中");
+ }
+ catch (Exception ex)
+ {
+ var (summary, details) = MapTelegramException(ex);
+ var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
+ return (false, msg, null);
+ }
+ }
+
+ private static string NormalizeTelegramJoinUrl(string input)
+ {
+ var s = (input ?? string.Empty).Trim();
+ if (string.IsNullOrWhiteSpace(s))
+ throw new ArgumentException("链接/用户名为空", nameof(input));
+
+ // tg://join?invite=xxxx
+ if (s.StartsWith("tg://", StringComparison.OrdinalIgnoreCase))
+ {
+ var inviteKey = "invite=";
+ var idx = s.IndexOf(inviteKey, StringComparison.OrdinalIgnoreCase);
+ if (idx >= 0)
+ {
+ var hash = s[(idx + inviteKey.Length)..];
+ var amp = hash.IndexOf('&');
+ if (amp >= 0)
+ hash = hash[..amp];
+ hash = hash.Trim();
+ if (!string.IsNullOrWhiteSpace(hash))
+ return $"https://t.me/+{hash}";
+ }
+ }
+
+ // 直接是 t.me/xxx
+ if (s.StartsWith("t.me/", StringComparison.OrdinalIgnoreCase) || s.StartsWith("telegram.me/", StringComparison.OrdinalIgnoreCase))
+ return "https://" + s;
+
+ // @username / username
+ if (s.StartsWith("@", StringComparison.Ordinal))
+ s = s.TrimStart('@');
+
+ if (!s.Contains("://", StringComparison.Ordinal))
+ return $"https://t.me/{s}";
+
+ return s;
+ }
+
///
/// 更新当前账号头像(静态图片)。
///
diff --git a/src/TelegramPanel.Web/Components/Dialogs/CreateBatchTaskDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/CreateBatchTaskDialog.razor
new file mode 100644
index 0000000..d79026d
--- /dev/null
+++ b/src/TelegramPanel.Web/Components/Dialogs/CreateBatchTaskDialog.razor
@@ -0,0 +1,324 @@
+@namespace TelegramPanel.Web.Components.Dialogs
+@inject AccountManagementService AccountManagement
+@inject TelegramPanel.Core.Services.AccountRiskService RiskService
+@inject BatchTaskManagementService TaskManagement
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@inject NavigationManager Navigation
+@using System.Text.Json
+@using TelegramPanel.Core.BatchTasks
+@using TelegramPanel.Data.Entities
+
+
+
+
+
+ 任务会提交到后台执行,可在【任务中心】查看进度与结果。
+
+
+
+
+
+ 用户任务
+ Bot 任务
+
+
+
+
+ @foreach (var opt in AvailableOptions)
+ {
+ @opt.DisplayName
+ }
+
+
+
+
+ @if (CurrentOption != null && !string.IsNullOrWhiteSpace(CurrentOption.Description))
+ {
+ @CurrentOption.Description
+ }
+
+ @if (selectedTaskType == BatchTaskTypes.UserJoinSubscribe)
+ {
+
+
+
+ 批量订阅频道/加群(用户任务):支持输入多个链接/用户名,换行分隔。支持:https://t.me/xxx、t.me/+hash、@username、username、tg://join?invite=hash。
+
+
+
+
+
+
+
+ 选择执行账号:@selectedAccounts.Count / @accounts.Count
+
+ 全选
+ 清空
+
+
+
+
+
+
+ 手机号
+ 昵称
+ 用户名
+
+
+ @context.Phone
+ @(string.IsNullOrWhiteSpace(context.Nickname) ? "-" : context.Nickname)
+ @(string.IsNullOrWhiteSpace(context.Username) ? "-" : "@" + context.Username)
+
+
+ 暂无账号
+
+
+ }
+ else if (selectedTaskType == BatchTaskTypes.Invite)
+ {
+
+ 当前版本暂未在任务中心内置“批量邀请”的创建表单,请使用“批量邀请”页面创建任务。
+
+
+ 前往批量邀请
+
+ }
+ else if (selectedTaskType == BatchTaskTypes.SetAdmin)
+ {
+
+ 当前版本暂未在任务中心内置“设置管理员”的创建表单,请使用“批量设置管理员”页面创建任务。
+
+
+ 前往批量设置管理员
+
+ }
+
+
+
+ 关闭
+
+ @if (creating)
+ {
+
+ 提交中...
+ }
+ else
+ {
+ 提交任务
+ }
+
+
+
+
+@code
+{
+ [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
+
+ private sealed record TaskOption(string Category, string TaskType, string DisplayName, string? Description);
+
+ private static readonly List Options = new()
+ {
+ new("bot", BatchTaskTypes.Invite, "批量邀请", "对所选频道批量邀请用户名列表(Bot/执行账号需具备权限)。"),
+ new("bot", BatchTaskTypes.SetAdmin, "设置管理员", "对所选频道批量设置管理员(Bot/执行账号需具备权限)。"),
+ new("user", BatchTaskTypes.UserJoinSubscribe, "批量订阅/加群", "用户账号批量加入群组/订阅频道(Bot 无法主动加入)。")
+ };
+
+ private string _selectedCategory = "user";
+ private string selectedCategory
+ {
+ get => _selectedCategory;
+ set
+ {
+ if (string.Equals(_selectedCategory, value, StringComparison.Ordinal))
+ return;
+ _selectedCategory = value;
+ EnsureTaskTypeValid();
+ }
+ }
+ private string selectedTaskType = BatchTaskTypes.UserJoinSubscribe;
+
+ private bool creating;
+
+ // join/subscribe config
+ private string linksText = "";
+ private int delayMs = 2000;
+
+ private bool loadingAccounts;
+ private List accounts = new();
+ private string accountSearch = "";
+ private HashSet selectedAccounts = new();
+
+ private IEnumerable AvailableOptions => Options.Where(o => o.Category == selectedCategory);
+ private TaskOption? CurrentOption => Options.FirstOrDefault(o => o.TaskType == selectedTaskType);
+
+ private IEnumerable FilteredAccounts =>
+ accounts.Where(a =>
+ {
+ if (string.IsNullOrWhiteSpace(accountSearch))
+ return true;
+ var q = accountSearch.Trim();
+ return a.Phone.Contains(q, StringComparison.OrdinalIgnoreCase)
+ || (a.Nickname?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false)
+ || (a.Username?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false);
+ });
+
+ protected override async Task OnInitializedAsync()
+ {
+ EnsureTaskTypeValid();
+ await LoadAccounts();
+ }
+
+ private async Task LoadAccounts()
+ {
+ loadingAccounts = true;
+ try
+ {
+ var list = await AccountManagement.GetActiveAccountsAsync();
+ accounts = list.OrderBy(a => a.Id).ToList();
+ selectedAccounts = selectedAccounts.Where(a => accounts.Any(x => x.Id == a.Id)).ToHashSet();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"加载账号失败:{ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ loadingAccounts = false;
+ }
+ }
+
+ private void EnsureTaskTypeValid()
+ {
+ if (AvailableOptions.All(o => o.TaskType != selectedTaskType))
+ {
+ selectedTaskType = AvailableOptions.FirstOrDefault()?.TaskType
+ ?? Options.First().TaskType;
+ }
+ }
+
+ private void SelectAll() => selectedAccounts = accounts.ToHashSet();
+ private void ClearSelection() => selectedAccounts = new HashSet();
+
+ private async Task Create()
+ {
+ if (creating)
+ return;
+
+ EnsureTaskTypeValid();
+
+ if (selectedTaskType != BatchTaskTypes.UserJoinSubscribe)
+ {
+ Snackbar.Add("当前任务类型暂不支持从任务中心直接创建", Severity.Info);
+ return;
+ }
+
+ var links = ParseLines(linksText);
+ if (links.Count == 0)
+ {
+ Snackbar.Add("请至少输入一个频道/群链接", Severity.Warning);
+ return;
+ }
+
+ var selected = selectedAccounts.ToList();
+ if (selected.Count == 0)
+ {
+ Snackbar.Add("请至少选择一个执行账号", Severity.Warning);
+ return;
+ }
+
+ // 批量风控检查:加入群/订阅频道是敏感操作
+ var batchCheck = RiskService.CheckBatchAccounts(selected);
+ if (batchCheck.HasRiskyAccounts)
+ {
+ var parameters = new DialogParameters
+ {
+ ["Message"] = $"检测到 {batchCheck.RiskyCount} 个风险账号",
+ ["DetailedMessage"] = batchCheck.GetRiskySummary(),
+ ["ShowRecommendations"] = true,
+ ["ShowExcludeOption"] = true,
+ ["RiskyAccounts"] = batchCheck.RiskyAccounts.ToList()
+ };
+ var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large };
+ var dialog = DialogService.Show("批量操作风控警告", parameters, options);
+ var result = await dialog.Result;
+
+ if (result.Canceled)
+ return;
+
+ if (result.Data is TelegramPanel.Core.Services.RiskWarningAction action && action == TelegramPanel.Core.Services.RiskWarningAction.ExcludeRisky)
+ {
+ selected = batchCheck.SafeAccounts.ToList();
+ if (selected.Count == 0)
+ {
+ Snackbar.Add("排除风险账号后无剩余账号", Severity.Info);
+ return;
+ }
+ }
+ }
+
+ bool? confirm = await DialogService.ShowMessageBox(
+ "确认提交任务",
+ $"将对 {selected.Count} 个账号执行 {links.Count} 个链接的订阅/加群操作,总计 {selected.Count * links.Count} 次。是否继续?",
+ yesText: "继续", cancelText: "取消");
+
+ if (confirm != true)
+ return;
+
+ creating = true;
+ try
+ {
+ var ids = selected.Select(a => a.Id).Distinct().ToList();
+ var task = new BatchTask
+ {
+ TaskType = BatchTaskTypes.UserJoinSubscribe,
+ Total = ids.Count * links.Count,
+ Completed = 0,
+ Failed = 0,
+ Config = JsonSerializer.Serialize(new
+ {
+ AccountIds = ids,
+ Links = links,
+ DelayMs = delayMs
+ })
+ };
+
+ var created = await TaskManagement.CreateTaskAsync(task);
+ Snackbar.Add($"已提交后台任务 #{created.Id}(批量订阅/加群)。请到【任务中心】查看进度。", Severity.Success);
+ MudDialog.Close(DialogResult.Ok(created.Id));
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"提交失败:{ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ creating = false;
+ }
+ }
+
+ private void NavigateTo(string url)
+ {
+ MudDialog.Close();
+ Navigation.NavigateTo(url);
+ }
+
+ private static List ParseLines(string? text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ return new List();
+
+ return text
+ .Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => (x ?? string.Empty).Trim())
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ private void Cancel() => MudDialog.Close();
+}
diff --git a/src/TelegramPanel.Web/Components/Dialogs/JoinSubscribeDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/JoinSubscribeDialog.razor
new file mode 100644
index 0000000..ae6175d
--- /dev/null
+++ b/src/TelegramPanel.Web/Components/Dialogs/JoinSubscribeDialog.razor
@@ -0,0 +1,158 @@
+@namespace TelegramPanel.Web.Components.Dialogs
+@inject AccountManagementService AccountManagement
+@inject TelegramPanel.Core.Services.AccountRiskService RiskService
+@inject BatchTaskManagementService TaskManagement
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@using System.Text.Json
+@using TelegramPanel.Core.BatchTasks
+@using TelegramPanel.Data.Entities
+
+
+
+
+
+ 将使用指定账号订阅频道/加入群组。支持输入多个链接/用户名,换行分隔。
+
+ 支持:https://t.me/xxx、t.me/+hash、@username、username、tg://join?invite=hash
+
+
+
+ @if (account == null)
+ {
+ 账号不存在或已被删除
+ }
+ else
+ {
+
+ 执行账号:@account.Phone @(string.IsNullOrWhiteSpace(account.Nickname) ? "" : $"({account.Nickname})")
+
+ }
+
+
+
+
+
+
+
+ 关闭
+
+ @if (submitting)
+ {
+
+ 提交中...
+ }
+ else
+ {
+ 确认执行
+ }
+
+
+
+
+@code
+{
+ [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
+ [Parameter] public int AccountId { get; set; }
+
+ private Account? account;
+ private string linksText = "";
+ private int delayMs = 2000;
+ private bool submitting;
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (AccountId <= 0)
+ return;
+
+ account = await AccountManagement.GetAccountAsync(AccountId);
+ }
+
+ private async Task Submit()
+ {
+ if (submitting || account == null)
+ return;
+
+ var links = ParseLines(linksText);
+ if (links.Count == 0)
+ {
+ Snackbar.Add("请至少输入一个频道/群链接", Severity.Warning);
+ return;
+ }
+
+ // 风控检查:加入群/订阅频道是敏感操作
+ var batchCheck = RiskService.CheckBatchAccounts(new List { account });
+ if (batchCheck.HasRiskyAccounts)
+ {
+ var parameters = new DialogParameters
+ {
+ ["Message"] = $"检测到 {batchCheck.RiskyCount} 个风险账号",
+ ["DetailedMessage"] = batchCheck.GetRiskySummary(),
+ ["ShowRecommendations"] = true,
+ ["ShowExcludeOption"] = false,
+ ["RiskyAccounts"] = batchCheck.RiskyAccounts.ToList()
+ };
+ var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large };
+ var dialog = DialogService.Show("批量操作风控警告", parameters, options);
+ var result = await dialog.Result;
+ if (result.Canceled)
+ return;
+ }
+
+ bool? confirm = await DialogService.ShowMessageBox(
+ "确认执行",
+ $"将使用账号 {account.Phone} 对 {links.Count} 个链接执行订阅/加群操作。是否继续?",
+ yesText: "继续", cancelText: "取消");
+
+ if (confirm != true)
+ return;
+
+ submitting = true;
+ try
+ {
+ var task = new BatchTask
+ {
+ TaskType = BatchTaskTypes.UserJoinSubscribe,
+ Total = links.Count,
+ Completed = 0,
+ Failed = 0,
+ Config = JsonSerializer.Serialize(new
+ {
+ AccountIds = new List { account.Id },
+ Links = links,
+ DelayMs = delayMs
+ })
+ };
+
+ var created = await TaskManagement.CreateTaskAsync(task);
+ Snackbar.Add($"已提交后台任务 #{created.Id}(订阅/加群)。请到【任务中心】查看进度。", Severity.Success);
+ MudDialog.Close(DialogResult.Ok(created.Id));
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"提交失败:{ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ submitting = false;
+ }
+ }
+
+ private static List ParseLines(string? text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ return new List();
+
+ return text
+ .Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => (x ?? string.Empty).Trim())
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ private void Cancel() => MudDialog.Close();
+}
+
diff --git a/src/TelegramPanel.Web/Components/Pages/Accounts.razor b/src/TelegramPanel.Web/Components/Pages/Accounts.razor
index 303c058..422ae96 100644
--- a/src/TelegramPanel.Web/Components/Pages/Accounts.razor
+++ b/src/TelegramPanel.Web/Components/Pages/Accounts.razor
@@ -125,6 +125,8 @@
OnClick="@(() => ShowDetails(context))" Title="查看详情" />
+
("进群/订阅", parameters, options)!;
+ var result = await dialog.Result;
+ if (result is { Canceled: false })
+ await LoadAccounts();
+ }
+
private async Task OpenBatchChangeTwoFactorPassword()
{
if (loading)
diff --git a/src/TelegramPanel.Web/Components/Pages/Tasks.razor b/src/TelegramPanel.Web/Components/Pages/Tasks.razor
index 6162a0b..19b54f7 100644
--- a/src/TelegramPanel.Web/Components/Pages/Tasks.razor
+++ b/src/TelegramPanel.Web/Components/Pages/Tasks.razor
@@ -3,6 +3,7 @@
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using TelegramPanel.Data.Entities
+@using TelegramPanel.Core.BatchTasks
@implements IDisposable
任务中心 - Telegram Panel
@@ -17,6 +18,15 @@
+
+ 新建任务
+
+
+ 全部
+ 用户任务
+ Bot 任务
+
全部
待执行
@@ -27,6 +37,7 @@
任务ID
+ 归属
任务类型
状态
进度
@@ -36,6 +47,9 @@
@context.Id
+
+ @GetCategoryName(context.TaskType)
+
@GetTaskTypeName(context.TaskType)
@@ -83,12 +97,15 @@
private List tasks = new();
private bool loading = true;
private string filterStatus = "all";
+ private string filterCategory = "all";
private PeriodicTimer? refreshTimer;
private CancellationTokenSource? refreshCts;
private IEnumerable filteredTasks =>
- filterStatus == "all" ? tasks : tasks.Where(t => t.Status == filterStatus);
+ tasks.Where(t =>
+ (filterStatus == "all" || t.Status == filterStatus) &&
+ (filterCategory == "all" || GetCategory(t.TaskType) == filterCategory));
protected override async Task OnInitializedAsync()
{
@@ -154,17 +171,28 @@
return (double)task.Completed / task.Total * 100;
}
+ private static string GetCategory(string taskType) =>
+ taskType == BatchTaskTypes.UserJoinSubscribe ? "user" : "bot";
+
+ private static string GetCategoryName(string taskType) =>
+ GetCategory(taskType) == "user" ? "用户" : "Bot";
+
+ private static Color GetCategoryColor(string taskType) =>
+ GetCategory(taskType) == "user" ? Color.Info : Color.Secondary;
+
private string GetTaskIcon(string type) => type switch
{
- "invite" => Icons.Material.Filled.PersonAdd,
- "set_admin" => Icons.Material.Filled.AdminPanelSettings,
+ BatchTaskTypes.Invite => Icons.Material.Filled.PersonAdd,
+ BatchTaskTypes.SetAdmin => Icons.Material.Filled.AdminPanelSettings,
+ BatchTaskTypes.UserJoinSubscribe => Icons.Material.Filled.GroupAdd,
_ => Icons.Material.Filled.Task
};
private string GetTaskTypeName(string type) => type switch
{
- "invite" => "批量邀请",
- "set_admin" => "设置管理员",
+ BatchTaskTypes.Invite => "批量邀请",
+ BatchTaskTypes.SetAdmin => "设置管理员",
+ BatchTaskTypes.UserJoinSubscribe => "批量订阅/加群",
_ => type
};
@@ -258,4 +286,13 @@
}
}
}
+
+ private async Task OpenCreateTaskDialog()
+ {
+ var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
+ var dialog = DialogService.Show("新建任务", new DialogParameters(), options)!;
+ var result = await dialog.Result;
+ if (result is { Canceled: false })
+ await LoadTasks();
+ }
}
diff --git a/src/TelegramPanel.Web/Services/BatchTaskBackgroundService.cs b/src/TelegramPanel.Web/Services/BatchTaskBackgroundService.cs
index 9a14c44..befa9d9 100644
--- a/src/TelegramPanel.Web/Services/BatchTaskBackgroundService.cs
+++ b/src/TelegramPanel.Web/Services/BatchTaskBackgroundService.cs
@@ -2,8 +2,10 @@ using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using TelegramPanel.Core.BatchTasks;
using TelegramPanel.Core.Interfaces;
using TelegramPanel.Core.Services;
+using TelegramPanel.Core.Services.Telegram;
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Web.Services;
@@ -71,6 +73,7 @@ public sealed class BatchTaskBackgroundService : BackgroundService
var taskManagement = scope.ServiceProvider.GetRequiredService();
var channelManagement = scope.ServiceProvider.GetRequiredService();
var channelService = scope.ServiceProvider.GetRequiredService();
+ var accountTelegramTools = scope.ServiceProvider.GetRequiredService();
var pending = (await taskManagement.GetTasksByStatusAsync("pending"))
.OrderBy(t => t.CreatedAt)
@@ -89,12 +92,15 @@ public sealed class BatchTaskBackgroundService : BackgroundService
{
switch (pending.TaskType)
{
- case "invite":
+ case BatchTaskTypes.Invite:
(completed, failed) = await RunInviteAsync(pending, taskManagement, channelManagement, channelService, completed, failed, cancellationToken);
break;
- case "set_admin":
+ case BatchTaskTypes.SetAdmin:
(completed, failed) = await RunSetAdminAsync(pending, taskManagement, channelManagement, channelService, completed, failed, cancellationToken);
break;
+ case BatchTaskTypes.UserJoinSubscribe:
+ (completed, failed) = await RunUserJoinSubscribeAsync(pending, taskManagement, accountTelegramTools, completed, failed, cancellationToken);
+ break;
default:
failed = pending.Total == 0 ? 1 : pending.Total;
await taskManagement.UpdateTaskProgressAsync(pending.Id, completed, failed);
@@ -246,6 +252,44 @@ public sealed class BatchTaskBackgroundService : BackgroundService
return (completed, failed);
}
+ private static async Task<(int completed, int failed)> RunUserJoinSubscribeAsync(
+ BatchTask task,
+ BatchTaskManagementService taskManagement,
+ AccountTelegramToolsService accountTelegramTools,
+ int completed,
+ int failed,
+ CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrWhiteSpace(task.Config))
+ throw new InvalidOperationException("任务配置为空");
+
+ var cfg = JsonSerializer.Deserialize(task.Config);
+ if (cfg == null || cfg.AccountIds.Count == 0 || cfg.Links.Count == 0)
+ throw new InvalidOperationException("任务配置缺少 AccountIds 或 Links");
+
+ foreach (var accountId in cfg.AccountIds)
+ {
+ foreach (var link in cfg.Links)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var latest = await taskManagement.GetTaskAsync(task.Id);
+ if (latest != null && latest.Status != "running")
+ return (completed, failed);
+
+ var (ok, _, _) = await accountTelegramTools.JoinChatOrChannelAsync(accountId, link, cancellationToken);
+ if (ok) completed++; else failed++;
+
+ await taskManagement.UpdateTaskProgressAsync(task.Id, completed, failed);
+
+ if (cfg.DelayMs > 0)
+ await Task.Delay(cfg.DelayMs + Random.Shared.Next(300, 900), cancellationToken);
+ }
+ }
+
+ return (completed, failed);
+ }
+
private sealed record InviteTaskConfig(
List ChannelTelegramIds,
List Usernames,
@@ -260,4 +304,10 @@ public sealed class BatchTaskBackgroundService : BackgroundService
string Title,
int PreferredExecuteAccountId
);
+
+ private sealed record UserJoinSubscribeTaskConfig(
+ List AccountIds,
+ List Links,
+ int DelayMs
+ );
}