feat(bot-channels): 删除频道时可勾选要移除的Bot绑定

This commit is contained in:
meoacgx
2026-03-04 22:45:08 +08:00
parent 62a4519c5e
commit ace820bb66
5 changed files with 277 additions and 26 deletions

View File

@@ -351,15 +351,55 @@ public class BotManagementService
}
/// <summary>
/// 按 TelegramId 全局删除频道:会移除该频道本体及所有 Bot 绑定关系
/// 获取指定频道绑定的 Bot 列表(按名称排序)
/// </summary>
public async Task DeleteChannelGloballyByTelegramIdAsync(long telegramId)
public async Task<IReadOnlyList<Bot>> GetChannelBoundBotsAsync(long telegramId)
{
var ch = await _botChannelRepository.GetGlobalByTelegramIdAsync(telegramId);
if (ch == null)
return;
return Array.Empty<Bot>();
await _botChannelRepository.DeleteAsync(ch);
var members = await _memberRepository.FindAsync(x => x.BotChannelId == ch.Id);
var botIdSet = members
.Select(x => x.BotId)
.Where(x => x > 0)
.Distinct()
.ToHashSet();
if (botIdSet.Count == 0)
return Array.Empty<Bot>();
var bots = (await _botRepository.GetAllAsync())
.Where(x => botIdSet.Contains(x.Id))
.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
return bots;
}
/// <summary>
/// 删除指定频道上选中 Bot 的绑定关系;若无剩余绑定,会自动清理频道本体。
/// 返回实际删除的绑定条数。
/// </summary>
public async Task<int> DeleteChannelBindingsByBotIdsAsync(long telegramId, IReadOnlyCollection<int> botIds)
{
var selected = botIds
.Where(x => x > 0)
.Distinct()
.ToList();
if (selected.Count == 0)
return 0;
var ch = await _botChannelRepository.GetGlobalByTelegramIdAsync(telegramId);
if (ch == null)
return 0;
var removed = await _memberRepository.DeleteByChannelAndBotsAsync(ch.Id, selected);
var remains = await _memberRepository.CountForChannelAsync(ch.Id);
if (remains == 0)
await _botChannelRepository.DeleteAsync(ch);
return removed;
}
public async Task UpdateChannelStatusAsync(int botId, long telegramId, bool ok, string? error, DateTime checkedAtUtc)

View File

@@ -44,9 +44,29 @@ public class BotChannelMemberRepository : Repository<BotChannelMember>, IBotChan
await _context.SaveChangesAsync();
}
public async Task<int> DeleteByChannelAndBotsAsync(int botChannelId, IReadOnlyCollection<int> botIds)
{
if (botChannelId <= 0 || botIds.Count == 0)
return 0;
var targets = botIds.Where(x => x > 0).Distinct().ToList();
if (targets.Count == 0)
return 0;
var rows = await _dbSet
.Where(x => x.BotChannelId == botChannelId && targets.Contains(x.BotId))
.ToListAsync();
if (rows.Count == 0)
return 0;
_dbSet.RemoveRange(rows);
await _context.SaveChangesAsync();
return rows.Count;
}
public async Task<int> CountForChannelAsync(int botChannelId)
{
return await _dbSet.CountAsync(x => x.BotChannelId == botChannelId);
}
}

View File

@@ -7,6 +7,6 @@ public interface IBotChannelMemberRepository : IRepository<BotChannelMember>
Task<BotChannelMember?> GetAsync(int botId, int botChannelId);
Task UpsertAsync(int botId, int botChannelId, DateTime syncedAt);
Task DeleteAsync(int botId, int botChannelId);
Task<int> DeleteByChannelAndBotsAsync(int botChannelId, IReadOnlyCollection<int> botIds);
Task<int> CountForChannelAsync(int botChannelId);
}

View File

@@ -0,0 +1,147 @@
@namespace TelegramPanel.Web.Components.Dialogs
@using TelegramPanel.Data.Entities
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudText Typo="Typo.body2">@Prompt</MudText>
@if (options.Count == 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
当前没有可删除的 Bot 绑定。
</MudAlert>
}
else
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Style="flex-wrap: wrap;">
<MudButton Variant="Variant.Outlined" Size="Size.Small" Color="Color.Primary"
OnClick="SelectCurrentBot"
Disabled="@(!options.Any(x => x.Id == CurrentBotId))">
仅当前 Bot
</MudButton>
<MudButton Variant="Variant.Outlined" Size="Size.Small" Color="Color.Default" OnClick="SelectAll">
全选
</MudButton>
<MudButton Variant="Variant.Outlined" Size="Size.Small" Color="Color.Default" OnClick="ClearAll">
清空
</MudButton>
<MudSpacer />
<MudChip T="string" Size="Size.Small" Color="Color.Info">
已选 @selectedBotIds.Count / @options.Count
</MudChip>
</MudStack>
<MudPaper Class="pa-2" Outlined="true">
<MudStack Spacing="1">
@foreach (var bot in options)
{
<MudCheckBox T="bool"
Label="@BuildBotLabel(bot)"
Value="@IsChecked(bot.Id)"
ValueChanged="@(value => SetChecked(bot.Id, value))" />
}
</MudStack>
</MudPaper>
}
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel">取消</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" OnClick="Confirm"
Disabled="@(options.Count == 0 || selectedBotIds.Count == 0)">
删除选中绑定
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public string Prompt { get; set; } = "";
[Parameter] public List<Bot> Bots { get; set; } = new();
[Parameter] public int CurrentBotId { get; set; }
private List<Bot> options = new();
private HashSet<int> selectedBotIds = new();
protected override void OnParametersSet()
{
options = (Bots ?? new List<Bot>())
.Where(x => x.Id > 0)
.GroupBy(x => x.Id)
.Select(x => x.First())
.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
selectedBotIds = new HashSet<int>();
if (options.Count == 0)
return;
if (CurrentBotId > 0 && options.Any(x => x.Id == CurrentBotId))
{
selectedBotIds.Add(CurrentBotId);
return;
}
foreach (var bot in options)
selectedBotIds.Add(bot.Id);
}
private bool IsChecked(int botId) => selectedBotIds.Contains(botId);
private void SetChecked(int botId, bool selected)
{
if (selected)
selectedBotIds.Add(botId);
else
selectedBotIds.Remove(botId);
}
private void SelectCurrentBot()
{
selectedBotIds.Clear();
if (CurrentBotId > 0)
selectedBotIds.Add(CurrentBotId);
}
private void SelectAll()
{
selectedBotIds.Clear();
foreach (var bot in options)
selectedBotIds.Add(bot.Id);
}
private void ClearAll()
{
selectedBotIds.Clear();
}
private static string BuildBotLabel(Bot bot)
{
var name = string.IsNullOrWhiteSpace(bot.Name) ? $"Bot {bot.Id}" : bot.Name.Trim();
var username = string.IsNullOrWhiteSpace(bot.Username) ? null : "@" + bot.Username.Trim().TrimStart('@');
return string.IsNullOrWhiteSpace(username) ? name : $"{name}{username}";
}
private void Cancel()
{
MudDialog.Cancel();
}
private void Confirm()
{
var result = selectedBotIds
.Where(x => x > 0)
.OrderBy(x => x)
.ToList();
if (result.Count == 0)
{
MudDialog.Cancel();
return;
}
MudDialog.Close(DialogResult.Ok(result));
}
}

View File

@@ -88,7 +88,7 @@
<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>
<MudMenuItem Disabled="@(selected.Count == 0)" OnClick="DeleteSelectedChannelsAsync">删除频道(已选)</MudMenuItem>
</MudMenu>
@if (selected.Count > 0)
@@ -157,7 +157,7 @@
<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="全局删除频道" />
OnClick="@(async () => await DeleteSingleChannelAsync(context))" title="删除频道" />
</MudStack>
</MudTd>
</RowTemplate>
@@ -484,19 +484,18 @@
return;
var channelName = string.IsNullOrWhiteSpace(channel.Title) ? channel.TelegramId.ToString() : channel.Title;
bool? ok = await DialogService.ShowMessageBox(
"确认删除",
$"确定全局删除频道“{channelName}”吗?该频道会从所有 Bot 的频道列表中移除。",
yesText: "删除", cancelText: "取消");
if (ok != true)
var selectedBotIds = await SelectBotsForDeleteAsync(new[] { channel.TelegramId }, channelName);
if (selectedBotIds == null || selectedBotIds.Count == 0)
return;
loading = true;
try
{
await BotManagement.DeleteChannelGloballyByTelegramIdAsync(channel.TelegramId);
Snackbar.Add($"已全局删除频道:{channelName}", Severity.Success);
var removed = await BotManagement.DeleteChannelBindingsByBotIdsAsync(channel.TelegramId, selectedBotIds);
if (removed > 0)
Snackbar.Add($"已删除频道绑定:{channelName}(删除 {removed} 条绑定)", Severity.Success);
else
Snackbar.Add($"未删除任何绑定:{channelName}(所选 Bot 可能未绑定该频道)", Severity.Info);
await ReloadTable();
}
catch (Exception ex)
@@ -520,26 +519,24 @@
return;
}
bool? ok = await DialogService.ShowMessageBox(
"确认批量删除",
$"确定全局删除已选 {selected.Count} 个频道吗?这些频道会从所有 Bot 的频道列表中移除。",
yesText: "删除", cancelText: "取消");
if (ok != true)
var ids = selected.Select(x => x.TelegramId).Distinct().ToList();
var selectedBotIds = await SelectBotsForDeleteAsync(ids, null);
if (selectedBotIds == null || selectedBotIds.Count == 0)
return;
loading = true;
try
{
var ids = selected.Select(x => x.TelegramId).Distinct().ToList();
var success = 0;
var failed = 0;
var removedTotal = 0;
foreach (var chatId in ids)
{
try
{
await BotManagement.DeleteChannelGloballyByTelegramIdAsync(chatId);
var removed = await BotManagement.DeleteChannelBindingsByBotIdsAsync(chatId, selectedBotIds);
removedTotal += removed;
success++;
}
catch
@@ -550,11 +547,11 @@
if (failed == 0)
{
Snackbar.Add($"批量删除完成:成功 {success} 个频道", Severity.Success);
Snackbar.Add($"批量删除完成:处理 {success} 个频道,删除绑定 {removedTotal} 条", Severity.Success);
}
else
{
Snackbar.Add($"批量删除完成:成功 {success} 个,失败 {failed} 个", Severity.Warning);
Snackbar.Add($"批量删除完成:处理成功 {success} 个,失败 {failed} 个,删除绑定 {removedTotal} 条", Severity.Warning);
}
await ReloadTable();
@@ -569,6 +566,53 @@
}
}
private async Task<List<int>?> SelectBotsForDeleteAsync(IReadOnlyCollection<long> channelTelegramIds, string? singleChannelName)
{
var ids = channelTelegramIds.Where(x => x != 0).Distinct().ToList();
if (ids.Count == 0)
return null;
var botMap = new Dictionary<int, Bot>();
foreach (var telegramId in ids)
{
var boundBots = await BotManagement.GetChannelBoundBotsAsync(telegramId);
foreach (var bot in boundBots)
{
if (bot.Id > 0 && !botMap.ContainsKey(bot.Id))
botMap[bot.Id] = bot;
}
}
if (botMap.Count == 0)
{
Snackbar.Add("未找到可删除的 Bot 绑定", Severity.Info);
return null;
}
var prompt = ids.Count == 1
? $"频道“{singleChannelName ?? ids[0].ToString()}”当前绑定了以下 Bot请勾选要删除的绑定"
: $"已选 {ids.Count} 个频道。请勾选要批量删除的 Bot 绑定(未绑定的频道会自动跳过)。";
var parameters = new DialogParameters
{
["Prompt"] = prompt,
["Bots"] = botMap.Values.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase).ToList(),
["CurrentBotId"] = selectedBotId
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
var dialog = DialogService.Show<BotChannelDeleteBindingsDialog>("选择要删除的 Bot 绑定", parameters, options);
var result = await dialog.Result;
if (result.Canceled)
return null;
if (result.Data is List<int> selectedIds)
return selectedIds.Where(x => x > 0).Distinct().ToList();
if (result.Data is IReadOnlyList<int> selectedReadOnlyIds)
return selectedReadOnlyIds.Where(x => x > 0).Distinct().ToList();
return null;
}
private async Task OpenCreateCategoryDialog()
{
if (selectedBotId <= 0)