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)