Files
Telegram-Panel/src/TelegramPanel.Web/Components/Pages/BatchInvite.razor
T

351 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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
{
ChannelIds = targetChannels.Select(c => c.TelegramId).ToList(),
ChannelTitles = targetChannels.Select(c => c.Title).ToList(),
DelayMs = delayMs,
ExecutedByAccountId = selectedAccountId
})
};
var createdTask = await TaskManagement.CreateTaskAsync(task);
await TaskManagement.StartTaskAsync(createdTask.Id);
var totalSuccess = 0;
var totalFailed = 0;
foreach (var channel in targetChannels)
{
var executeAccountId = await ChannelManagement.ResolveExecuteAccountIdAsync(channel, selectedAccountId > 0 ? selectedAccountId : null);
if (executeAccountId is not > 0)
throw new InvalidOperationException($"频道“{channel.Title}”没有可用的执行账号:请先【同步数据】或手动选择一个执行账号");
List<InviteResult> results;
try
{
results = await ChannelService.BatchInviteUsersAsync(executeAccountId.Value, channel.TelegramId, usernameList, delayMs);
}
catch (Exception ex)
{
results = usernameList.Select(u => new InviteResult(u, false, ex.Message)).ToList();
}
foreach (var r in results)
{
inviteResults.Add(new InviteOutcome(
ChannelTelegramId: channel.TelegramId,
ChannelTitle: channel.Title,
Username: r.Username,
Success: r.Success,
Error: r.Error,
ExecutedByAccountId: executeAccountId.Value
));
}
var successCount = results.Count(r => r.Success);
var failedCount = results.Count - successCount;
totalSuccess += successCount;
totalFailed += failedCount;
await TaskManagement.UpdateTaskProgressAsync(createdTask.Id, totalSuccess, totalFailed);
StateHasChanged();
}
await TaskManagement.CompleteTaskAsync(createdTask.Id, true);
Snackbar.Add($"邀请完成:成功 {totalSuccess},失败 {totalFailed}(频道:{targetChannels.Count})", totalFailed == 0 ? Severity.Success : Severity.Warning);
}
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
);
}