321 lines
12 KiB
Plaintext
321 lines
12 KiB
Plaintext
@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);
|
||
}
|