feat: 账号列表批量改用户名(模板随机变量)
This commit is contained in:
1 parent
0aee049813
commit
5c2b59ed02
2 files changed
+347
No files matched your search
@@ -0,0 +1,320 @@
|
||||
@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);
|
||||
}
|
||||
@@ -47,6 +47,10 @@
|
||||
Disabled="@(loading || selectedAccounts.Count == 0)" OnClick="OpenBatchChangeNickname" Class="ml-2">
|
||||
批量改昵称(已选)
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Warning" StartIcon="@Icons.Material.Filled.AlternateEmail"
|
||||
Disabled="@(loading || selectedAccounts.Count == 0)" OnClick="OpenBatchChangeUsername" Class="ml-2">
|
||||
批量改用户名(已选)
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Edit"
|
||||
Disabled="@(loading || selectedAccounts.Count == 0)" OnClick="OpenBatchChangeBio" Class="ml-2">
|
||||
批量改Bio(已选)
|
||||
@@ -328,6 +332,29 @@
|
||||
await LoadAccounts();
|
||||
}
|
||||
|
||||
private async Task OpenBatchChangeUsername()
|
||||
{
|
||||
if (loading)
|
||||
return;
|
||||
|
||||
var ids = selectedAccounts.Select(a => a.Id).Distinct().ToList();
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
Snackbar.Add("请先选择账号", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["AccountIds"] = ids
|
||||
};
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.BatchChangeUsernameDialog>("批量修改用户名", parameters, options)!;
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
await LoadAccounts();
|
||||
}
|
||||
|
||||
private async Task OpenBatchChangeBio()
|
||||
{
|
||||
if (loading)
|
||||
|
||||
Reference in new issue
Block a user