From 5c2b59ed028f11fa9d6779b9fb85c6676da8938e Mon Sep 17 00:00:00 2001 From: meoacgx Date: Sun, 21 Dec 2025 01:06:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B4=A6=E5=8F=B7=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E6=94=B9=E7=94=A8=E6=88=B7=E5=90=8D=EF=BC=88?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E9=9A=8F=E6=9C=BA=E5=8F=98=E9=87=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialogs/BatchChangeUsernameDialog.razor | 320 ++++++++++++++++++ .../Components/Pages/Accounts.razor | 27 ++ 2 files changed, 347 insertions(+) create mode 100644 src/TelegramPanel.Web/Components/Dialogs/BatchChangeUsernameDialog.razor diff --git a/src/TelegramPanel.Web/Components/Dialogs/BatchChangeUsernameDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BatchChangeUsernameDialog.razor new file mode 100644 index 0000000..6f1a16d --- /dev/null +++ b/src/TelegramPanel.Web/Components/Dialogs/BatchChangeUsernameDialog.razor @@ -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 + + + + + + 将对选中的账号批量修改用户名(t.me/xxx)。用户名模板支持变量: + + {number} / {letter} 表示 1 位随机数字/字母;{number3} 表示 3 位随机数字;{letter4} 表示 4 位随机字母。可组合使用。 + + + 示例:test_{number3}{letter4} → test_123abcd + + + + 账号数量:@accounts.Count + + + + @if (running || results.Count > 0) + { + + + + 进度:@processed / @accounts.Count + 成功:@success + 失败:@failed + @if (running) + { + + } + + + + + 账号 + 新用户名 + 结果 + 原因 + + + @context.Phone + @context.NewUsername + + + @(context.Success ? "成功" : "失败") + + + @((context.Error ?? "").Trim()) + + + } + + + + 关闭 + + @if (running) + { + + 执行中... + } + else + { + 开始修改 + } + + + + +@code +{ + [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!; + [Parameter] public IReadOnlyList AccountIds { get; set; } = Array.Empty(); + + private static readonly Regex SupportedTokenRegex = new(@"\{(?number|nuber|letter)(?\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 accounts = new(); + private string usernameTemplate = ""; + + private bool running; + private int processed; + private int success; + private int failed; + private readonly List results = new(); + + protected override async Task OnInitializedAsync() + { + var ids = AccountIds?.Distinct().Where(x => x > 0).ToList() ?? new List(); + 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("批量操作风控警告", 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(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 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); +} diff --git a/src/TelegramPanel.Web/Components/Pages/Accounts.razor b/src/TelegramPanel.Web/Components/Pages/Accounts.razor index 2c00a9e..303c058 100644 --- a/src/TelegramPanel.Web/Components/Pages/Accounts.razor +++ b/src/TelegramPanel.Web/Components/Pages/Accounts.razor @@ -47,6 +47,10 @@ Disabled="@(loading || selectedAccounts.Count == 0)" OnClick="OpenBatchChangeNickname" 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("批量修改用户名", parameters, options)!; + var result = await dialog.Result; + if (result is { Canceled: false }) + await LoadAccounts(); + } + private async Task OpenBatchChangeBio() { if (loading)