refactor: Bot频道全局共享(共用分类与频道)

This commit is contained in:
meoacgx committed 2026-01-03 19:11:27 +08:00
1 parent efbe67791d
commit 7d8f93b4c8
23 files changed
+1362 -76

No files matched your search

@@ -10,17 +10,20 @@ public class BotManagementService
private readonly IBotRepository _botRepository;
private readonly IBotChannelRepository _botChannelRepository;
private readonly IBotChannelCategoryRepository _categoryRepository;
private readonly IBotChannelMemberRepository _memberRepository;
private readonly ILogger<BotManagementService> _logger;
public BotManagementService(
IBotRepository botRepository,
IBotChannelRepository botChannelRepository,
IBotChannelCategoryRepository categoryRepository,
IBotChannelMemberRepository memberRepository,
ILogger<BotManagementService> logger)
{
_botRepository = botRepository;
_botChannelRepository = botChannelRepository;
_categoryRepository = categoryRepository;
_memberRepository = memberRepository;
_logger = logger;
}
@@ -96,28 +99,25 @@ public class BotManagementService
}
}
public async Task<IEnumerable<BotChannelCategory>> GetCategoriesAsync(int botId)
public async Task<IEnumerable<BotChannelCategory>> GetCategoriesAsync()
{
return await _categoryRepository.GetForBotAsync(botId);
return await _categoryRepository.GetAllOrderedAsync();
}
public async Task<BotChannelCategory> CreateCategoryAsync(int botId, string name, string? description = null)
public async Task<BotChannelCategory> CreateCategoryAsync(string name, string? description = null)
{
name = (name ?? string.Empty).Trim();
description = string.IsNullOrWhiteSpace(description) ? null : description.Trim();
if (botId <= 0)
throw new ArgumentException("botId 无效", nameof(botId));
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("分类名称不能为空", nameof(name));
var existing = await _categoryRepository.GetByNameAsync(botId, name);
var existing = await _categoryRepository.GetByNameAsync(name);
if (existing != null)
throw new InvalidOperationException("分类名称已存在");
var cat = new BotChannelCategory
{
BotId = botId,
Name = name,
Description = description,
CreatedAt = DateTime.UtcNow
@@ -173,15 +173,22 @@ public class BotManagementService
return await _botChannelRepository.GetByTelegramIdAsync(botId, telegramId);
}
public async Task<BotChannel> UpsertChannelAsync(BotChannel channel)
public async Task<BotChannel> UpsertChannelAsync(int botId, BotChannel channel)
{
if (channel.BotId <= 0)
throw new ArgumentException("BotId 无效", nameof(channel));
if (botId <= 0)
throw new ArgumentException("BotId 无效", nameof(botId));
if (channel.TelegramId == 0)
throw new ArgumentException("TelegramId 无效", nameof(channel));
var existing = await _botChannelRepository.GetByTelegramIdAsync(channel.BotId, channel.TelegramId);
if (existing != null)
var now = DateTime.UtcNow;
var existing = await _botChannelRepository.GetGlobalByTelegramIdAsync(channel.TelegramId);
if (existing == null)
{
channel.SyncedAt = now;
existing = await _botChannelRepository.AddAsync(channel);
}
else
{
existing.Title = channel.Title;
existing.Username = channel.Username;
@@ -191,23 +198,27 @@ public class BotManagementService
existing.AccessHash = channel.AccessHash;
if (channel.CreatedAt.HasValue)
existing.CreatedAt = channel.CreatedAt;
existing.SyncedAt = DateTime.UtcNow;
existing.SyncedAt = now;
await _botChannelRepository.UpdateAsync(existing);
return existing;
}
channel.SyncedAt = DateTime.UtcNow;
return await _botChannelRepository.AddAsync(channel);
await _memberRepository.UpsertAsync(botId, existing.Id, now);
return existing;
}
public async Task DeleteChannelByTelegramIdAsync(int botId, long telegramId)
{
var existing = await _botChannelRepository.GetByTelegramIdAsync(botId, telegramId);
if (existing == null)
var ch = await _botChannelRepository.GetByTelegramIdAsync(botId, telegramId);
if (ch == null)
return;
await _botChannelRepository.DeleteAsync(existing);
await _memberRepository.DeleteAsync(botId, ch.Id);
// 清理无任何 Bot 关联的“孤儿频道”
var remains = await _memberRepository.CountForChannelAsync(ch.Id);
if (remains == 0)
await _botChannelRepository.DeleteAsync(ch);
}
public async Task SetChannelCategoryAsync(int botChannelId, int? categoryId)
@@ -256,9 +256,8 @@ public class BotTelegramService
// left/kicked 或降权为 member 等,直接从列表移除(否则导出邀请会失败且列表不准)。
if (change.Status is "administrator" or "creator")
{
await _botManagement.UpsertChannelAsync(new BotChannel
await _botManagement.UpsertChannelAsync(bot.Id, new BotChannel
{
BotId = bot.Id,
TelegramId = chatId,
Title = string.IsNullOrWhiteSpace(change.Chat.Title) ? $"频道 {chatId}" : change.Chat.Title,
Username = string.IsNullOrWhiteSpace(change.Chat.Username) ? null : change.Chat.Username.Trim().TrimStart('@'),
+22 -12
View File
@@ -23,6 +23,7 @@ public class AppDbContext : DbContext
public DbSet<Bot> Bots => Set<Bot>();
public DbSet<BotChannel> BotChannels => Set<BotChannel>();
public DbSet<BotChannelCategory> BotChannelCategories => Set<BotChannelCategory>();
public DbSet<BotChannelMember> BotChannelMembers => Set<BotChannelMember>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -166,12 +167,7 @@ public class AppDbContext : DbContext
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Description).HasMaxLength(500);
entity.HasIndex(e => new { e.BotId, e.Name }).IsUnique();
entity.HasOne(e => e.Bot)
.WithMany(b => b.Categories)
.HasForeignKey(e => e.BotId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(e => e.Name).IsUnique();
});
// BotChannel配置
@@ -182,19 +178,33 @@ public class AppDbContext : DbContext
entity.Property(e => e.Username).HasMaxLength(100);
entity.Property(e => e.About).HasMaxLength(1000);
entity.HasIndex(e => new { e.BotId, e.TelegramId }).IsUnique();
entity.HasIndex(e => e.TelegramId).IsUnique();
entity.HasIndex(e => e.Username);
entity.HasIndex(e => e.CategoryId);
entity.HasOne(e => e.Bot)
.WithMany(b => b.Channels)
.HasForeignKey(e => e.BotId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Category)
.WithMany(c => c.Channels)
.HasForeignKey(e => e.CategoryId)
.OnDelete(DeleteBehavior.SetNull);
});
// BotChannelMember 配置
modelBuilder.Entity<BotChannelMember>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => new { e.BotId, e.BotChannelId }).IsUnique();
entity.HasIndex(e => e.BotId);
entity.HasIndex(e => e.BotChannelId);
entity.HasOne(e => e.Bot)
.WithMany(b => b.ChannelMembers)
.HasForeignKey(e => e.BotId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.BotChannel)
.WithMany(c => c.Members)
.HasForeignKey(e => e.BotChannelId)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
@@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace TelegramPanel.Data;
/// <summary>
/// 仅用于 dotnet-ef 在设计时创建 DbContext(不影响运行时)。
/// </summary>
public sealed class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args)
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite("Data Source=design-time.db")
.Options;
return new AppDbContext(options);
}
}
+1 -2
View File
@@ -14,6 +14,5 @@ public class Bot
public DateTime? LastSyncAt { get; set; }
public long? LastUpdateId { get; set; }
public ICollection<BotChannelCategory> Categories { get; set; } = new List<BotChannelCategory>();
public ICollection<BotChannel> Channels { get; set; } = new List<BotChannel>();
public ICollection<BotChannelMember> ChannelMembers { get; set; } = new List<BotChannelMember>();
}
@@ -6,7 +6,6 @@ namespace TelegramPanel.Data.Entities;
public class BotChannel
{
public int Id { get; set; }
public int BotId { get; set; }
public long TelegramId { get; set; }
public long? AccessHash { get; set; }
public string Title { get; set; } = null!;
@@ -19,7 +18,6 @@ public class BotChannel
public int? CategoryId { get; set; }
public Bot? Bot { get; set; }
public BotChannelCategory? Category { get; set; }
public ICollection<BotChannelMember> Members { get; set; } = new List<BotChannelMember>();
}
@@ -6,12 +6,9 @@ namespace TelegramPanel.Data.Entities;
public class BotChannelCategory
{
public int Id { get; set; }
public int BotId { get; set; }
public string Name { get; set; } = null!;
public string? Description { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public Bot? Bot { get; set; }
public ICollection<BotChannel> Channels { get; set; } = new List<BotChannel>();
}
@@ -0,0 +1,18 @@
namespace TelegramPanel.Data.Entities;
/// <summary>
/// Bot - 频道/群 成员关系(用于记录“哪个 Bot 在哪个聊天里”)。
/// </summary>
public class BotChannelMember
{
public int Id { get; set; }
public int BotId { get; set; }
public int BotChannelId { get; set; }
public DateTime SyncedAt { get; set; } = DateTime.UtcNow;
public Bot Bot { get; set; } = null!;
public BotChannel BotChannel { get; set; } = null!;
}
@@ -0,0 +1,620 @@
// <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("20260103094343_GlobalizeBotChannels")]
partial class GlobalizeBotChannels
{
/// <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?>("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("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,298 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TelegramPanel.Data.Migrations
{
/// <inheritdoc />
public partial class GlobalizeBotChannels : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// 将“每个 Bot 一套 BotChannels/BotChannelCategories”的旧结构,升级为:
// - BotChannels:全局唯一(TelegramId 唯一)
// - BotChannelCategories:全局唯一(Name 唯一)
// - BotChannelMembers:Bot-频道关系(哪个 Bot 在哪个频道里)
//
// 分类合并策略(用户选择 A):同一 TelegramId 在多 Bot 下若存在不同分类,
// 取“出现次数最多”的分类作为全局分类。
migrationBuilder.Sql("ALTER TABLE BotChannels RENAME TO BotChannels_Old;");
migrationBuilder.Sql("ALTER TABLE BotChannelCategories RENAME TO BotChannelCategories_Old;");
// SQLite:表重命名后索引名不会自动变更;为避免与新表的同名索引冲突,这里先删旧索引(数据复制不依赖它们)。
migrationBuilder.Sql("DROP INDEX IF EXISTS IX_BotChannels_BotId_TelegramId;");
migrationBuilder.Sql("DROP INDEX IF EXISTS IX_BotChannels_Username;");
migrationBuilder.Sql("DROP INDEX IF EXISTS IX_BotChannels_CategoryId;");
migrationBuilder.Sql("DROP INDEX IF EXISTS IX_BotChannelCategories_BotId_Name;");
migrationBuilder.CreateTable(
name: "BotChannelCategories",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BotChannelCategories", x => x.Id);
});
migrationBuilder.CreateTable(
name: "BotChannels",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
TelegramId = table.Column<long>(type: "INTEGER", nullable: false),
AccessHash = table.Column<long>(type: "INTEGER", nullable: true),
Title = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
Username = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
IsBroadcast = table.Column<bool>(type: "INTEGER", nullable: false),
MemberCount = table.Column<int>(type: "INTEGER", nullable: false),
About = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
SyncedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
CategoryId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BotChannels", x => x.Id);
table.ForeignKey(
name: "FK_BotChannels_BotChannelCategories_CategoryId",
column: x => x.CategoryId,
principalTable: "BotChannelCategories",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "BotChannelMembers",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
BotId = table.Column<int>(type: "INTEGER", nullable: false),
BotChannelId = table.Column<int>(type: "INTEGER", nullable: false),
SyncedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BotChannelMembers", x => x.Id);
table.ForeignKey(
name: "FK_BotChannelMembers_BotChannels_BotChannelId",
column: x => x.BotChannelId,
principalTable: "BotChannels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BotChannelMembers_Bots_BotId",
column: x => x.BotId,
principalTable: "Bots",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_BotChannelCategories_Name",
table: "BotChannelCategories",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BotChannels_TelegramId",
table: "BotChannels",
column: "TelegramId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BotChannels_Username",
table: "BotChannels",
column: "Username");
migrationBuilder.CreateIndex(
name: "IX_BotChannels_CategoryId",
table: "BotChannels",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_BotChannelMembers_BotId_BotChannelId",
table: "BotChannelMembers",
columns: new[] { "BotId", "BotChannelId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BotChannelMembers_BotId",
table: "BotChannelMembers",
column: "BotId");
migrationBuilder.CreateIndex(
name: "IX_BotChannelMembers_BotChannelId",
table: "BotChannelMembers",
column: "BotChannelId");
// 1) 迁移分类(去重:Name 全局唯一)
migrationBuilder.Sql(@"
INSERT INTO BotChannelCategories (Name, Description, CreatedAt)
SELECT Name, MAX(Description), MIN(CreatedAt)
FROM BotChannelCategories_Old
GROUP BY Name;
");
// 2) 迁移频道(TelegramId 全局唯一;取 SyncedAt 最新的一条作为全局信息)
migrationBuilder.Sql(@"
INSERT INTO BotChannels (TelegramId, AccessHash, Title, Username, IsBroadcast, MemberCount, About, CreatedAt, SyncedAt, CategoryId)
SELECT bc.TelegramId, bc.AccessHash, bc.Title, bc.Username, bc.IsBroadcast, bc.MemberCount, bc.About, bc.CreatedAt, bc.SyncedAt, NULL
FROM BotChannels_Old bc
JOIN (
SELECT TelegramId, MAX(SyncedAt) AS MaxSyncedAt
FROM BotChannels_Old
GROUP BY TelegramId
) m ON m.TelegramId = bc.TelegramId AND m.MaxSyncedAt = bc.SyncedAt
JOIN (
SELECT TelegramId, SyncedAt, MAX(Id) AS MaxId
FROM BotChannels_Old
GROUP BY TelegramId, SyncedAt
) m2 ON m2.TelegramId = bc.TelegramId AND m2.SyncedAt = bc.SyncedAt AND m2.MaxId = bc.Id;
");
// 3) 迁移 Bot-频道关系(同一 bot + 同一频道去重,SyncedAt 取最大)
migrationBuilder.Sql(@"
INSERT INTO BotChannelMembers (BotId, BotChannelId, SyncedAt)
SELECT old.BotId, now.Id AS BotChannelId, MAX(old.SyncedAt) AS SyncedAt
FROM BotChannels_Old old
JOIN BotChannels now ON now.TelegramId = old.TelegramId
GROUP BY old.BotId, now.Id;
");
// 4) 合并分类:同一 TelegramId 在多 bot 下若存在不同分类,取“出现次数最多”的分类(方案 A)
migrationBuilder.Sql(@"
WITH CatNames AS (
SELECT bc.TelegramId AS TelegramId, c.Name AS CatName
FROM BotChannels_Old bc
JOIN BotChannelCategories_Old c ON c.Id = bc.CategoryId
WHERE bc.CategoryId IS NOT NULL
),
Counts AS (
SELECT TelegramId, CatName, COUNT(*) AS Cnt
FROM CatNames
GROUP BY TelegramId, CatName
),
Pick AS (
SELECT TelegramId, CatName
FROM (
SELECT TelegramId, CatName, Cnt,
ROW_NUMBER() OVER (PARTITION BY TelegramId ORDER BY Cnt DESC, CatName ASC) AS rn
FROM Counts
)
WHERE rn = 1
)
UPDATE BotChannels
SET CategoryId = (
SELECT catNew.Id
FROM Pick p
JOIN BotChannelCategories catNew ON catNew.Name = p.CatName
WHERE p.TelegramId = BotChannels.TelegramId
)
WHERE TelegramId IN (SELECT TelegramId FROM Pick);
");
// 5) 清理旧表
migrationBuilder.Sql("DROP TABLE BotChannels_Old;");
migrationBuilder.Sql("DROP TABLE BotChannelCategories_Old;");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// 降级不做数据回填(不可逆):仅恢复旧结构的空表,避免 Down 失败阻塞回滚链路。
migrationBuilder.DropTable(name: "BotChannelMembers");
migrationBuilder.DropTable(name: "BotChannels");
migrationBuilder.DropTable(name: "BotChannelCategories");
migrationBuilder.CreateTable(
name: "BotChannelCategories",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
BotId = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BotChannelCategories", x => x.Id);
table.ForeignKey(
name: "FK_BotChannelCategories_Bots_BotId",
column: x => x.BotId,
principalTable: "Bots",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BotChannels",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
BotId = table.Column<int>(type: "INTEGER", nullable: false),
TelegramId = table.Column<long>(type: "INTEGER", nullable: false),
AccessHash = table.Column<long>(type: "INTEGER", nullable: true),
Title = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
Username = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
IsBroadcast = table.Column<bool>(type: "INTEGER", nullable: false),
MemberCount = table.Column<int>(type: "INTEGER", nullable: false),
About = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
SyncedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
CategoryId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BotChannels", x => x.Id);
table.ForeignKey(
name: "FK_BotChannels_Bots_BotId",
column: x => x.BotId,
principalTable: "Bots",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BotChannels_BotChannelCategories_CategoryId",
column: x => x.CategoryId,
principalTable: "BotChannelCategories",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateIndex(
name: "IX_BotChannelCategories_BotId_Name",
table: "BotChannelCategories",
columns: new[] { "BotId", "Name" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BotChannels_BotId_TelegramId",
table: "BotChannels",
columns: new[] { "BotId", "TelegramId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BotChannels_CategoryId",
table: "BotChannels",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_BotChannels_Username",
table: "BotChannels",
column: "Username");
}
}
}
@@ -1,4 +1,4 @@
// <auto-generated />
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace TelegramPanel.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.HasAnnotation("ProductVersion", "8.0.22");
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
{
@@ -64,16 +64,17 @@ namespace TelegramPanel.Data.Migrations
.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")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<long>("UserId")
@@ -113,6 +114,7 @@ namespace TelegramPanel.Data.Migrations
.HasColumnType("TEXT");
b.Property<bool>("ExcludeFromOperations")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false);
@@ -129,6 +131,37 @@ namespace TelegramPanel.Data.Migrations
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")
@@ -175,6 +208,153 @@ namespace TelegramPanel.Data.Migrations
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?>("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("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")
@@ -191,7 +371,7 @@ namespace TelegramPanel.Data.Migrations
b.Property<DateTime?>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int>("CreatorAccountId")
b.Property<int?>("CreatorAccountId")
.HasColumnType("INTEGER");
b.Property<int?>("GroupId")
@@ -317,13 +497,60 @@ namespace TelegramPanel.Data.Migrations
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.Cascade)
.IsRequired();
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("TelegramPanel.Data.Entities.ChannelGroup", "Group")
.WithMany("Channels")
@@ -348,6 +575,8 @@ namespace TelegramPanel.Data.Migrations
modelBuilder.Entity("TelegramPanel.Data.Entities.Account", b =>
{
b.Navigation("AccountChannels");
b.Navigation("Channels");
b.Navigation("Groups");
@@ -358,6 +587,26 @@ namespace TelegramPanel.Data.Migrations
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");
@@ -9,21 +9,19 @@ public class BotChannelCategoryRepository : Repository<BotChannelCategory>, IBot
{
}
public async Task<IEnumerable<BotChannelCategory>> GetForBotAsync(int botId)
public async Task<IEnumerable<BotChannelCategory>> GetAllOrderedAsync()
{
return await _dbSet
.Where(x => x.BotId == botId)
.OrderBy(x => x.Name)
.ToListAsync();
}
public async Task<BotChannelCategory?> GetByNameAsync(int botId, string name)
public async Task<BotChannelCategory?> GetByNameAsync(string name)
{
name = (name ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(name))
return null;
return await _dbSet.FirstOrDefaultAsync(x => x.BotId == botId && x.Name == name);
return await _dbSet.FirstOrDefaultAsync(x => x.Name == name);
}
}
@@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore;
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Data.Repositories;
public class BotChannelMemberRepository : Repository<BotChannelMember>, IBotChannelMemberRepository
{
public BotChannelMemberRepository(AppDbContext context) : base(context)
{
}
public async Task<BotChannelMember?> GetAsync(int botId, int botChannelId)
{
return await _dbSet.FirstOrDefaultAsync(x => x.BotId == botId && x.BotChannelId == botChannelId);
}
public async Task UpsertAsync(int botId, int botChannelId, DateTime syncedAt)
{
var existing = await GetAsync(botId, botChannelId);
if (existing != null)
{
existing.SyncedAt = syncedAt;
_dbSet.Update(existing);
await _context.SaveChangesAsync();
return;
}
await _dbSet.AddAsync(new BotChannelMember
{
BotId = botId,
BotChannelId = botChannelId,
SyncedAt = syncedAt
});
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int botId, int botChannelId)
{
var existing = await GetAsync(botId, botChannelId);
if (existing == null)
return;
_dbSet.Remove(existing);
await _context.SaveChangesAsync();
}
public async Task<int> CountForChannelAsync(int botChannelId)
{
return await _dbSet.CountAsync(x => x.BotChannelId == botChannelId);
}
}
@@ -14,7 +14,7 @@ public class BotChannelRepository : Repository<BotChannel>, IBotChannelRepositor
var query = _dbSet
.AsNoTracking()
.Include(x => x.Category)
.Where(x => x.BotId == botId);
.Where(x => x.Members.Any(m => m.BotId == botId));
// 约定:categoryId = -1 表示“未分类”(CategoryId == null)
if (categoryId.HasValue)
@@ -44,23 +44,36 @@ public class BotChannelRepository : Repository<BotChannel>, IBotChannelRepositor
{
return await _dbSet
.Include(x => x.Category)
.FirstOrDefaultAsync(x => x.BotId == botId && x.TelegramId == telegramId);
.FirstOrDefaultAsync(x => x.TelegramId == telegramId && x.Members.Any(m => m.BotId == botId));
}
public async Task<IEnumerable<BotChannel>> GetForBotAsync(int botId, int? categoryId = null)
{
var query = _dbSet
.Include(x => x.Category)
.Where(x => x.BotId == botId);
.Where(x => x.Members.Any(m => m.BotId == botId));
// 约定:categoryId = -1 表示“未分类”(CategoryId == null)
if (categoryId.HasValue)
query = query.Where(x => x.CategoryId == categoryId.Value);
{
if (categoryId.Value == -1)
query = query.Where(x => x.CategoryId == null);
else
query = query.Where(x => x.CategoryId == categoryId.Value);
}
return await query
.OrderByDescending(x => x.SyncedAt)
.ToListAsync();
}
public async Task<BotChannel?> GetGlobalByTelegramIdAsync(long telegramId)
{
return await _dbSet
.Include(x => x.Category)
.FirstOrDefaultAsync(x => x.TelegramId == telegramId);
}
public async Task<(IReadOnlyList<BotChannel> Items, int TotalCount)> QueryPagedAsync(
int botId,
int? categoryId,
@@ -31,10 +31,8 @@ public class BotRepository : Repository<Bot>, IBotRepository
{
// 只用于列表显示统计信息,避免一次性加载全部频道详情
return await _dbSet
.Include(x => x.Channels)
.Include(x => x.Categories)
.Include(x => x.ChannelMembers)
.OrderByDescending(x => x.CreatedAt)
.ToListAsync();
}
}
@@ -4,7 +4,6 @@ namespace TelegramPanel.Data.Repositories;
public interface IBotChannelCategoryRepository : IRepository<BotChannelCategory>
{
Task<IEnumerable<BotChannelCategory>> GetForBotAsync(int botId);
Task<BotChannelCategory?> GetByNameAsync(int botId, string name);
Task<IEnumerable<BotChannelCategory>> GetAllOrderedAsync();
Task<BotChannelCategory?> GetByNameAsync(string name);
}
@@ -0,0 +1,12 @@
using TelegramPanel.Data.Entities;
namespace TelegramPanel.Data.Repositories;
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> CountForChannelAsync(int botChannelId);
}
@@ -6,6 +6,7 @@ public interface IBotChannelRepository : IRepository<BotChannel>
{
Task<BotChannel?> GetByTelegramIdAsync(int botId, long telegramId);
Task<IEnumerable<BotChannel>> GetForBotAsync(int botId, int? categoryId = null);
Task<BotChannel?> GetGlobalByTelegramIdAsync(long telegramId);
Task<(IReadOnlyList<BotChannel> Items, int TotalCount)> QueryPagedAsync(
int botId,
@@ -30,6 +30,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<IBotRepository, BotRepository>();
services.AddScoped<IBotChannelRepository, BotChannelRepository>();
services.AddScoped<IBotChannelCategoryRepository, BotChannelCategoryRepository>();
services.AddScoped<IBotChannelMemberRepository, BotChannelMemberRepository>();
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
return services;
@@ -31,7 +31,6 @@
@code
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public int BotId { get; set; }
[Parameter] public BotChannelCategory? Category { get; set; }
private bool saving;
@@ -71,7 +70,7 @@
if (Category == null)
{
// 新增模式:使用现有的 CreateCategoryAsync 方法
var category = await BotManagement.CreateCategoryAsync(BotId, name, description);
var category = await BotManagement.CreateCategoryAsync(name, description);
Snackbar.Add("分类创建成功", Severity.Success);
MudDialog.Close(DialogResult.Ok(category));
}
@@ -124,9 +124,8 @@
var username = chatInfo?.Username ?? dbChannel?.Username;
var memberCount = chatInfo?.MemberCount ?? dbChannel?.MemberCount ?? 0;
await BotManagement.UpsertChannelAsync(new BotChannel
await BotManagement.UpsertChannelAsync(BotId, new BotChannel
{
BotId = BotId,
TelegramId = ChannelTelegramId,
Title = title,
Username = username,
@@ -252,7 +252,7 @@
return;
}
categories = (await BotManagement.GetCategoriesAsync(selectedBotId)).ToList();
categories = (await BotManagement.GetCategoriesAsync()).ToList();
await ReloadTable();
}
catch (Exception ex)
@@ -364,10 +364,7 @@
if (selectedBotId <= 0)
return;
var parameters = new DialogParameters
{
["BotId"] = selectedBotId
};
var parameters = new DialogParameters();
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
var dialog = DialogService.Show<BotChannelCategoryDialog>("新建分类", parameters, options);
var result = await dialog.Result;
@@ -40,8 +40,7 @@
<HeaderContent>
<MudTh>名称</MudTh>
<MudTh>用户名</MudTh>
<MudTh>频道数</MudTh>
<MudTh>分类数</MudTh>
<MudTh>可管理聊天数</MudTh>
<MudTh>最后同步</MudTh>
<MudTh>状态</MudTh>
<MudTh>操作</MudTh>
@@ -49,8 +48,7 @@
<RowTemplate>
<MudTd DataLabel="名称">@context.Name</MudTd>
<MudTd DataLabel="用户名">@(!string.IsNullOrWhiteSpace(context.Username) ? $"@{context.Username}" : "-")</MudTd>
<MudTd DataLabel="频道数">@context.Channels.Count(x => x.IsBroadcast)</MudTd>
<MudTd DataLabel="分类数">@context.Categories.Count</MudTd>
<MudTd DataLabel="可管理聊天数">@context.ChannelMembers.Count</MudTd>
<MudTd DataLabel="最后同步"><PanelTime Value="context.LastSyncAt" /></MudTd>
<MudTd DataLabel="状态">
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
@@ -151,7 +149,7 @@
{
bool? result = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除机器人 {bot.Name} 吗?该机器人加入的频道与分类也会一并删除。",
$"确定要删除机器人 {bot.Name} 吗?该机器人加入的频道关联也会一并删除。",
yesText: "删除", cancelText: "取消");
if (result != true)