Files
Telegram-Panel/src/TelegramPanel.Web/Components/Dialogs/BatchChangeUsernameDialog.razor
T

321 lines
12 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.
@namespace TelegramPanel.Web.Components.Dialogs
@inject AccountManagementService AccountManagement
@inject TelegramPanel.Core.Services.Telegram.AccountTelegramToolsService AccountTelegramTools
@inject TelegramPanel.Core.Services.AccountRiskService RiskService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using System.Security.Cryptography
@using System.Text.RegularExpressions
@using TelegramPanel.Data.Entities
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
将对选中的账号批量修改用户名(t.me/xxx)。用户名模板支持变量:
<MudText Typo="Typo.body2" Class="mt-1">
<b>{number}</b> / <b>{letter}</b> 表示 1 位随机数字/字母;<b>{number3}</b> 表示 3 位随机数字;<b>{letter4}</b> 表示 4 位随机字母。可组合使用。
</MudText>
<MudText Typo="Typo.body2" Class="mt-1">
示例:<b>test_{number3}{letter4}</b> → <b>test_123abcd</b>
</MudText>
</MudAlert>
<MudText Typo="Typo.subtitle2">账号数量:@accounts.Count</MudText>
<MudTextField @bind-Value="usernameTemplate" Label="用户名模板" Variant="Variant.Outlined" Required="true"
HelperText="请输入用户名模板(不要带开头的 at 符号)。例如:test_{number3}{letter4}" />
@if (running || results.Count > 0)
{
<MudDivider Class="my-2" />
<MudStack direction="Row" AlignItems="AlignItems.Center" Spacing="2">
<MudText Typo="Typo.body2">进度:@processed / @accounts.Count</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">成功:@success</MudText>
<MudText Typo="Typo.body2" Color="Color.Error">失败:@failed</MudText>
@if (running)
{
<MudProgressCircular Indeterminate="true" Size="Size.Small" />
}
</MudStack>
<MudTable Items="@results" Dense="true" Hover="true" Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>账号</MudTh>
<MudTh>新用户名</MudTh>
<MudTh>结果</MudTh>
<MudTh>原因</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="账号">@context.Phone</MudTd>
<MudTd DataLabel="新用户名">@context.NewUsername</MudTd>
<MudTd DataLabel="结果">
<MudChip T="string" Size="Size.Small" Color="@(context.Success ? Color.Success : Color.Error)">
@(context.Success ? "成功" : "失败")
</MudChip>
</MudTd>
<MudTd DataLabel="原因">@((context.Error ?? "").Trim())</MudTd>
</RowTemplate>
</MudTable>
}
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel" Disabled="@running">关闭</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Start" Disabled="@running">
@if (running)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
<span>执行中...</span>
}
else
{
<span>开始修改</span>
}
</MudButton>
</DialogActions>
</MudDialog>
@code
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public IReadOnlyList<int> AccountIds { get; set; } = Array.Empty<int>();
private static readonly Regex SupportedTokenRegex = new(@"\{(?<kind>number|nuber|letter)(?<len>\d*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AnyTokenRegex = new(@"\{[^}]*\}", RegexOptions.Compiled);
private static readonly Regex UsernameRegex = new(@"^[A-Za-z][A-Za-z0-9_]{4,31}$", RegexOptions.Compiled);
private List<Account> accounts = new();
private string usernameTemplate = "";
private bool running;
private int processed;
private int success;
private int failed;
private readonly List<ResultRow> results = new();
protected override async Task OnInitializedAsync()
{
var ids = AccountIds?.Distinct().Where(x => x > 0).ToList() ?? new List<int>();
if (ids.Count == 0)
return;
var all = await AccountManagement.GetAllAccountsAsync();
accounts = all.Where(a => ids.Contains(a.Id)).OrderBy(a => a.Id).ToList();
}
private async Task Start()
{
if (running)
return;
if (accounts.Count == 0)
{
Snackbar.Add("未选择账号", Severity.Warning);
return;
}
var template = (usernameTemplate ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(template))
{
Snackbar.Add("用户名模板不能为空", Severity.Warning);
return;
}
if (accounts.Count > 1 && !SupportedTokenRegex.IsMatch(template))
{
Snackbar.Add("批量修改多个账号时,用户名模板需要包含至少一个随机变量(例如 {number3} / {letter4})", Severity.Warning);
return;
}
if (!ValidateTemplateTokens(template, out var templateErr))
{
Snackbar.Add(templateErr ?? "用户名模板不合法", Severity.Warning);
return;
}
// 批量风控检查:修改用户名是敏感操作
var batchCheck = RiskService.CheckBatchAccounts(accounts);
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<TelegramPanel.Web.Components.Dialogs.RiskWarningDialog>("批量操作风控警告", 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)
{
accounts = batchCheck.SafeAccounts.ToList();
if (accounts.Count == 0)
{
Snackbar.Add("排除风险账号后无剩余账号", Severity.Info);
return;
}
}
}
bool? confirm = await DialogService.ShowMessageBox(
"确认修改",
$"将对 {accounts.Count} 个账号批量修改用户名。是否继续?",
yesText: "继续", cancelText: "取消");
if (confirm != true)
return;
running = true;
processed = 0;
success = 0;
failed = 0;
results.Clear();
try
{
var generated = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var a in accounts)
{
if (!TryGenerateUniqueUsername(template, generated, out var newUsername, out var genErr))
{
processed++;
failed++;
results.Add(new ResultRow(a.Phone, "", false, genErr ?? "生成用户名失败"));
StateHasChanged();
continue;
}
var (ok, err, username) = await AccountTelegramTools.UpdateUsernameAsync(
accountId: a.Id,
username: newUsername,
cancellationToken: CancellationToken.None);
processed++;
if (ok) success++; else failed++;
results.Add(new ResultRow(a.Phone, newUsername, ok, err));
if (ok)
{
a.Username = username;
await AccountManagement.UpdateAccountAsync(a);
}
StateHasChanged();
}
Snackbar.Add($"用户名批量修改完成:成功 {success}/{accounts.Count},失败 {failed}/{accounts.Count}", failed == 0 ? Severity.Success : Severity.Warning);
}
catch (Exception ex)
{
Snackbar.Add($"执行失败:{ex.Message}", Severity.Error);
}
finally
{
running = false;
}
}
private static bool ValidateTemplateTokens(string template, out string? error)
{
error = null;
var tokens = AnyTokenRegex.Matches(template);
foreach (Match t in tokens)
{
if (!SupportedTokenRegex.IsMatch(t.Value))
{
error = $"不支持的变量:{t.Value}。仅支持 {{number}}/{{letter}}/{{numberN}}/{{letterN}}。";
return false;
}
var m = SupportedTokenRegex.Match(t.Value);
var lenStr = m.Groups["len"].Value;
if (string.IsNullOrWhiteSpace(lenStr))
continue;
if (!int.TryParse(lenStr, out var len) || len <= 0 || len > 32)
{
error = $"变量长度不合法:{t.Value}。长度需为 1-32。";
return false;
}
}
return true;
}
private static bool TryGenerateUniqueUsername(
string template,
HashSet<string> reserved,
out string username,
out string? error)
{
username = "";
error = null;
for (var i = 0; i < 25; i++)
{
var candidate = ExpandTemplate(template);
candidate = (candidate ?? string.Empty).Trim().TrimStart('@');
if (!UsernameRegex.IsMatch(candidate))
{
error = "生成的用户名不符合 Telegram 规则:仅允许字母/数字/下划线,长度 5-32,且必须以字母开头。";
return false;
}
if (reserved.Add(candidate))
{
username = candidate;
return true;
}
}
error = "生成的用户名在本次批量操作中重复次数过多,请增加随机位数(例如 {number6} / {letter6})。";
return false;
}
private static string ExpandTemplate(string template)
{
return SupportedTokenRegex.Replace(template, m =>
{
var kind = (m.Groups["kind"].Value ?? string.Empty).ToLowerInvariant();
if (kind == "nuber")
kind = "number";
var lenStr = m.Groups["len"].Value;
var len = string.IsNullOrWhiteSpace(lenStr) ? 1 : int.Parse(lenStr);
return kind == "number"
? RandomDigits(len)
: RandomLetters(len);
});
}
private static string RandomDigits(int length)
{
var chars = new char[length];
for (var i = 0; i < length; i++)
chars[i] = (char)('0' + RandomNumberGenerator.GetInt32(0, 10));
return new string(chars);
}
private static string RandomLetters(int length)
{
var chars = new char[length];
for (var i = 0; i < length; i++)
chars[i] = (char)('a' + RandomNumberGenerator.GetInt32(0, 26));
return new string(chars);
}
private void Cancel() => MudDialog.Close();
private sealed record ResultRow(string Phone, string NewUsername, bool Success, string? Error);
}