feat(bot-channels): 支持删除频道与状态检测筛选
This commit is contained in:
1 parent
a4669f8561
commit
8a6cdd4868
10 files changed
+1115
-2
No files matched your search
@@ -273,6 +273,7 @@ public class BotManagementService
|
||||
int botId,
|
||||
int? categoryId,
|
||||
string? search,
|
||||
int statusFilter,
|
||||
int pageIndex,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -282,6 +283,7 @@ public class BotManagementService
|
||||
categoryId: categoryId,
|
||||
broadcastOnly: true,
|
||||
search: search,
|
||||
statusFilter: statusFilter,
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize,
|
||||
cancellationToken: cancellationToken);
|
||||
@@ -348,6 +350,23 @@ public class BotManagementService
|
||||
await _botChannelRepository.DeleteAsync(ch);
|
||||
}
|
||||
|
||||
public async Task UpdateChannelStatusAsync(int botId, long telegramId, bool ok, string? error, DateTime checkedAtUtc)
|
||||
{
|
||||
var ch = await _botChannelRepository.GetByTelegramIdAsync(botId, telegramId);
|
||||
if (ch == null)
|
||||
return;
|
||||
|
||||
ch.ChannelStatusOk = ok;
|
||||
ch.ChannelStatusCheckedAtUtc = checkedAtUtc;
|
||||
ch.ChannelStatusError = ok
|
||||
? null
|
||||
: string.IsNullOrWhiteSpace(error)
|
||||
? "检测失败"
|
||||
: error.Trim()[..Math.Min(error.Trim().Length, 500)];
|
||||
|
||||
await _botChannelRepository.UpdateAsync(ch);
|
||||
}
|
||||
|
||||
public async Task SetChannelCategoryAsync(int botChannelId, int? categoryId)
|
||||
{
|
||||
var ch = await _botChannelRepository.GetByIdAsync(botChannelId);
|
||||
|
||||
@@ -37,6 +37,7 @@ public class BotTelegramService
|
||||
}
|
||||
|
||||
public sealed record BotChannelSyncResult(int AppliedUpdates, int RemovedStale);
|
||||
public sealed record ChannelStatusCheckResult(int SuccessCount, int FailedCount, int TotalCount, IReadOnlyDictionary<long, string> Failures);
|
||||
|
||||
/// <summary>
|
||||
/// 手动同步(新增 + 清理):
|
||||
@@ -632,6 +633,132 @@ public class BotTelegramService
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量检测 Bot 在频道中的可操作状态:
|
||||
/// 通过 sendMessage 发送探测消息并立即 deleteMessage;发送失败则记为异常。
|
||||
/// </summary>
|
||||
public async Task<ChannelStatusCheckResult> CheckChannelsStatusAsync(
|
||||
int botId,
|
||||
IReadOnlyList<long> channelTelegramIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var bot = await _botManagement.GetBotAsync(botId)
|
||||
?? throw new InvalidOperationException($"机器人不存在:{botId}");
|
||||
|
||||
if (!bot.IsActive)
|
||||
throw new InvalidOperationException("该机器人已停用");
|
||||
|
||||
var ids = channelTelegramIds
|
||||
.Where(x => x != 0)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (ids.Count == 0)
|
||||
return new ChannelStatusCheckResult(0, 0, 0, new Dictionary<long, string>());
|
||||
|
||||
var failures = new Dictionary<long, string>();
|
||||
|
||||
var delayMs = ReadInt(_configuration, "Telegram:DefaultDelayMs", 1000);
|
||||
if (delayMs < 0) delayMs = 0;
|
||||
if (delayMs > 10000) delayMs = 10000;
|
||||
|
||||
var maxRetries = ReadInt(_configuration, "Telegram:MaxRetries", 1);
|
||||
if (maxRetries < 0) maxRetries = 0;
|
||||
if (maxRetries > 5) maxRetries = 5;
|
||||
|
||||
var ok = 0;
|
||||
for (var index = 0; index < ids.Count; index++)
|
||||
{
|
||||
var chatId = ids[index];
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var attempt = 0;
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProbeChannelBySendDeleteAsync(bot.Token, chatId, cancellationToken);
|
||||
await _botManagement.UpdateChannelStatusAsync(bot.Id, chatId, ok: true, error: null, checkedAtUtc: DateTime.UtcNow);
|
||||
ok++;
|
||||
break;
|
||||
}
|
||||
catch (InvalidOperationException ex) when (TryGetRetryAfterSeconds(ex.Message, out var seconds))
|
||||
{
|
||||
attempt++;
|
||||
if (attempt > maxRetries)
|
||||
{
|
||||
var msg = string.IsNullOrWhiteSpace(ex.Message) ? "检测失败(限流重试超限)" : ex.Message.Trim();
|
||||
failures[chatId] = msg;
|
||||
await _botManagement.UpdateChannelStatusAsync(bot.Id, chatId, ok: false, error: msg, checkedAtUtc: DateTime.UtcNow);
|
||||
_logger.LogWarning(
|
||||
"CheckChannelsStatus retry exceeded: bot={BotId} chat={ChatId} retryAfter={RetryAfter}s",
|
||||
botId,
|
||||
chatId,
|
||||
seconds);
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"CheckChannelsStatus hit rate limit, retry after {Seconds}s (attempt {Attempt}/{Max}): bot={BotId} chat={ChatId}",
|
||||
seconds,
|
||||
attempt,
|
||||
maxRetries,
|
||||
botId,
|
||||
chatId);
|
||||
await Task.Delay(TimeSpan.FromSeconds(seconds), cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var msg = string.IsNullOrWhiteSpace(ex.Message) ? "检测失败" : ex.Message.Trim();
|
||||
failures[chatId] = msg;
|
||||
await _botManagement.UpdateChannelStatusAsync(bot.Id, chatId, ok: false, error: msg, checkedAtUtc: DateTime.UtcNow);
|
||||
_logger.LogWarning(ex, "CheckChannelsStatus failed: bot={BotId} chat={ChatId}", botId, chatId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (delayMs > 0 && index < ids.Count - 1)
|
||||
{
|
||||
var jitter = Random.Shared.Next(0, Math.Min(300, delayMs + 1));
|
||||
await Task.Delay(delayMs + jitter, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return new ChannelStatusCheckResult(ok, failures.Count, ids.Count, failures);
|
||||
}
|
||||
|
||||
private async Task ProbeChannelBySendDeleteAsync(string token, long chatId, CancellationToken cancellationToken)
|
||||
{
|
||||
var probeText = $"频道状态检测 {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}";
|
||||
var sendResult = await _api.CallAsync(token, "sendMessage", new Dictionary<string, string?>
|
||||
{
|
||||
["chat_id"] = chatId.ToString(),
|
||||
["text"] = probeText,
|
||||
["disable_notification"] = "true"
|
||||
}, cancellationToken);
|
||||
|
||||
if (sendResult.ValueKind != JsonValueKind.Object
|
||||
|| !sendResult.TryGetProperty("message_id", out var messageIdEl)
|
||||
|| !messageIdEl.TryGetInt64(out var messageId))
|
||||
{
|
||||
throw new InvalidOperationException("Bot API sendMessage 返回格式异常,无法完成频道检测");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _api.CallAsync(token, "deleteMessage", new Dictionary<string, string?>
|
||||
{
|
||||
["chat_id"] = chatId.ToString(),
|
||||
["message_id"] = messageId.ToString()
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 删除失败不判定为“频道异常”,但记录日志便于排查(例如 Bot 缺少删消息权限)。
|
||||
_logger.LogWarning(ex, "Probe deleteMessage failed: chat={ChatId}", chatId);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseChatMemberUpdate(JsonElement myChatMember, out BotApiChat chat, out string status)
|
||||
{
|
||||
chat = default;
|
||||
|
||||
@@ -177,10 +177,12 @@ public class AppDbContext : DbContext
|
||||
entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
|
||||
entity.Property(e => e.Username).HasMaxLength(100);
|
||||
entity.Property(e => e.About).HasMaxLength(1000);
|
||||
entity.Property(e => e.ChannelStatusError).HasMaxLength(500);
|
||||
|
||||
entity.HasIndex(e => e.TelegramId).IsUnique();
|
||||
entity.HasIndex(e => e.Username);
|
||||
entity.HasIndex(e => e.CategoryId);
|
||||
entity.HasIndex(e => e.ChannelStatusOk);
|
||||
|
||||
entity.HasOne(e => e.Category)
|
||||
.WithMany(c => c.Channels)
|
||||
|
||||
@@ -16,6 +16,21 @@ public class BotChannel
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime SyncedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// 频道状态检测结果:true=正常,false=异常,null=未检测
|
||||
/// </summary>
|
||||
public bool? ChannelStatusOk { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次频道状态检测时间(UTC)
|
||||
/// </summary>
|
||||
public DateTime? ChannelStatusCheckedAtUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次检测失败原因(仅异常时有值)
|
||||
/// </summary>
|
||||
public string? ChannelStatusError { get; set; }
|
||||
|
||||
public int? CategoryId { get; set; }
|
||||
|
||||
public BotChannelCategory? Category { get; set; }
|
||||
|
||||
+632
@@ -0,0 +1,632 @@
|
||||
// <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("20260302171929_AddBotChannelStatusFields")]
|
||||
partial class AddBotChannelStatusFields
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.22");
|
||||
|
||||
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<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.ToTable("AccountChannels");
|
||||
});
|
||||
|
||||
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("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<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("CreatorAccountId");
|
||||
|
||||
b.HasIndex("TelegramId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Username");
|
||||
|
||||
b.ToTable("Groups");
|
||||
});
|
||||
|
||||
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.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.Account", "CreatorAccount")
|
||||
.WithMany("Groups")
|
||||
.HasForeignKey("CreatorAccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CreatorAccount");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
|
||||
{
|
||||
b.Navigation("AccountChannels");
|
||||
|
||||
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");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TelegramPanel.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBotChannelStatusFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "ChannelStatusCheckedAtUtc",
|
||||
table: "BotChannels",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ChannelStatusError",
|
||||
table: "BotChannels",
|
||||
type: "TEXT",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ChannelStatusOk",
|
||||
table: "BotChannels",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BotChannels_ChannelStatusOk",
|
||||
table: "BotChannels",
|
||||
column: "ChannelStatusOk");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_BotChannels_ChannelStatusOk",
|
||||
table: "BotChannels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChannelStatusCheckedAtUtc",
|
||||
table: "BotChannels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChannelStatusError",
|
||||
table: "BotChannels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChannelStatusOk",
|
||||
table: "BotChannels");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +266,16 @@ namespace TelegramPanel.Data.Migrations
|
||||
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");
|
||||
|
||||
@@ -294,6 +304,8 @@ namespace TelegramPanel.Data.Migrations
|
||||
|
||||
b.HasIndex("CategoryId");
|
||||
|
||||
b.HasIndex("ChannelStatusOk");
|
||||
|
||||
b.HasIndex("TelegramId")
|
||||
.IsUnique();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ public class BotChannelRepository : Repository<BotChannel>, IBotChannelRepositor
|
||||
{
|
||||
}
|
||||
|
||||
private IQueryable<BotChannel> BuildQuery(int botId, int? categoryId, bool broadcastOnly, string? search)
|
||||
private IQueryable<BotChannel> BuildQuery(int botId, int? categoryId, bool broadcastOnly, string? search, int statusFilter)
|
||||
{
|
||||
// 兼容旧模块/旧 UI 约定:categoryId=0 表示“不筛选分类”
|
||||
if (categoryId == 0)
|
||||
@@ -32,6 +32,16 @@ public class BotChannelRepository : Repository<BotChannel>, IBotChannelRepositor
|
||||
if (broadcastOnly)
|
||||
query = query.Where(x => x.IsBroadcast);
|
||||
|
||||
// 频道状态筛选:
|
||||
// 0=全部,1=正常,2=异常,3=未检测
|
||||
query = statusFilter switch
|
||||
{
|
||||
1 => query.Where(x => x.ChannelStatusOk == true),
|
||||
2 => query.Where(x => x.ChannelStatusOk == false),
|
||||
3 => query.Where(x => x.ChannelStatusOk == null),
|
||||
_ => query
|
||||
};
|
||||
|
||||
search = (search ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
@@ -87,6 +97,7 @@ public class BotChannelRepository : Repository<BotChannel>, IBotChannelRepositor
|
||||
int? categoryId,
|
||||
bool broadcastOnly,
|
||||
string? search,
|
||||
int statusFilter,
|
||||
int pageIndex,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -95,7 +106,7 @@ public class BotChannelRepository : Repository<BotChannel>, IBotChannelRepositor
|
||||
if (pageSize <= 0) pageSize = 20;
|
||||
if (pageSize > 500) pageSize = 500;
|
||||
|
||||
var query = BuildQuery(botId, categoryId, broadcastOnly, search);
|
||||
var query = BuildQuery(botId, categoryId, broadcastOnly, search, statusFilter);
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
.Skip(pageIndex * pageSize)
|
||||
|
||||
@@ -13,6 +13,7 @@ public interface IBotChannelRepository : IRepository<BotChannel>
|
||||
int? categoryId,
|
||||
bool broadcastOnly,
|
||||
string? search,
|
||||
int statusFilter,
|
||||
int pageIndex,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -35,6 +35,15 @@
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudSelect @bind-Value="filterStatus" @bind-Value:after="OnFiltersChanged" Label="频道状态" Variant="Variant.Outlined" Dense="true"
|
||||
Class="tp-input-full-xs"
|
||||
Style="min-width: 160px; flex: 0 1 200px;" Disabled="@(selectedBotId <= 0)">
|
||||
<MudSelectItem Value="0">全部状态</MudSelectItem>
|
||||
<MudSelectItem Value="1">正常</MudSelectItem>
|
||||
<MudSelectItem Value="2">异常</MudSelectItem>
|
||||
<MudSelectItem Value="3">未检测</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add" Color="Color.Primary" Size="Size.Small"
|
||||
Title="新建分类" OnClick="OpenCreateCategoryDialog"
|
||||
Disabled="@(selectedBotId <= 0)" />
|
||||
@@ -72,12 +81,14 @@
|
||||
|
||||
<MudMenu Label="批量操作" Variant="Variant.Outlined" Color="Color.Default" StartIcon="@Icons.Material.Filled.MoreHoriz"
|
||||
Size="Size.Small" Disabled="@(loading || selectedBotId <= 0)" Dense="true" EndIcon="@Icons.Material.Filled.KeyboardArrowDown">
|
||||
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="CheckSelectedChannelsStatus">检测状态(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="ExportInvites">导出邀请(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenBatchInviteMembers">批量邀请成员(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenBatchSetAdminsByAccount">批量设置管理员(账号执行)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenSetAdmins">批量设置管理员(机器人/ID)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenBatchSetCategory">批量设置分类(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="OpenBanMember">踢出用户(已选)</MudMenuItem>
|
||||
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="DeleteSelectedChannelsAsync">删除频道(已选)</MudMenuItem>
|
||||
</MudMenu>
|
||||
|
||||
@if (selected.Count > 0)
|
||||
@@ -99,6 +110,7 @@
|
||||
<MudTh>频道名称</MudTh>
|
||||
<MudTh>用户名</MudTh>
|
||||
<MudTh>分类</MudTh>
|
||||
<MudTh>频道状态</MudTh>
|
||||
<MudTh>最后同步</MudTh>
|
||||
<MudTh>操作</MudTh>
|
||||
</HeaderContent>
|
||||
@@ -126,15 +138,26 @@
|
||||
<MudText Typo="Typo.body2" Color="Color.Default">未分类</MudText>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="频道状态">
|
||||
<MudTooltip Text="@GetChannelStatusTooltip(context)">
|
||||
<MudChip T="string" Size="Size.Small" Color="@GetChannelStatusColor(context)">
|
||||
@GetChannelStatusText(context)
|
||||
</MudChip>
|
||||
</MudTooltip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="最后同步"><PanelTime Value="context.SyncedAt" /></MudTd>
|
||||
<MudTd DataLabel="操作">
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Style="flex-wrap: wrap;">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Size="Size.Small" Color="Color.Success"
|
||||
OnClick="@(async () => await CheckSingleChannelStatus(context))" title="检测状态" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Link" Size="Size.Small" Color="Color.Info"
|
||||
OnClick="@(async () => await CopyLinkAsync(context))" Title="复制链接" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Color="Color.Default"
|
||||
OnClick="@(async () => await ShowDetailsAsync(context))" Title="查看详情" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Primary"
|
||||
OnClick="@(async () => await EditChannelAsync(context))" Title="编辑频道" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
|
||||
OnClick="@(async () => await DeleteSingleChannelAsync(context))" title="删除频道" />
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
@@ -172,6 +195,7 @@
|
||||
private HashSet<BotChannel> selected = new();
|
||||
|
||||
private int filterCategoryId = 0;
|
||||
private int filterStatus = 0;
|
||||
private string search = "";
|
||||
private bool _actionsExpanded = true;
|
||||
private CancellationTokenSource? _searchDebounceCts;
|
||||
@@ -227,6 +251,7 @@
|
||||
|
||||
selectedBotId = botId;
|
||||
filterCategoryId = 0;
|
||||
filterStatus = 0;
|
||||
search = "";
|
||||
selected.Clear();
|
||||
|
||||
@@ -285,6 +310,7 @@
|
||||
botId: selectedBotId,
|
||||
categoryId: filterCategoryId == -1 ? -1 : (filterCategoryId > 0 ? filterCategoryId : null),
|
||||
search: search,
|
||||
statusFilter: filterStatus,
|
||||
pageIndex: state.Page,
|
||||
pageSize: state.PageSize,
|
||||
cancellationToken: cancellationToken);
|
||||
@@ -367,6 +393,182 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckSingleChannelStatus(BotChannel channel)
|
||||
{
|
||||
if (selectedBotId <= 0)
|
||||
return;
|
||||
|
||||
loading = true;
|
||||
try
|
||||
{
|
||||
var result = await BotTelegram.CheckChannelsStatusAsync(
|
||||
selectedBotId,
|
||||
new List<long> { channel.TelegramId },
|
||||
CancellationToken.None);
|
||||
|
||||
if (result.FailedCount == 0)
|
||||
{
|
||||
Snackbar.Add($"频道“{channel.Title}”检测正常", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
var msg = result.Failures.TryGetValue(channel.TelegramId, out var err) ? err : "检测失败";
|
||||
Snackbar.Add($"频道“{channel.Title}”检测异常:{msg}", Severity.Warning);
|
||||
}
|
||||
|
||||
await ReloadTable();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"检测失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckSelectedChannelsStatus()
|
||||
{
|
||||
if (selectedBotId <= 0)
|
||||
return;
|
||||
|
||||
if (selected.Count == 0)
|
||||
{
|
||||
Snackbar.Add("请先选择频道", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
bool? ok = await DialogService.ShowMessageBox(
|
||||
"确认检测",
|
||||
$"将检测 {selected.Count} 个频道的状态(发送一条探测消息并立即删除)。是否继续?",
|
||||
yesText: "继续", cancelText: "取消");
|
||||
|
||||
if (ok != true)
|
||||
return;
|
||||
|
||||
loading = true;
|
||||
try
|
||||
{
|
||||
var ids = selected.Select(x => x.TelegramId).Distinct().ToList();
|
||||
var result = await BotTelegram.CheckChannelsStatusAsync(selectedBotId, ids, CancellationToken.None);
|
||||
|
||||
if (result.FailedCount == 0)
|
||||
{
|
||||
Snackbar.Add($"检测完成:{result.SuccessCount} 个频道均正常", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
var first = result.Failures.FirstOrDefault();
|
||||
var msg = $"检测完成:正常 {result.SuccessCount} 个,异常 {result.FailedCount} 个";
|
||||
if (first.Key != 0)
|
||||
msg += $"(首个失败:{first.Value})";
|
||||
Snackbar.Add(msg, Severity.Warning);
|
||||
}
|
||||
|
||||
await ReloadTable();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"检测失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSingleChannelAsync(BotChannel channel)
|
||||
{
|
||||
if (selectedBotId <= 0)
|
||||
return;
|
||||
|
||||
var channelName = string.IsNullOrWhiteSpace(channel.Title) ? channel.TelegramId.ToString() : channel.Title;
|
||||
bool? ok = await DialogService.ShowMessageBox(
|
||||
"确认删除",
|
||||
$"确定删除频道“{channelName}”吗?此操作只会移除面板中的 Bot 频道记录。",
|
||||
yesText: "删除", cancelText: "取消");
|
||||
|
||||
if (ok != true)
|
||||
return;
|
||||
|
||||
loading = true;
|
||||
try
|
||||
{
|
||||
await BotManagement.DeleteChannelByTelegramIdAsync(selectedBotId, channel.TelegramId);
|
||||
Snackbar.Add($"已删除频道:{channelName}", Severity.Success);
|
||||
await ReloadTable();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"删除失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSelectedChannelsAsync()
|
||||
{
|
||||
if (selectedBotId <= 0)
|
||||
return;
|
||||
|
||||
if (selected.Count == 0)
|
||||
{
|
||||
Snackbar.Add("请先选择频道", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
bool? ok = await DialogService.ShowMessageBox(
|
||||
"确认批量删除",
|
||||
$"确定删除已选 {selected.Count} 个频道吗?此操作只会移除面板中的 Bot 频道记录。",
|
||||
yesText: "删除", cancelText: "取消");
|
||||
|
||||
if (ok != true)
|
||||
return;
|
||||
|
||||
loading = true;
|
||||
try
|
||||
{
|
||||
var ids = selected.Select(x => x.TelegramId).Distinct().ToList();
|
||||
var success = 0;
|
||||
var failed = 0;
|
||||
|
||||
foreach (var chatId in ids)
|
||||
{
|
||||
try
|
||||
{
|
||||
await BotManagement.DeleteChannelByTelegramIdAsync(selectedBotId, chatId);
|
||||
success++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (failed == 0)
|
||||
{
|
||||
Snackbar.Add($"批量删除完成:成功 {success} 个频道", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add($"批量删除完成:成功 {success} 个,失败 {failed} 个", Severity.Warning);
|
||||
}
|
||||
|
||||
await ReloadTable();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"批量删除失败:{ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenCreateCategoryDialog()
|
||||
{
|
||||
if (selectedBotId <= 0)
|
||||
@@ -741,6 +943,39 @@
|
||||
await LoadBotData();
|
||||
}
|
||||
|
||||
private static string GetChannelStatusText(BotChannel channel)
|
||||
{
|
||||
return channel.ChannelStatusOk switch
|
||||
{
|
||||
true => "正常",
|
||||
false => "异常",
|
||||
null => "未检测"
|
||||
};
|
||||
}
|
||||
|
||||
private static Color GetChannelStatusColor(BotChannel channel)
|
||||
{
|
||||
return channel.ChannelStatusOk switch
|
||||
{
|
||||
true => Color.Success,
|
||||
false => Color.Error,
|
||||
null => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetChannelStatusTooltip(BotChannel channel)
|
||||
{
|
||||
if (channel.ChannelStatusOk == null)
|
||||
return "尚未检测";
|
||||
|
||||
var checkedAt = channel.ChannelStatusCheckedAtUtc?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||
if (channel.ChannelStatusOk == true)
|
||||
return $"最近检测:{checkedAt}";
|
||||
|
||||
var err = string.IsNullOrWhiteSpace(channel.ChannelStatusError) ? "未知错误" : channel.ChannelStatusError;
|
||||
return $"最近检测:{checkedAt};失败原因:{err}";
|
||||
}
|
||||
|
||||
private Task SelectAllFiltered()
|
||||
{
|
||||
selected = channels.ToHashSet();
|
||||
|
||||
Reference in new issue
Block a user