实现账号风控系统:24小时登录时长检查

核心功能:
- 新增 AccountRiskService 风控检查服务
- 添加 Account.LastLoginAt 字段记录登录时间
- 实现 24 小时风控阈值检查逻辑
- 新增 RiskWarningDialog 风控警告对话框

风控集成点:
- 创建频道(ChannelCreate)
- 深度探测账号状态(Accounts - 深度探测)
- 批量修改昵称(BatchChangeNicknameDialog)
- 批量修改个性签名(BatchChangeBioDialog)
- 编辑用户资料(EditUserProfileDialog)

风控逻辑:
- 刚导入账号(LastLoginAt 为 null)→ 高风险
- 登录时长 < 24 小时 → 高风险,提示剩余等待时间
- 登录时长 ≥ 24 小时 → 安全,允许操作

数据库变更:
- 添加 Accounts.LastLoginAt (nullable DateTime) 字段
- 创建数据库迁移 20251220000000_AddLastLoginAt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
meoacgxandClaude committed 2025-12-20 21:23:28 +08:00
1 parent 1e09449431
commit ccb3f3e8a2
13 files changed
+550 -2

No files matched your search

@@ -35,6 +35,9 @@ public static class ServiceCollectionExtensions
services.AddScoped<BatchTaskManagementService>();
services.AddScoped<BotManagementService>();
// 注册风控服务
services.AddScoped<AccountRiskService>();
return services;
}
}
@@ -0,0 +1,154 @@
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Core.Services;
/// <summary>
/// 账号风控检查服务
/// </summary>
public class AccountRiskService
{
/// <summary>
/// 风控阈值:登录后需满 24 小时才能进行敏感操作
/// </summary>
private const double RISK_THRESHOLD_HOURS = 24.0;
/// <summary>
/// 检查单个账号的登录时长是否满足风控要求
/// </summary>
public RiskCheckResult CheckLoginDuration(Account account)
{
if (account.LastLoginAt == null)
{
// 无登录记录,视为高风险(刚导入的账号)
return new RiskCheckResult
{
IsRisky = true,
Message = "账号未记录登录时间",
DetailedMessage = "该账号尚未记录登录时间(可能是刚导入的新账号),建议等待 24 小时后再进行敏感操作"
};
}
var loginHours = (DateTime.UtcNow - account.LastLoginAt.Value).TotalHours;
if (loginHours < RISK_THRESHOLD_HOURS)
{
var remainingHours = RISK_THRESHOLD_HOURS - loginHours;
return new RiskCheckResult
{
IsRisky = true,
Message = $"账号登录时长不足 24 小时(当前:{loginHours:F1} 小时)",
DetailedMessage = $"建议等待 {remainingHours:F1} 小时后再进行敏感操作,以降低被 Telegram 风控的风险"
};
}
return new RiskCheckResult
{
IsRisky = false,
Message = "账号登录时长已满足要求",
DetailedMessage = $"当前登录时长:{loginHours:F1} 小时"
};
}
/// <summary>
/// 批量检查账号的登录时长
/// </summary>
public BatchRiskCheckResult CheckBatchAccounts(IEnumerable<Account> accounts)
{
var accountList = accounts.ToList();
var riskyAccounts = new List<Account>();
var safeAccounts = new List<Account>();
foreach (var account in accountList)
{
var check = CheckLoginDuration(account);
if (check.IsRisky)
riskyAccounts.Add(account);
else
safeAccounts.Add(account);
}
return new BatchRiskCheckResult
{
TotalCount = accountList.Count,
RiskyCount = riskyAccounts.Count,
SafeCount = safeAccounts.Count,
RiskyAccounts = riskyAccounts,
SafeAccounts = safeAccounts,
HasRiskyAccounts = riskyAccounts.Count > 0
};
}
}
/// <summary>
/// 单账号风控检查结果
/// </summary>
public class RiskCheckResult
{
/// <summary>
/// 是否存在风险
/// </summary>
public bool IsRisky { get; set; }
/// <summary>
/// 风险消息
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 详细说明
/// </summary>
public string DetailedMessage { get; set; } = string.Empty;
}
/// <summary>
/// 批量账号风控检查结果
/// </summary>
public class BatchRiskCheckResult
{
/// <summary>
/// 总账号数
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// 风险账号数
/// </summary>
public int RiskyCount { get; set; }
/// <summary>
/// 安全账号数
/// </summary>
public int SafeCount { get; set; }
/// <summary>
/// 风险账号列表
/// </summary>
public List<Account> RiskyAccounts { get; set; } = new();
/// <summary>
/// 安全账号列表
/// </summary>
public List<Account> SafeAccounts { get; set; } = new();
/// <summary>
/// 是否存在风险账号
/// </summary>
public bool HasRiskyAccounts { get; set; }
/// <summary>
/// 获取风险摘要信息
/// </summary>
public string GetRiskySummary()
{
if (RiskyCount == 0)
return "所有账号均已满足登录时长要求";
var riskyPhones = string.Join("、", RiskyAccounts.Select(a =>
{
var hours = a.GetLoginHours();
return hours.HasValue ? $"{a.Phone}({hours.Value:F1}h)" : a.Phone;
}));
return $"以下账号登录时长不足 24 小时:{riskyPhones}";
}
}
@@ -0,0 +1,17 @@
namespace TelegramPanel.Core.Services;
/// <summary>
/// 风控警告对话框的用户操作
/// </summary>
public enum RiskWarningAction
{
/// <summary>
/// 继续操作(包含风险账号)
/// </summary>
Continue,
/// <summary>
/// 排除风险账号后继续
/// </summary>
ExcludeRisky
}
@@ -21,6 +21,11 @@ public class Account
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastSyncAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// 最后一次登录 Telegram 的时间(UTC),用于风控检查
/// </summary>
public DateTime? LastLoginAt { get; set; }
/// <summary>
/// Telegram 状态检测结果摘要(用于页面刷新后仍可展示上次检测结论)
/// </summary>
@@ -0,0 +1,30 @@
namespace TelegramPanel.Data.Entities;
/// <summary>
/// Account 实体扩展方法
/// </summary>
public static class AccountExtensions
{
/// <summary>
/// 获取登录小时数的格式化字符串
/// </summary>
public static string GetLoginHoursFormatted(this Account account)
{
if (account.LastLoginAt == null)
return "未知";
var hours = (DateTime.UtcNow - account.LastLoginAt.Value).TotalHours;
return hours.ToString("F1");
}
/// <summary>
/// 获取登录小时数
/// </summary>
public static double? GetLoginHours(this Account account)
{
if (account.LastLoginAt == null)
return null;
return (DateTime.UtcNow - account.LastLoginAt.Value).TotalHours;
}
}
@@ -0,0 +1,32 @@
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TelegramPanel.Data.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20251220000000_AddLastLoginAt")]
/// <inheritdoc />
public partial class AddLastLoginAt : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastLoginAt",
table: "Accounts",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastLoginAt",
table: "Accounts");
}
}
}
@@ -40,9 +40,16 @@ namespace TelegramPanel.Data.Migrations
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("TEXT");
b.Property<DateTime>("LastSyncAt")
.HasColumnType("TEXT");
b.Property<string>("Nickname")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
@@ -53,6 +60,18 @@ namespace TelegramPanel.Data.Migrations
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime?>("TelegramStatusCheckedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("TelegramStatusDetails")
.HasColumnType("TEXT");
b.Property<bool?>("TelegramStatusOk")
.HasColumnType("INTEGER");
b.Property<string>("TelegramStatusSummary")
.HasColumnType("TEXT");
b.Property<long>("UserId")
.HasColumnType("INTEGER");
@@ -1,6 +1,7 @@
@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 TelegramPanel.Data.Entities
@@ -101,6 +102,38 @@
return;
}
// 批量风控检查:修改 Bio 是敏感操作
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;
}
}
// else: 用户选择继续所有账号
}
bool? confirm = await DialogService.ShowMessageBox(
"确认修改",
$"将对 {accounts.Count} 个账号批量修改 Bio(简介)。是否继续?",
@@ -1,6 +1,7 @@
@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 TelegramPanel.Data.Entities
@@ -112,6 +113,38 @@
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;
}
}
// else: 用户选择继续所有账号
}
bool? confirm = await DialogService.ShowMessageBox(
"确认修改",
$"将对 {accounts.Count} 个账号批量修改昵称。是否继续?",
@@ -1,6 +1,7 @@
@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 TelegramPanel.Data.Entities
@@ -156,6 +157,26 @@
if (confirm != true)
return;
// 风控检查:修改资料是敏感操作
var riskCheck = RiskService.CheckLoginDuration(account);
if (riskCheck.IsRisky)
{
var parameters = new DialogParameters
{
["Message"] = riskCheck.Message,
["DetailedMessage"] = riskCheck.DetailedMessage,
["ShowRecommendations"] = true
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium };
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.RiskWarningDialog>("风控警告", parameters, options);
var result = await dialog.Result;
if (result.Canceled)
{
saving = false;
return;
}
}
saving = true;
try
{
@@ -0,0 +1,110 @@
@namespace TelegramPanel.Web.Components.Dialogs
@using TelegramPanel.Core.Services
@using TelegramPanel.Data.Entities
@* 账号风控警告对话框 *@
<MudDialog>
<DialogContent>
<MudAlert Severity="Severity.Warning" Icon="@Icons.Material.Filled.Warning" Class="mb-4">
<MudText Typo="Typo.body1" Class="mb-2">
<strong>⚠️ 风控警告</strong>
</MudText>
<MudText Typo="Typo.body2">
@Message
</MudText>
@if (!string.IsNullOrWhiteSpace(DetailedMessage))
{
<MudText Typo="Typo.body2" Class="mt-2">
@DetailedMessage
</MudText>
}
</MudAlert>
@if (ShowRecommendations)
{
<MudText Typo="Typo.subtitle2" Class="mb-2">
<strong>建议操作:</strong>
</MudText>
<MudList T="string" Dense="true" Class="pl-2">
<MudListItem T="string" Icon="@Icons.Material.Filled.Schedule">
等待满 24 小时后再进行敏感操作
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.SlowMotionVideo">
降低操作频率,避免被 Telegram 风控系统标记
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.SafetyCheck">
优先使用低风险操作替代
</MudListItem>
</MudList>
}
@if (RiskyAccounts != null && RiskyAccounts.Any())
{
<MudText Typo="Typo.subtitle2" Class="mt-3 mb-2">
<strong>风险账号列表:</strong>
</MudText>
<MudPaper Class="pa-2" Elevation="0" Style="max-height: 200px; overflow-y: auto;">
@foreach (var account in RiskyAccounts)
{
<MudChip T="string" Size="Size.Small" Color="Color.Warning" Class="ma-1">
@account.Phone
@if (account.LastLoginAt != null)
{
<text> (登录 @account.GetLoginHoursFormatted() 小时)</text>
}
</MudChip>
}
</MudPaper>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Color="Color.Default">
取消操作
</MudButton>
@if (ShowExcludeOption && RiskyAccounts != null && RiskyAccounts.Any())
{
<MudButton OnClick="ExcludeRisky" Color="Color.Info">
排除风险账号后继续
</MudButton>
}
<MudButton OnClick="Continue" Color="Color.Warning" Variant="Variant.Filled">
我了解风险,继续操作
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
MudDialogInstance? MudDialog { get; set; }
[Parameter]
public string Message { get; set; } = "存在风控风险";
[Parameter]
public string? DetailedMessage { get; set; }
[Parameter]
public bool ShowRecommendations { get; set; } = true;
[Parameter]
public bool ShowExcludeOption { get; set; } = false;
[Parameter]
public List<Account>? RiskyAccounts { get; set; }
private void Cancel()
{
MudDialog?.Close(DialogResult.Cancel());
}
private void Continue()
{
MudDialog?.Close(DialogResult.Ok(RiskWarningAction.Continue));
}
private void ExcludeRisky()
{
MudDialog?.Close(DialogResult.Ok(RiskWarningAction.ExcludeRisky));
}
}
@@ -2,10 +2,12 @@
@inject AccountManagementService AccountManagement
@inject AccountCategoryManagementService CategoryManagement
@inject AccountTelegramToolsService AccountTelegramTools
@inject TelegramPanel.Core.Services.AccountRiskService RiskService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Navigation
@using TelegramPanel.Data.Entities
@using TelegramPanel.Web.Components.Dialogs
@implements IDisposable
<PageTitle>账号列表 - Telegram Panel</PageTitle>
@@ -370,11 +372,35 @@
loading = true;
bool? probeResult = await DialogService.ShowMessageBox(
"刷新 Telegram 状态",
"是否进行深度探测(将创建并删除一个测试频道,用于判断“创建频道接口是否被冻结”)?",
"是否进行深度探测(将创建并删除一个测试频道,用于判断【创建频道接口是否被冻结】)?",
yesText: "深度探测",
cancelText: "普通刷新");
var probe = probeResult == true;
// 风控检查:深度探测是敏感操作
if (probe)
{
var riskCheck = RiskService.CheckLoginDuration(account);
if (riskCheck.IsRisky)
{
var parameters = new DialogParameters
{
["Message"] = riskCheck.Message,
["DetailedMessage"] = riskCheck.DetailedMessage,
["ShowRecommendations"] = true
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium };
var dialog = DialogService.Show<RiskWarningDialog>("风控警告", parameters, options);
var dialogResult = await dialog.Result;
if (dialogResult.Canceled)
{
loading = false;
return; // 用户取消操作
}
}
}
var result = await AccountTelegramTools.RefreshAccountStatusAsync(account.Id, probeCreateChannel: probe, cancellationToken: _pageCts.Token);
telegramStatus[account.Id] = result;
@@ -406,12 +432,53 @@
{
bool? probeResult = await DialogService.ShowMessageBox(
"批量刷新 Telegram 状态",
"是否进行深度探测(将对每个账号创建并删除一个测试频道,用于判断“创建频道接口是否被冻结”)?",
"是否进行深度探测(将对每个账号创建并删除一个测试频道,用于判断【创建频道接口是否被冻结】)?",
yesText: "深度探测",
cancelText: "普通刷新");
var probe = probeResult == true;
var targets = filteredAccounts.ToList();
// 批量风控检查:深度探测是敏感操作
if (probe && targets.Count > 0)
{
var batchCheck = RiskService.CheckBatchAccounts(targets);
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<RiskWarningDialog>("批量操作风控警告", parameters, options);
var dialogResult = await dialog.Result;
if (dialogResult.Canceled)
{
loading = false;
return; // 用户取消操作
}
if (dialogResult.Data is TelegramPanel.Core.Services.RiskWarningAction action
&& action == TelegramPanel.Core.Services.RiskWarningAction.ExcludeRisky)
{
// 排除风险账号后继续
targets = batchCheck.SafeAccounts.ToList();
if (targets.Count == 0)
{
Snackbar.Add("排除风险账号后无剩余账号", Severity.Info);
loading = false;
return;
}
}
// else: 用户选择继续所有账号
}
}
foreach (var a in targets)
{
try
@@ -1,11 +1,14 @@
@page "/channels/create"
@inject IChannelService ChannelService
@inject AccountManagementService AccountManagement
@inject TelegramPanel.Core.Services.AccountRiskService RiskService
@inject ChannelManagementService ChannelManagement
@inject ChannelGroupManagementService ChannelGroupManagement
@inject ISnackbar Snackbar
@inject NavigationManager Navigation
@inject IDialogService DialogService
@using TelegramPanel.Data.Entities
@using TelegramPanel.Web.Components.Dialogs
<PageTitle>创建频道 - Telegram Panel</PageTitle>
@@ -139,6 +142,27 @@
return;
}
// 风控检查:创建频道是敏感操作
var account = await AccountManagement.GetAccountAsync(selectedAccountId);
if (account != null)
{
var riskCheck = RiskService.CheckLoginDuration(account);
if (riskCheck.IsRisky)
{
var parameters = new DialogParameters
{
["Message"] = riskCheck.Message,
["DetailedMessage"] = riskCheck.DetailedMessage,
["ShowRecommendations"] = true
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium };
var dialog = DialogService.Show<RiskWarningDialog>("风控警告", parameters, options);
var result = await dialog.Result;
if (result.Canceled)
return; // 用户取消操作
}
}
creating = true;
try