移除内置批量操作入口,改为模块提供
This commit is contained in:
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;
|
||||
|
||||
Reference in new issue
Block a user