移除内置批量操作入口,改为模块提供

This commit is contained in:
meoacgx committed 2025-12-21 11:08:02 +08:00
1 parent 21ad1c4630
commit 3427c68f12
8 files changed
+19 -903

No files matched your search

@@ -22,11 +22,6 @@
<MudNavLink Href="/groups" Icon="@Icons.Material.Filled.List">群组列表</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="批量操作" Icon="@Icons.Material.Filled.PlaylistAddCheck">
<MudNavLink Href="/batch/invite" Icon="@Icons.Material.Filled.PersonAdd">批量邀请</MudNavLink>
<MudNavLink Href="/batch/admins" Icon="@Icons.Material.Filled.AdminPanelSettings">批量设管理</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="机器人管理" Icon="@Icons.Material.Filled.SmartToy">
<MudNavLink Href="/bots" Icon="@Icons.Material.Filled.SmartToy">机器人列表</MudNavLink>
<MudNavLink Href="/bots/channels" Icon="@Icons.Material.Filled.List">Bot 频道</MudNavLink>
@@ -1,385 +0,0 @@
@page "/batch/admins"
@inject IChannelService ChannelService
@inject AccountManagementService AccountManagement
@inject ChannelManagementService ChannelManagement
@inject ChannelGroupManagementService ChannelGroupManagement
@inject BatchTaskManagementService TaskManagement
@inject ISnackbar Snackbar
@using TelegramPanel.Data.Entities
@using TelegramPanel.Core.Interfaces
<PageTitle>批量设置管理员 - Telegram Panel</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4">批量设置管理员</MudText>
<MudGrid>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">配置管理员</MudText>
</MudCardHeader>
<MudCardContent>
<MudSelect @bind-Value="selectedAccountId" Label="执行账号(可选)" Variant="Variant.Outlined">
<MudSelectItem Value="0">(默认:按频道创建账号执行)</MudSelectItem>
@foreach (var account in accounts)
{
<MudSelectItem Value="@account.Id">@FormatAccountLabel(account)</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="filterGroupId" Label="频道分组筛选(可选)" Variant="Variant.Outlined" Dense="true" Class="mt-4">
<MudSelectItem Value="0">全部分组</MudSelectItem>
@foreach (var g in groups)
{
<MudSelectItem Value="@g.Id">@g.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Spacing="2" Class="mt-3">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.SelectAll"
Disabled="@channelsLoading" OnClick="SelectAllFilteredChannels">
全选当前筛选
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Checklist"
Disabled="@(channelsLoading || filterGroupId <= 0)" OnClick="SelectCurrentGroupChannels">
选择该分组全部
</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="@channelsLoading" OnClick="ClearSelectedChannels">
清空选择
</MudButton>
</MudStack>
<MudTable Items="@filteredChannels" Hover="true" Dense="true" Class="mt-3"
MultiSelection="true" @bind-SelectedItems="selectedChannels"
Loading="@channelsLoading">
<HeaderContent>
<MudTh>频道名称</MudTh>
<MudTh>分组</MudTh>
<MudTh>创建账号</MudTh>
<MudTh>用户名</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="频道名称">@context.Title</MudTd>
<MudTd DataLabel="分组">@(context.Group?.Name ?? "未分组")</MudTd>
<MudTd DataLabel="创建账号">@(context.CreatorAccount?.Phone ?? (context.CreatorAccountId?.ToString() ?? "(非系统创建)"))</MudTd>
<MudTd DataLabel="用户名">@(string.IsNullOrWhiteSpace(context.Username) ? "-" : "@" + context.Username)</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>暂无频道数据</MudText>
</NoRecordsContent>
</MudTable>
<MudTextField @bind-Value="adminTitle" Label="管理员头衔" Variant="Variant.Outlined" Class="mt-4"
HelperText="可选,为管理员设置自定义头衔" />
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mt-4 mb-2">
<MudText Typo="Typo.h6">管理员权限</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="SelectAllPermissions">全选</MudButton>
<MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="ClearAllPermissions">全不选</MudButton>
</MudStack>
<MudCheckBox @bind-Value="permissions.ChangeInfo" Label="修改频道信息" />
<MudCheckBox @bind-Value="permissions.PostMessages" Label="发布消息" />
<MudCheckBox @bind-Value="permissions.EditMessages" Label="编辑消息" />
<MudCheckBox @bind-Value="permissions.DeleteMessages" Label="删除消息" />
<MudCheckBox @bind-Value="permissions.BanUsers" Label="封禁用户" />
<MudCheckBox @bind-Value="permissions.InviteUsers" Label="邀请用户" />
<MudCheckBox @bind-Value="permissions.PinMessages" Label="置顶消息" />
<MudCheckBox @bind-Value="permissions.ManageCall" Label="管理语音聊天" />
<MudCheckBox @bind-Value="permissions.ManageTopics" Label="管理话题" />
<MudCheckBox @bind-Value="permissions.AddAdmins" Label="添加管理员" />
<MudCheckBox @bind-Value="permissions.Anonymous" Label="匿名管理员(如支持)" />
<MudTextField @bind-Value="usernames" Label="用户名列表" Variant="Variant.Outlined" Lines="8" Class="mt-4"
HelperText="每行一个用户名" />
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Text" Color="Color.Secondary" OnClick="ClearForm">清空</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Primary"
Disabled="@(selectedChannels.Count == 0 || string.IsNullOrEmpty(usernames) || processing)"
OnClick="StartSetAdmins">
@if (processing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
<span>设置中...</span>
}
else
{
<span>开始设置</span>
}
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">设置结果</MudText>
</MudCardHeader>
<MudCardContent>
<MudList T="string" Dense="true">
@foreach (var result in results)
{
<MudListItem T="string" Icon="@(result.Success ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)"
IconColor="@(result.Success ? Color.Success : Color.Error)">
<div>
<MudText Typo="Typo.body2">
<strong>@result.ChannelTitle</strong>:@("@" + result.Username)
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">执行账号:@result.ExecutedByAccountId</MudText>
@if (!result.Success && result.Error != null)
{
<MudText Typo="Typo.caption" Color="Color.Error">@result.Error</MudText>
}
</div>
</MudListItem>
}
</MudList>
@if (results.Any())
{
<MudDivider Class="my-3" />
<MudText Typo="Typo.body2">
成功:@results.Count(r => r.Success) / 总计:@results.Count(频道:@results.Select(r => r.ChannelTelegramId).Distinct().Count())
</MudText>
}
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
@code {
private List<Account> accounts = new();
private List<Channel> channels = new();
private List<ChannelGroup> groups = new();
private int selectedAccountId = 0;
private int filterGroupId = 0;
private bool channelsLoading = true;
private HashSet<Channel> selectedChannels = new();
private string adminTitle = "Admin";
private string usernames = "";
private bool processing = false;
private AdminPermissions permissions = new();
private List<AdminSetOutcome> results = new();
private IEnumerable<Channel> filteredChannels =>
channels.Where(c => filterGroupId <= 0 || c.GroupId == filterGroupId);
protected override async Task OnInitializedAsync()
{
await LoadAccounts();
await LoadGroups();
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 LoadGroups()
{
try
{
var list = await ChannelGroupManagement.GetAllGroupsAsync();
groups = list.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载频道分组失败:{ex.Message}", Severity.Error);
}
}
private async Task LoadChannels()
{
channelsLoading = true;
try
{
var channelList = await ChannelManagement.GetAllChannelsAsync();
channels = channelList.ToList();
selectedChannels = selectedChannels.Where(c => channels.Any(x => x.Id == c.Id)).ToHashSet();
}
catch (Exception ex)
{
Snackbar.Add($"加载频道列表失败:{ex.Message}", Severity.Error);
}
finally
{
channelsLoading = false;
}
}
private void SelectAllFilteredChannels()
{
selectedChannels = filteredChannels.ToHashSet();
}
private void SelectCurrentGroupChannels()
{
if (filterGroupId <= 0)
return;
selectedChannels = channels.Where(c => c.GroupId == filterGroupId).ToHashSet();
}
private void ClearSelectedChannels()
{
selectedChannels = new HashSet<Channel>();
}
private void SelectAllPermissions()
{
permissions.ChangeInfo = true;
permissions.PostMessages = true;
permissions.EditMessages = true;
permissions.DeleteMessages = true;
permissions.BanUsers = true;
permissions.InviteUsers = true;
permissions.PinMessages = true;
permissions.ManageCall = true;
permissions.ManageTopics = true;
permissions.AddAdmins = true;
permissions.Anonymous = true;
}
private void ClearAllPermissions()
{
permissions.ChangeInfo = false;
permissions.PostMessages = false;
permissions.EditMessages = false;
permissions.DeleteMessages = false;
permissions.BanUsers = false;
permissions.InviteUsers = false;
permissions.PinMessages = false;
permissions.ManageCall = false;
permissions.ManageTopics = false;
permissions.AddAdmins = false;
permissions.Anonymous = false;
}
private async Task StartSetAdmins()
{
processing = true;
results.Clear();
try
{
var usernameList = usernames.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(u => u.Trim().TrimStart('@'))
.Where(u => !string.IsNullOrEmpty(u))
.ToList();
if (usernameList.Count == 0)
{
Snackbar.Add("请输入至少一个用户名", Severity.Warning);
return;
}
var targetChannels = selectedChannels.ToList();
if (targetChannels.Count == 0)
{
Snackbar.Add("请先勾选至少一个频道", Severity.Warning);
return;
}
var rights = BuildAdminRights();
var task = new BatchTask
{
TaskType = "set_admin",
Total = usernameList.Count * targetChannels.Count,
Completed = 0,
Failed = 0,
Config = System.Text.Json.JsonSerializer.Serialize(new
{
ChannelTelegramIds = targetChannels.Select(c => c.TelegramId).ToList(),
Usernames = usernameList,
Rights = (int)rights,
Title = adminTitle,
PreferredExecuteAccountId = selectedAccountId
})
};
var createdTask = await TaskManagement.CreateTaskAsync(task);
Snackbar.Add($"已提交后台任务 #{createdTask.Id}(批量设置管理员)。请到【任务中心】查看进度。", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"设置失败:{ex.Message}", Severity.Error);
}
finally
{
processing = false;
}
}
private AdminRights BuildAdminRights()
{
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.ManageCall) rights |= AdminRights.ManageCall;
if (permissions.ManageTopics) rights |= AdminRights.ManageTopics;
if (permissions.AddAdmins) rights |= AdminRights.AddAdmins;
if (permissions.Anonymous) rights |= AdminRights.Anonymous;
return rights;
}
private void ClearForm()
{
usernames = "";
results.Clear();
}
private static string FormatAccountLabel(Account account)
{
var nickname = string.IsNullOrWhiteSpace(account.Nickname) ? null : account.Nickname.Trim();
var username = string.IsNullOrWhiteSpace(account.Username) ? null : account.Username.Trim();
var namePart = nickname ?? account.Phone;
if (!string.IsNullOrWhiteSpace(username))
return $"{namePart} (@{username})";
return namePart;
}
private class AdminPermissions
{
public bool ChangeInfo { get; set; } = true;
public bool PostMessages { get; set; } = true;
public bool EditMessages { get; set; } = true;
public bool DeleteMessages { get; set; } = true;
public bool BanUsers { get; set; }
public bool InviteUsers { get; set; } = true;
public bool PinMessages { get; set; } = true;
public bool ManageCall { get; set; }
public bool ManageTopics { get; set; }
public bool AddAdmins { get; set; }
public bool Anonymous { get; set; }
}
private record AdminSetOutcome(
long ChannelTelegramId,
string ChannelTitle,
string Username,
bool Success,
string? Error,
int ExecutedByAccountId
);
}
@@ -1,308 +0,0 @@
@page "/batch/invite"
@inject IChannelService ChannelService
@inject AccountManagementService AccountManagement
@inject ChannelManagementService ChannelManagement
@inject ChannelGroupManagementService ChannelGroupManagement
@inject BatchTaskManagementService TaskManagement
@inject ISnackbar Snackbar
@using TelegramPanel.Data.Entities
<PageTitle>批量邀请 - Telegram Panel</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4">批量邀请用户</MudText>
<MudGrid>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">配置邀请</MudText>
</MudCardHeader>
<MudCardContent>
<MudSelect @bind-Value="selectedAccountId" Label="执行账号(可选)" Variant="Variant.Outlined">
<MudSelectItem Value="0">(默认:按频道创建账号执行)</MudSelectItem>
@foreach (var account in accounts)
{
<MudSelectItem Value="@account.Id">@FormatAccountLabel(account)</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="filterGroupId" Label="频道分组筛选(可选)" Variant="Variant.Outlined" Dense="true" Class="mt-4">
<MudSelectItem Value="0">全部分组</MudSelectItem>
@foreach (var g in groups)
{
<MudSelectItem Value="@g.Id">@g.Name</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" Spacing="2" Class="mt-3">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.SelectAll"
Disabled="@channelsLoading" OnClick="SelectAllFilteredChannels">
全选当前筛选
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Checklist"
Disabled="@(channelsLoading || filterGroupId <= 0)" OnClick="SelectCurrentGroupChannels">
选择该分组全部
</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="@channelsLoading" OnClick="ClearSelectedChannels">
清空选择
</MudButton>
</MudStack>
<MudTable Items="@filteredChannels" Hover="true" Dense="true" Class="mt-3"
MultiSelection="true" @bind-SelectedItems="selectedChannels"
Loading="@channelsLoading">
<HeaderContent>
<MudTh>频道名称</MudTh>
<MudTh>分组</MudTh>
<MudTh>创建账号</MudTh>
<MudTh>用户名</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="频道名称">@context.Title</MudTd>
<MudTd DataLabel="分组">@(context.Group?.Name ?? "未分组")</MudTd>
<MudTd DataLabel="创建账号">@(context.CreatorAccount?.Phone ?? (context.CreatorAccountId?.ToString() ?? "(非系统创建)"))</MudTd>
<MudTd DataLabel="用户名">@(string.IsNullOrWhiteSpace(context.Username) ? "-" : "@" + context.Username)</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>暂无频道数据</MudText>
</NoRecordsContent>
</MudTable>
<MudTextField @bind-Value="usernames" Label="用户名列表" Variant="Variant.Outlined" Lines="10" Class="mt-4"
HelperText="每行一个用户名" />
<MudNumericField @bind-Value="delayMs" Label="邀请间隔 (毫秒)" Variant="Variant.Outlined" Min="1000" Max="10000" Class="mt-4"
HelperText="建议设置2000-5000ms,避免触发风控" />
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Text" Color="Color.Secondary" OnClick="ClearForm">清空</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Primary"
Disabled="@(selectedChannels.Count == 0 || string.IsNullOrEmpty(usernames) || inviting)"
OnClick="StartInvite">
@if (inviting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
<span>邀请中...</span>
}
else
{
<span>开始邀请</span>
}
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">邀请结果</MudText>
</MudCardHeader>
<MudCardContent>
<MudList T="string" Dense="true">
@foreach (var result in inviteResults)
{
<MudListItem T="string" Icon="@(result.Success ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)"
IconColor="@(result.Success ? Color.Success : Color.Error)">
<div>
<MudText Typo="Typo.body2">
<strong>@result.ChannelTitle</strong>:@("@" + result.Username)
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">执行账号:@result.ExecutedByAccountId</MudText>
@if (!result.Success && result.Error != null)
{
<MudText Typo="Typo.caption" Color="Color.Error">@result.Error</MudText>
}
</div>
</MudListItem>
}
</MudList>
@if (inviteResults.Any())
{
<MudDivider Class="my-3" />
<MudText Typo="Typo.body2">
成功:@inviteResults.Count(r => r.Success) / 总计:@inviteResults.Count(频道:@inviteResults.Select(r => r.ChannelTelegramId).Distinct().Count())
</MudText>
}
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
@code {
[SupplyParameterFromQuery(Name = "channelId")] public int? ChannelDbIdQuery { get; set; }
private List<Account> accounts = new();
private List<Channel> channels = new();
private List<ChannelGroup> groups = new();
private int selectedAccountId = 0;
private int filterGroupId = 0;
private bool channelsLoading = true;
private HashSet<Channel> selectedChannels = new();
private string usernames = "";
private int delayMs = 2000;
private bool inviting = false;
private List<InviteOutcome> inviteResults = new();
private IEnumerable<Channel> filteredChannels =>
channels.Where(c => filterGroupId <= 0 || c.GroupId == filterGroupId);
protected override async Task OnInitializedAsync()
{
await LoadAccounts();
await LoadGroups();
await LoadChannels();
if (ChannelDbIdQuery.HasValue && ChannelDbIdQuery.Value > 0)
{
var c = channels.FirstOrDefault(x => x.Id == ChannelDbIdQuery.Value);
if (c != null)
selectedChannels = new HashSet<Channel> { c };
}
}
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 LoadGroups()
{
try
{
var list = await ChannelGroupManagement.GetAllGroupsAsync();
groups = list.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"加载频道分组失败:{ex.Message}", Severity.Error);
}
}
private async Task LoadChannels()
{
channelsLoading = true;
try
{
var channelList = await ChannelManagement.GetAllChannelsAsync();
channels = channelList.ToList();
selectedChannels = selectedChannels.Where(c => channels.Any(x => x.Id == c.Id)).ToHashSet();
}
catch (Exception ex)
{
Snackbar.Add($"加载频道列表失败:{ex.Message}", Severity.Error);
}
finally
{
channelsLoading = false;
}
}
private void SelectAllFilteredChannels()
{
selectedChannels = filteredChannels.ToHashSet();
}
private void SelectCurrentGroupChannels()
{
if (filterGroupId <= 0)
return;
selectedChannels = channels.Where(c => c.GroupId == filterGroupId).ToHashSet();
}
private void ClearSelectedChannels()
{
selectedChannels = new HashSet<Channel>();
}
private async Task StartInvite()
{
inviting = true;
inviteResults.Clear();
try
{
var usernameList = usernames.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(u => u.Trim().TrimStart('@'))
.Where(u => !string.IsNullOrEmpty(u))
.ToList();
if (usernameList.Count == 0)
{
Snackbar.Add("请输入至少一个用户名", Severity.Warning);
return;
}
var targetChannels = selectedChannels.ToList();
if (targetChannels.Count == 0)
{
Snackbar.Add("请先勾选至少一个频道", Severity.Warning);
return;
}
var task = new BatchTask
{
TaskType = "invite",
Total = usernameList.Count * targetChannels.Count,
Completed = 0,
Failed = 0,
Config = System.Text.Json.JsonSerializer.Serialize(new
{
ChannelTelegramIds = targetChannels.Select(c => c.TelegramId).ToList(),
Usernames = usernameList,
DelayMs = delayMs,
PreferredExecuteAccountId = selectedAccountId
})
};
var createdTask = await TaskManagement.CreateTaskAsync(task);
Snackbar.Add($"已提交后台任务 #{createdTask.Id}(批量邀请)。请到【任务中心】查看进度。", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"邀请失败:{ex.Message}", Severity.Error);
}
finally
{
inviting = false;
}
}
private void ClearForm()
{
usernames = "";
inviteResults.Clear();
}
private static string FormatAccountLabel(Account account)
{
var nickname = string.IsNullOrWhiteSpace(account.Nickname) ? null : account.Nickname.Trim();
var username = string.IsNullOrWhiteSpace(account.Username) ? null : account.Username.Trim();
var namePart = nickname ?? account.Phone;
if (!string.IsNullOrWhiteSpace(username))
return $"{namePart} (@{username})";
return namePart;
}
private record InviteOutcome(
long ChannelTelegramId,
string ChannelTitle,
string Username,
bool Success,
string? Error,
int ExecutedByAccountId
);
}
@@ -101,7 +101,6 @@
<MudMenuItem Icon="@Icons.Material.Filled.Info" OnClick="@(async () => await ShowDetails(context))">查看详情</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Edit" OnClick="@(async () => await EditChannel(context))">编辑频道</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PersonRemove" OnClick="@(async () => await KickUser(context))">踢人</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PersonAdd" OnClick="@(() => InviteUsers(context))">邀请用户</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(async () => await CopyLinkAsync(context))">复制链接</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(() => DeleteChannel(context.Id))">删除频道</MudMenuItem>
@@ -278,11 +277,6 @@
await ShowDetails(channel);
}
private void InviteUsers(Channel channel)
{
Navigation.NavigateTo($"/batch/invite?channelId={channel.Id}");
}
private async Task CopyLinkAsync(Channel channel)
{
try
@@ -75,7 +75,6 @@
<MudTd DataLabel="操作">
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small">
<MudMenuItem Icon="@Icons.Material.Filled.Info" OnClick="@(() => ShowDetails(context))">查看详情</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PersonAdd" OnClick="@(() => InviteMembers(context))">邀请成员</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(async () => await CopyLink(context))">复制链接</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(() => DeleteGroup(context.Id))">删除群组</MudMenuItem>
@@ -160,11 +159,6 @@
DialogService.Show<GroupDetailsDialog>("群组详情", parameters, options);
}
private void InviteMembers(Group group)
{
Navigation.NavigateTo($"/batch/invite?groupId={group.Id}");
}
private async Task CopyLink(Group group)
{
try
@@ -73,9 +73,6 @@
<MudButton Variant="Variant.Filled" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Add" Href="/channels/create">
创建频道
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Tertiary" StartIcon="@Icons.Material.Filled.PersonAdd" Href="/batch/invite">
批量邀请
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Sync"
Disabled="@_syncing"
OnClick="@(async (MouseEventArgs _) => await SyncAllAccounts())">
@@ -32,28 +32,6 @@ public sealed class TaskCatalogModule : ITelegramPanelModule, IModuleTaskProvide
public IEnumerable<ModuleTaskDefinition> GetTasks(ModuleHostContext context)
{
yield return new ModuleTaskDefinition
{
Category = "bot",
TaskType = BatchTaskTypes.Invite,
DisplayName = "批量邀请",
Description = "对所选频道批量邀请用户名列表(Bot/执行账号需具备权限)。",
Icon = Icons.Material.Filled.PersonAdd,
CreateRoute = "/batch/invite",
Order = 10
};
yield return new ModuleTaskDefinition
{
Category = "bot",
TaskType = BatchTaskTypes.SetAdmin,
DisplayName = "设置管理员",
Description = "对所选频道批量设置管理员(Bot/执行账号需具备权限)。",
Icon = Icons.Material.Filled.AdminPanelSettings,
CreateRoute = "/batch/admins",
Order = 20
};
yield return new ModuleTaskDefinition
{
Category = "system",
@@ -2,8 +2,6 @@ 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.Data.Entities;
using TelegramPanel.Modules;
@@ -71,8 +69,6 @@ public sealed class BatchTaskBackgroundService : BackgroundService
{
using var scope = _scopeFactory.CreateScope();
var taskManagement = scope.ServiceProvider.GetRequiredService<BatchTaskManagementService>();
var channelManagement = scope.ServiceProvider.GetRequiredService<ChannelManagementService>();
var channelService = scope.ServiceProvider.GetRequiredService<IChannelService>();
var pending = (await taskManagement.GetTasksByStatusAsync("pending"))
.OrderBy(t => t.CreatedAt)
@@ -89,39 +85,28 @@ public sealed class BatchTaskBackgroundService : BackgroundService
try
{
switch (pending.TaskType)
// 模块扩展任务:从 DI 中查找对应 TaskType 的执行器
var handler = scope.ServiceProvider
.GetServices<IModuleTaskHandler>()
.FirstOrDefault(h => string.Equals(h.TaskType, pending.TaskType, StringComparison.OrdinalIgnoreCase));
if (handler == null)
{
case BatchTaskTypes.Invite:
(completed, failed) = await RunInviteAsync(pending, taskManagement, channelManagement, channelService, completed, failed, cancellationToken);
break;
case BatchTaskTypes.SetAdmin:
(completed, failed) = await RunSetAdminAsync(pending, taskManagement, channelManagement, channelService, completed, failed, cancellationToken);
break;
default:
// 模块扩展任务:从 DI 中查找对应 TaskType 的执行器
var handler = scope.ServiceProvider
.GetServices<IModuleTaskHandler>()
.FirstOrDefault(h => string.Equals(h.TaskType, pending.TaskType, StringComparison.OrdinalIgnoreCase));
failed = pending.Total == 0 ? 1 : pending.Total;
await taskManagement.UpdateTaskProgressAsync(pending.Id, completed, failed);
await taskManagement.CompleteTaskAsync(pending.Id, success: false);
_logger.LogWarning("Unsupported batch task type: {TaskType} (task {TaskId})", pending.TaskType, pending.Id);
return;
}
if (handler == null)
{
failed = pending.Total == 0 ? 1 : pending.Total;
await taskManagement.UpdateTaskProgressAsync(pending.Id, completed, failed);
await taskManagement.CompleteTaskAsync(pending.Id, success: false);
_logger.LogWarning("Unsupported batch task type: {TaskType} (task {TaskId})", pending.TaskType, pending.Id);
return;
}
var host = new DbBackedModuleTaskExecutionHost(pending, taskManagement, scope.ServiceProvider);
await handler.ExecuteAsync(host, cancellationToken);
var host = new DbBackedModuleTaskExecutionHost(pending, taskManagement, scope.ServiceProvider);
await handler.ExecuteAsync(host, cancellationToken);
var after = await taskManagement.GetTaskAsync(pending.Id);
if (after != null)
{
completed = after.Completed;
failed = after.Failed;
}
break;
var after = await taskManagement.GetTaskAsync(pending.Id);
if (after != null)
{
completed = after.Completed;
failed = after.Failed;
}
// 如果任务被用户取消(当前实现:Cancel 会把状态写成 failed),则不覆盖它
@@ -148,140 +133,6 @@ public sealed class BatchTaskBackgroundService : BackgroundService
}
}
private static async Task<(int completed, int failed)> RunInviteAsync(
BatchTask task,
BatchTaskManagementService taskManagement,
ChannelManagementService channelManagement,
IChannelService channelService,
int completed,
int failed,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(task.Config))
throw new InvalidOperationException("任务配置为空");
var cfg = JsonSerializer.Deserialize<InviteTaskConfig>(task.Config);
if (cfg == null || cfg.ChannelTelegramIds.Count == 0 || cfg.Usernames.Count == 0)
throw new InvalidOperationException("任务配置缺少 ChannelTelegramIds 或 Usernames");
foreach (var channelTelegramId in cfg.ChannelTelegramIds)
{
cancellationToken.ThrowIfCancellationRequested();
var latest = await taskManagement.GetTaskAsync(task.Id);
if (latest != null && latest.Status != "running")
return (completed, failed);
var channel = await channelManagement.GetChannelByTelegramIdAsync(channelTelegramId);
if (channel == null)
{
failed += cfg.Usernames.Count;
await taskManagement.UpdateTaskProgressAsync(task.Id, completed, failed);
continue;
}
var executeAccountId = await channelManagement.ResolveExecuteAccountIdAsync(channel, cfg.PreferredExecuteAccountId > 0 ? cfg.PreferredExecuteAccountId : null);
if (executeAccountId is not > 0)
{
failed += cfg.Usernames.Count;
await taskManagement.UpdateTaskProgressAsync(task.Id, completed, failed);
continue;
}
List<InviteResult> results;
try
{
results = await channelService.BatchInviteUsersAsync(executeAccountId.Value, channelTelegramId, cfg.Usernames, cfg.DelayMs);
}
catch (Exception ex)
{
results = cfg.Usernames.Select(u => new InviteResult(u, false, ex.Message)).ToList();
}
completed += results.Count(r => r.Success);
failed += results.Count(r => !r.Success);
await taskManagement.UpdateTaskProgressAsync(task.Id, completed, failed);
}
return (completed, failed);
}
private static async Task<(int completed, int failed)> RunSetAdminAsync(
BatchTask task,
BatchTaskManagementService taskManagement,
ChannelManagementService channelManagement,
IChannelService channelService,
int completed,
int failed,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(task.Config))
throw new InvalidOperationException("任务配置为空");
var cfg = JsonSerializer.Deserialize<SetAdminTaskConfig>(task.Config);
if (cfg == null || cfg.ChannelTelegramIds.Count == 0 || cfg.Usernames.Count == 0)
throw new InvalidOperationException("任务配置缺少 ChannelTelegramIds 或 Usernames");
var rights = (AdminRights)cfg.Rights;
var adminRequests = cfg.Usernames.Select(u => new AdminRequest(u, rights, cfg.Title)).ToList();
foreach (var channelTelegramId in cfg.ChannelTelegramIds)
{
cancellationToken.ThrowIfCancellationRequested();
var latest = await taskManagement.GetTaskAsync(task.Id);
if (latest != null && latest.Status != "running")
return (completed, failed);
var channel = await channelManagement.GetChannelByTelegramIdAsync(channelTelegramId);
if (channel == null)
{
failed += cfg.Usernames.Count;
await taskManagement.UpdateTaskProgressAsync(task.Id, completed, failed);
continue;
}
var executeAccountId = await channelManagement.ResolveExecuteAccountIdAsync(channel, cfg.PreferredExecuteAccountId > 0 ? cfg.PreferredExecuteAccountId : null);
if (executeAccountId is not > 0)
{
failed += cfg.Usernames.Count;
await taskManagement.UpdateTaskProgressAsync(task.Id, completed, failed);
continue;
}
List<SetAdminResult> results;
try
{
results = await channelService.BatchSetAdminsAsync(executeAccountId.Value, channelTelegramId, adminRequests);
}
catch (Exception ex)
{
results = cfg.Usernames.Select(u => new SetAdminResult(u, false, ex.Message)).ToList();
}
completed += results.Count(r => r.Success);
failed += results.Count(r => !r.Success);
await taskManagement.UpdateTaskProgressAsync(task.Id, completed, failed);
}
return (completed, failed);
}
private sealed record InviteTaskConfig(
List<long> ChannelTelegramIds,
List<string> Usernames,
int DelayMs,
int PreferredExecuteAccountId
);
private sealed record SetAdminTaskConfig(
List<long> ChannelTelegramIds,
List<string> Usernames,
int Rights,
string Title,
int PreferredExecuteAccountId
);
private sealed class DbBackedModuleTaskExecutionHost : IModuleTaskExecutionHost
{
private readonly BatchTask _task;