fix: cleanup stale sync data and estimate account registration
This commit is contained in:
1 parent
efebe9e66b
commit
3cdc458f27
14 files changed
+1047
-5
No files matched your search
@@ -184,7 +184,30 @@ public class ChannelManagementService
|
||||
|
||||
public async Task DeleteStaleAccountChannelsAsync(int accountId, IReadOnlyCollection<int> keepChannelIds)
|
||||
{
|
||||
await _accountChannelRepository.DeleteForAccountExceptAsync(accountId, keepChannelIds);
|
||||
var keepSet = keepChannelIds.Count == 0
|
||||
? new HashSet<int>()
|
||||
: keepChannelIds.ToHashSet();
|
||||
|
||||
var staleChannelIds = (await _accountChannelRepository.GetByAccountAsync(accountId))
|
||||
.Where(x => !keepSet.Contains(x.ChannelId))
|
||||
.Select(x => x.ChannelId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var channelId in staleChannelIds)
|
||||
await RemoveAccountChannelAsync(channelId, accountId);
|
||||
|
||||
var staleChannelIdSet = staleChannelIds.ToHashSet();
|
||||
var creatorOnlyChannelIds = (await _channelRepository.FindAsync(x => x.CreatorAccountId == accountId))
|
||||
.Select(x => x.Id)
|
||||
.Where(id => !keepSet.Contains(id) && !staleChannelIdSet.Contains(id))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var channelId in creatorOnlyChannelIds)
|
||||
await DetachCreatorFromChannelAsync(channelId, accountId);
|
||||
|
||||
await _channelRepository.DeleteOrphanedAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AccountChannel>> GetAccountChannelMembershipsAsync(int accountId, CancellationToken cancellationToken = default)
|
||||
@@ -224,6 +247,24 @@ public class ChannelManagementService
|
||||
await _channelRepository.UpdateAsync(channel);
|
||||
}
|
||||
|
||||
private async Task DetachCreatorFromChannelAsync(int channelId, int accountId)
|
||||
{
|
||||
var channel = await _channelRepository.GetByIdAsync(channelId);
|
||||
if (channel == null || channel.CreatorAccountId != accountId)
|
||||
return;
|
||||
|
||||
channel.CreatorAccountId = null;
|
||||
|
||||
if (channel.AccountChannels.Count == 0)
|
||||
{
|
||||
await _channelRepository.DeleteAsync(channel);
|
||||
return;
|
||||
}
|
||||
|
||||
channel.SyncedAt = DateTime.UtcNow;
|
||||
await _channelRepository.UpdateAsync(channel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析频道操作的执行账号:
|
||||
/// 优先使用 preferredAccountId,其次 CreatorAccountId,否则从关联表中挑选一个管理员账号。
|
||||
|
||||
@@ -127,7 +127,30 @@ public class GroupManagementService
|
||||
|
||||
public async Task DeleteStaleAccountGroupsAsync(int accountId, IReadOnlyCollection<int> keepGroupIds)
|
||||
{
|
||||
await _accountGroupRepository.DeleteForAccountExceptAsync(accountId, keepGroupIds);
|
||||
var keepSet = keepGroupIds.Count == 0
|
||||
? new HashSet<int>()
|
||||
: keepGroupIds.ToHashSet();
|
||||
|
||||
var staleGroupIds = (await _accountGroupRepository.GetByAccountAsync(accountId))
|
||||
.Where(x => !keepSet.Contains(x.GroupId))
|
||||
.Select(x => x.GroupId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var groupId in staleGroupIds)
|
||||
await RemoveAccountGroupAsync(groupId, accountId);
|
||||
|
||||
var staleGroupIdSet = staleGroupIds.ToHashSet();
|
||||
var creatorOnlyGroupIds = (await _groupRepository.FindAsync(x => x.CreatorAccountId == accountId))
|
||||
.Select(x => x.Id)
|
||||
.Where(id => !keepSet.Contains(id) && !staleGroupIdSet.Contains(id))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var groupId in creatorOnlyGroupIds)
|
||||
await DetachCreatorFromGroupAsync(groupId, accountId);
|
||||
|
||||
await _groupRepository.DeleteOrphanedAsync();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AccountGroup>> GetAccountGroupMembershipsAsync(int accountId, CancellationToken cancellationToken = default)
|
||||
@@ -167,6 +190,24 @@ public class GroupManagementService
|
||||
await _groupRepository.UpdateAsync(group);
|
||||
}
|
||||
|
||||
private async Task DetachCreatorFromGroupAsync(int groupId, int accountId)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(groupId);
|
||||
if (group == null || group.CreatorAccountId != accountId)
|
||||
return;
|
||||
|
||||
group.CreatorAccountId = null;
|
||||
|
||||
if (group.AccountGroups.Count == 0)
|
||||
{
|
||||
await _groupRepository.DeleteAsync(group);
|
||||
return;
|
||||
}
|
||||
|
||||
group.SyncedAt = DateTime.UtcNow;
|
||||
await _groupRepository.UpdateAsync(group);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析群组操作的执行账号:
|
||||
/// 优先使用 preferredAccountId,其次 CreatorAccountId,否则从关联表中挑选一个管理员账号。
|
||||
|
||||
@@ -88,6 +88,7 @@ public class AccountTelegramToolsService
|
||||
if (account != null)
|
||||
{
|
||||
profile.ApplyTo(account);
|
||||
await TryPopulateEstimatedRegistrationAsync(account, client, accountId, cancellationToken);
|
||||
}
|
||||
|
||||
var summary = "正常";
|
||||
@@ -246,6 +247,127 @@ public class AccountTelegramToolsService
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task EnsureEstimatedRegistrationAsync(int accountId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var account = await _accountManagement.GetAccountAsync(accountId);
|
||||
if (account == null)
|
||||
return;
|
||||
|
||||
if (account.EstimatedRegistrationAt.HasValue || account.EstimatedRegistrationCheckedAtUtc.HasValue)
|
||||
return;
|
||||
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
|
||||
await TryPopulateEstimatedRegistrationAsync(account, client, accountId, cancellationToken);
|
||||
|
||||
if (account.EstimatedRegistrationAt.HasValue || account.EstimatedRegistrationCheckedAtUtc.HasValue)
|
||||
await _accountManagement.UpdateAccountAsync(account);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "EnsureEstimatedRegistrationAsync skipped for account {AccountId}", accountId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryPopulateEstimatedRegistrationAsync(
|
||||
Account account,
|
||||
Client client,
|
||||
int accountId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (account.EstimatedRegistrationAt.HasValue || account.EstimatedRegistrationCheckedAtUtc.HasValue)
|
||||
return;
|
||||
|
||||
var (checkedSuccessfully, estimatedAtUtc) = await TryGetEstimatedRegistrationFromSystemMessagesAsync(client, accountId, cancellationToken);
|
||||
if (!checkedSuccessfully)
|
||||
return;
|
||||
|
||||
if (estimatedAtUtc.HasValue)
|
||||
account.EstimatedRegistrationAt = estimatedAtUtc.Value;
|
||||
|
||||
account.EstimatedRegistrationCheckedAtUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private async Task<(bool CheckedSuccessfully, DateTime? EstimatedAtUtc)> TryGetEstimatedRegistrationFromSystemMessagesAsync(
|
||||
Client client,
|
||||
int accountId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var peer = await TryResolveSystemPeerAsync(client);
|
||||
if (peer == null)
|
||||
return (true, null);
|
||||
|
||||
const int pageSize = 100;
|
||||
const int maxPages = 200;
|
||||
var offsetId = 0;
|
||||
DateTime? earliest = null;
|
||||
|
||||
for (var page = 0; page < maxPages; page++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var history = await ExecuteTelegramRequestAsync(
|
||||
accountId,
|
||||
"读取 777000 系统通知历史",
|
||||
() => client.Messages_GetHistory(peer, offset_id: offsetId, limit: pageSize),
|
||||
cancellationToken,
|
||||
resetClientOnTimeout: true);
|
||||
|
||||
if (history.Messages == null || history.Messages.Length == 0)
|
||||
break;
|
||||
|
||||
foreach (var msgBase in history.Messages)
|
||||
{
|
||||
if (msgBase is not Message message)
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message.message))
|
||||
continue;
|
||||
|
||||
var messageUtc = message.Date.ToUniversalTime();
|
||||
if (!earliest.HasValue || messageUtc < earliest.Value)
|
||||
earliest = messageUtc;
|
||||
}
|
||||
|
||||
var nextOffsetId = history.Messages
|
||||
.Select(GetTelegramMessageId)
|
||||
.Where(id => id > 0)
|
||||
.DefaultIfEmpty(0)
|
||||
.Min();
|
||||
|
||||
if (nextOffsetId <= 0 || nextOffsetId == offsetId || history.Messages.Length < pageSize)
|
||||
break;
|
||||
|
||||
offsetId = nextOffsetId;
|
||||
}
|
||||
|
||||
return (true, earliest);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to estimate registration time from 777000 for account {AccountId}", accountId);
|
||||
return (false, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetTelegramMessageId(MessageBase msgBase) => msgBase switch
|
||||
{
|
||||
Message message => message.id,
|
||||
MessageService service => service.id,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
public async Task<IReadOnlyList<TelegramAuthorizationInfo>> GetAuthorizationsAsync(int accountId)
|
||||
{
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId);
|
||||
|
||||
@@ -38,6 +38,16 @@ public class Account
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime LastSyncAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 通过 777000 系统通知最早消息时间估算的注册时间(非百分百准确)
|
||||
/// </summary>
|
||||
public DateTime? EstimatedRegistrationAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次尝试估算注册时间的时间(UTC)
|
||||
/// </summary>
|
||||
public DateTime? EstimatedRegistrationCheckedAtUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后一次登录 Telegram 的时间(UTC),用于风控检查
|
||||
/// </summary>
|
||||
|
||||
Generated
+745
@@ -0,0 +1,745 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using TelegramPanel.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TelegramPanel.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260309051752_AddAccountEstimatedRegistrationFields")]
|
||||
partial class AddAccountEstimatedRegistrationFields
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.24");
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ApiHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ApiId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EstimatedRegistrationAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EstimatedRegistrationCheckedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
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)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("TelegramStatusCheckedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TelegramStatusDetails")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool?>("TelegramStatusOk")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TelegramStatusSummary")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TwoFactorPassword")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("Phone")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountCategory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Color")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("ExcludeFromOperations")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AccountCategories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountChannel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsCreator")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.HasIndex("AccountId", "ChannelId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ChannelId", "IsCreator", "IsAdmin");
|
||||
|
||||
b.ToTable("AccountChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("GroupId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsAdmin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsCreator")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("AccountId", "GroupId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("GroupId", "IsCreator", "IsAdmin");
|
||||
|
||||
b.ToTable("AccountGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BatchTask", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Completed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Failed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("StartedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TaskType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Total")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("BatchTasks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Bot", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastSyncAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("LastUpdateId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("Bots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("About")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("AccessHash")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("ChannelStatusCheckedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChannelStatusError")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool?>("ChannelStatusOk")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsBroadcast")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MemberCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TelegramId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("ChannelStatusOk");
|
||||
|
||||
b.HasIndex("TelegramId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("BotChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannelCategory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("BotChannelCategories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannelMember", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BotChannelId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BotId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BotChannelId");
|
||||
|
||||
b.HasIndex("BotId");
|
||||
|
||||
b.HasIndex("BotId", "BotChannelId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("BotChannelMembers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Channel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("About")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("AccessHash")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("CreatorAccountId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("GroupId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsBroadcast")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MemberCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TelegramId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatorAccountId");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("SyncedAt");
|
||||
|
||||
b.HasIndex("TelegramId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.ChannelGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ChannelGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Group", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("About")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("AccessHash")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("CreatorAccountId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MemberCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("TelegramId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("CreatorAccountId");
|
||||
|
||||
b.HasIndex("SyncedAt");
|
||||
|
||||
b.HasIndex("TelegramId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("Groups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.GroupCategory", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("GroupCategories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.AccountCategory", "Category")
|
||||
.WithMany("Accounts")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountChannel", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.Account", "Account")
|
||||
.WithMany("AccountChannels")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.Channel", "Channel")
|
||||
.WithMany("AccountChannels")
|
||||
.HasForeignKey("ChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("Channel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountGroup", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.Account", "Account")
|
||||
.WithMany("AccountGroups")
|
||||
.HasForeignKey("AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.Group", "Group")
|
||||
.WithMany("AccountGroups")
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("Group");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannel", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.BotChannelCategory", "Category")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannelMember", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.BotChannel", "BotChannel")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("BotChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.Bot", "Bot")
|
||||
.WithMany("ChannelMembers")
|
||||
.HasForeignKey("BotId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Bot");
|
||||
|
||||
b.Navigation("BotChannel");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Channel", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.Account", "CreatorAccount")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("CreatorAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.ChannelGroup", "Group")
|
||||
.WithMany("Channels")
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("CreatorAccount");
|
||||
|
||||
b.Navigation("Group");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Group", b =>
|
||||
{
|
||||
b.HasOne("TelegramPanel.Data.Entities.GroupCategory", "Category")
|
||||
.WithMany("Groups")
|
||||
.HasForeignKey("CategoryId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("TelegramPanel.Data.Entities.Account", "CreatorAccount")
|
||||
.WithMany("Groups")
|
||||
.HasForeignKey("CreatorAccountId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("Category");
|
||||
|
||||
b.Navigation("CreatorAccount");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
|
||||
{
|
||||
b.Navigation("AccountChannels");
|
||||
|
||||
b.Navigation("AccountGroups");
|
||||
|
||||
b.Navigation("Channels");
|
||||
|
||||
b.Navigation("Groups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.AccountCategory", b =>
|
||||
{
|
||||
b.Navigation("Accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Bot", b =>
|
||||
{
|
||||
b.Navigation("ChannelMembers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannel", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.BotChannelCategory", b =>
|
||||
{
|
||||
b.Navigation("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Channel", b =>
|
||||
{
|
||||
b.Navigation("AccountChannels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.ChannelGroup", b =>
|
||||
{
|
||||
b.Navigation("Channels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Group", b =>
|
||||
{
|
||||
b.Navigation("AccountGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.GroupCategory", b =>
|
||||
{
|
||||
b.Navigation("Groups");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TelegramPanel.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAccountEstimatedRegistrationFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "EstimatedRegistrationAt",
|
||||
table: "Accounts",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "EstimatedRegistrationCheckedAtUtc",
|
||||
table: "Accounts",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EstimatedRegistrationAt",
|
||||
table: "Accounts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EstimatedRegistrationCheckedAtUtc",
|
||||
table: "Accounts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,12 @@ namespace TelegramPanel.Data.Migrations
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EstimatedRegistrationAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EstimatedRegistrationCheckedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -183,6 +183,20 @@ public class ChannelRepository : Repository<Channel>, IChannelRepository
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> DeleteOrphanedAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var orphanedChannels = await _dbSet
|
||||
.Where(c => c.CreatorAccountId == null && !c.AccountChannels.Any())
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (orphanedChannels.Count == 0)
|
||||
return 0;
|
||||
|
||||
_dbSet.RemoveRange(orphanedChannels);
|
||||
await SaveChangesWithSqliteLockRetryAsync(cancellationToken);
|
||||
return orphanedChannels.Count;
|
||||
}
|
||||
|
||||
public async Task<(IReadOnlyList<Channel> Items, int TotalCount)> QueryForViewPagedAsync(
|
||||
int accountId,
|
||||
int? groupId,
|
||||
|
||||
@@ -131,6 +131,20 @@ public class GroupRepository : Repository<Group>, IGroupRepository
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> DeleteOrphanedAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var orphanedGroups = await _dbSet
|
||||
.Where(g => g.CreatorAccountId == null && !g.AccountGroups.Any())
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (orphanedGroups.Count == 0)
|
||||
return 0;
|
||||
|
||||
_dbSet.RemoveRange(orphanedGroups);
|
||||
await SaveChangesWithSqliteLockRetryAsync(cancellationToken);
|
||||
return orphanedGroups.Count;
|
||||
}
|
||||
|
||||
public async Task<(IReadOnlyList<Group> Items, int TotalCount)> QueryForViewPagedAsync(
|
||||
int accountId,
|
||||
int? categoryId,
|
||||
|
||||
@@ -13,6 +13,7 @@ public interface IChannelRepository : IRepository<Channel>
|
||||
Task<IEnumerable<Channel>> GetForAccountAsync(int accountId, bool includeNonCreator);
|
||||
Task<IEnumerable<Channel>> GetByGroupAsync(int groupId);
|
||||
Task<IEnumerable<Channel>> GetBroadcastChannelsAsync();
|
||||
Task<int> DeleteOrphanedAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<(IReadOnlyList<Channel> Items, int TotalCount)> QueryForViewPagedAsync(
|
||||
int accountId,
|
||||
|
||||
@@ -9,6 +9,7 @@ public interface IGroupRepository : IRepository<Group>
|
||||
{
|
||||
Task<Group?> GetByTelegramIdAsync(long telegramId);
|
||||
Task<IEnumerable<Group>> GetByCreatorAccountAsync(int accountId);
|
||||
Task<int> DeleteOrphanedAsync(CancellationToken cancellationToken = default);
|
||||
Task<(IReadOnlyList<Group> Items, int TotalCount)> QueryForViewPagedAsync(
|
||||
int accountId,
|
||||
int? categoryId,
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
else
|
||||
{
|
||||
<MudStack Spacing="3">
|
||||
<MudTextField Value="@FormatCreatedAt()" Label="创建时间" ReadOnly="true" Variant="Variant.Outlined" />
|
||||
<MudTextField Value="@FormatEstimatedRegistrationAt()" Label="注册时间(估算,非百分百正确)" ReadOnly="true" Variant="Variant.Outlined" />
|
||||
<MudTextField Value="@FormatCreatedAt()" Label="导入时间" ReadOnly="true" Variant="Variant.Outlined" />
|
||||
<MudTextField Value="@account.SessionPath" Label="Session 路径" ReadOnly="true" Variant="Variant.Outlined" />
|
||||
|
||||
<MudDivider Class="my-2" />
|
||||
@@ -86,6 +87,11 @@
|
||||
return account == null ? "-" : TimeZone.Format(account.CreatedAt, "yyyy-MM-dd HH:mm");
|
||||
}
|
||||
|
||||
private string FormatEstimatedRegistrationAt()
|
||||
{
|
||||
return account == null ? "-" : TimeZone.Format(account.EstimatedRegistrationAt, "yyyy-MM-dd HH:mm", "-");
|
||||
}
|
||||
|
||||
private string DisplayPassword()
|
||||
{
|
||||
if (account == null)
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
<MudTh>频道/群组</MudTh>
|
||||
<MudTh>状态</MudTh>
|
||||
<MudTh>Telegram 状态</MudTh>
|
||||
<MudTh>注册时间</MudTh>
|
||||
<MudTh>注册时间(估算,非百分百正确)</MudTh>
|
||||
<MudTh>最后数据同步</MudTh>
|
||||
<MudTh Style="min-width: 180px;">操作</MudTh>
|
||||
</HeaderContent>
|
||||
@@ -156,7 +156,7 @@
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Default">未检测</MudChip>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="注册时间"><PanelTime Value="context.CreatedAt" /></MudTd>
|
||||
<MudTd DataLabel="注册时间(估算,非百分百正确)"><PanelTime Value="context.EstimatedRegistrationAt" EmptyText="-" /></MudTd>
|
||||
<MudTd DataLabel="最后数据同步"><PanelTime Value="context.LastSyncAt" /></MudTd>
|
||||
<MudTd DataLabel="操作">
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Style="flex-wrap: wrap;">
|
||||
|
||||
@@ -324,6 +324,8 @@ public class DataSyncService
|
||||
|
||||
await _groupManagement.DeleteStaleAccountGroupsAsync(account.Id, keepGroupIds);
|
||||
|
||||
await _telegramTools.EnsureEstimatedRegistrationAsync(account.Id, cancellationToken);
|
||||
|
||||
await _accountManagement.UpdateLastSyncTimeAsync(account.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in new issue
Block a user