feat: 账号列表批量改用户名(模板随机变量)

This commit is contained in:
meoacgx committed 2025-12-21 01:06:49 +08:00
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)