Files
Telegram-Panel/src/TelegramPanel.Core/Services/BotManagementService.cs
T

380 lines
15 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using TelegramPanel.Data.Entities;
using TelegramPanel.Data.Repositories;
namespace TelegramPanel.Core.Services;
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;
}
public async Task<IEnumerable<Bot>> GetAllBotsAsync()
{
return await _botRepository.GetAllWithStatsAsync();
}
public async Task<Bot?> GetBotAsync(int id)
{
return await _botRepository.GetByIdAsync(id);
}
public async Task<Bot> CreateBotAsync(string name, string token, string? username = null)
{
name = (name ?? string.Empty).Trim();
token = (token ?? string.Empty).Trim();
username = string.IsNullOrWhiteSpace(username) ? null : username.Trim().TrimStart('@');
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("机器人名称不能为空", nameof(name));
if (string.IsNullOrWhiteSpace(token))
throw new ArgumentException("机器人 Token 不能为空", nameof(token));
var existing = await _botRepository.GetByNameAsync(name);
if (existing != null)
throw new InvalidOperationException("机器人名称已存在");
var bot = new Bot
{
Name = name,
Token = token,
Username = username,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
return await _botRepository.AddAsync(bot);
}
public async Task UpdateBotAsync(Bot bot)
{
bot.Name = (bot.Name ?? string.Empty).Trim();
bot.Token = (bot.Token ?? string.Empty).Trim();
bot.Username = string.IsNullOrWhiteSpace(bot.Username) ? null : bot.Username.Trim().TrimStart('@');
await _botRepository.UpdateAsync(bot);
}
public async Task UpdateBotProfileAsync(int botId, string name, string? username, string? newToken)
{
name = (name ?? string.Empty).Trim();
username = string.IsNullOrWhiteSpace(username) ? null : username.Trim().TrimStart('@');
newToken = string.IsNullOrWhiteSpace(newToken) ? null : newToken.Trim();
if (botId <= 0)
throw new ArgumentException("BotId 无效", nameof(botId));
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("机器人名称不能为空", nameof(name));
var bot = await _botRepository.GetByIdAsync(botId)
?? throw new InvalidOperationException($"机器人不存在:{botId}");
if (!string.Equals(bot.Name, name, StringComparison.Ordinal))
{
var existing = await _botRepository.GetByNameAsync(name);
if (existing != null && existing.Id != botId)
throw new InvalidOperationException("机器人名称已存在");
}
bot.Name = name;
bot.Username = username;
if (newToken != null)
bot.Token = newToken;
try
{
await _botRepository.UpdateAsync(bot);
}
catch (DbUpdateException ex)
{
var msg = GetInnermostMessage(ex);
if (msg.Contains("UNIQUE constraint failed: Bots.Name", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("机器人名称已存在");
if (msg.Contains("database is locked", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("database is busy", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("SQLite Error 5", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("数据库正被占用(SQLite database is locked),请稍后重试;若云端部署了多个实例共享同一个 sqlite 文件,请改为单实例或换用 MySQL/PostgreSQL。");
if (msg.Contains("no such table: Bots", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("no such column", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException($"数据库结构过旧或未完成迁移,请重启主程序以自动迁移数据库。详情:{msg}");
throw new InvalidOperationException($"更新机器人失败:{msg}", ex);
}
}
public async Task SetBotActiveStatusAsync(int botId, bool isActive)
{
var bot = await _botRepository.GetByIdAsync(botId);
if (bot == null)
return;
bot.IsActive = isActive;
await _botRepository.UpdateAsync(bot);
}
public async Task DeleteBotAsync(int id)
{
var bot = await _botRepository.GetByIdAsync(id);
if (bot == null)
return;
try
{
await _botRepository.DeleteAsync(bot);
}
catch (DbUpdateConcurrencyException)
{
// 视为已被删除(幂等)
}
}
public async Task<IEnumerable<BotChannelCategory>> GetCategoriesAsync()
{
return await _categoryRepository.GetAllOrderedAsync();
}
public async Task<BotChannelCategory> CreateCategoryAsync(string name, string? description = null)
{
name = (name ?? string.Empty).Trim();
description = string.IsNullOrWhiteSpace(description) ? null : description.Trim();
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("分类名称不能为空", nameof(name));
var existing = await _categoryRepository.GetByNameAsync(name);
if (existing != null)
throw new InvalidOperationException("分类名称已存在");
var cat = new BotChannelCategory
{
Name = name,
Description = description,
CreatedAt = DateTime.UtcNow
};
try
{
return await _categoryRepository.AddAsync(cat);
}
catch (DbUpdateException ex)
{
var msg = GetInnermostMessage(ex);
if (msg.Contains("UNIQUE constraint failed: BotChannelCategories.Name", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("分类名称已存在");
if (msg.Contains("database is locked", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("database is busy", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("SQLite Error 5", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("数据库正被占用(SQLite database is locked),请稍后重试;若云端部署了多个实例共享同一个 sqlite 文件,请改为单实例或换用 MySQL/PostgreSQL。");
if (msg.Contains("no such table: BotChannelCategories", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("no such column", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("NOT NULL constraint failed: BotChannelCategories.BotId", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException($"数据库结构过旧或未完成迁移,请重启主程序以自动迁移数据库。详情:{msg}");
throw new InvalidOperationException($"创建分类失败:{msg}", ex);
}
}
public async Task<BotChannelCategory> UpdateCategoryAsync(int categoryId, string name, string? description = null)
{
if (categoryId <= 0)
throw new ArgumentException("分类ID无效", nameof(categoryId));
name = (name ?? string.Empty).Trim();
description = string.IsNullOrWhiteSpace(description) ? null : description.Trim();
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("分类名称不能为空", nameof(name));
var cat = await _categoryRepository.GetByIdAsync(categoryId);
if (cat == null)
throw new InvalidOperationException("分类不存在或已被删除");
if (!string.Equals(cat.Name, name, StringComparison.Ordinal))
{
var existing = await _categoryRepository.GetByNameAsync(name);
if (existing != null && existing.Id != cat.Id)
throw new InvalidOperationException("分类名称已存在");
}
cat.Name = name;
cat.Description = description;
try
{
await _categoryRepository.UpdateAsync(cat);
return cat;
}
catch (DbUpdateException ex)
{
var msg = GetInnermostMessage(ex);
if (msg.Contains("UNIQUE constraint failed: BotChannelCategories.Name", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("分类名称已存在");
if (msg.Contains("database is locked", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("database is busy", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("SQLite Error 5", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("数据库正被占用(SQLite database is locked),请稍后重试;若云端部署了多个实例共享同一个 sqlite 文件,请改为单实例或换用 MySQL/PostgreSQL。");
if (msg.Contains("no such table: BotChannelCategories", StringComparison.OrdinalIgnoreCase)
|| msg.Contains("no such column", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException($"数据库结构过旧或未完成迁移,请重启主程序以自动迁移数据库。详情:{msg}");
throw new InvalidOperationException($"更新分类失败:{msg}", ex);
}
}
private static string GetInnermostMessage(Exception ex)
{
var cur = ex;
while (cur.InnerException != null)
cur = cur.InnerException;
return string.IsNullOrWhiteSpace(cur.Message) ? ex.Message : cur.Message;
}
public async Task DeleteCategoryAsync(int categoryId)
{
var cat = await _categoryRepository.GetByIdAsync(categoryId);
if (cat == null)
return;
await _categoryRepository.DeleteAsync(cat);
}
public async Task<IEnumerable<BotChannel>> GetChannelsAsync(int botId, int? categoryId = null)
{
// Bot 频道列表仅展示“频道”(不展示群组/超级群组)
var list = await _botChannelRepository.GetForBotAsync(botId, categoryId);
return list.Where(x => x.IsBroadcast);
}
public async Task<(IReadOnlyList<BotChannel> Items, int TotalCount)> QueryChannelsPagedAsync(
int botId,
int? categoryId,
string? search,
int statusFilter,
int pageIndex,
int pageSize,
CancellationToken cancellationToken = default)
{
return await _botChannelRepository.QueryPagedAsync(
botId: botId,
categoryId: categoryId,
broadcastOnly: true,
search: search,
statusFilter: statusFilter,
pageIndex: pageIndex,
pageSize: pageSize,
cancellationToken: cancellationToken);
}
/// <summary>
/// 获取 Bot 可管理的聊天列表(包含频道/群组/超级群组)。
/// </summary>
public async Task<IEnumerable<BotChannel>> GetChatsAsync(int botId, int? categoryId = null)
{
return await _botChannelRepository.GetForBotAsync(botId, categoryId);
}
public async Task<BotChannel?> GetChannelByTelegramIdAsync(int botId, long telegramId)
{
return await _botChannelRepository.GetByTelegramIdAsync(botId, telegramId);
}
public async Task<BotChannel> UpsertChannelAsync(int botId, BotChannel channel)
{
if (botId <= 0)
throw new ArgumentException("BotId 无效", nameof(botId));
if (channel.TelegramId == 0)
throw new ArgumentException("TelegramId 无效", nameof(channel));
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;
existing.IsBroadcast = channel.IsBroadcast;
existing.MemberCount = channel.MemberCount;
existing.About = channel.About;
existing.AccessHash = channel.AccessHash;
if (channel.CreatedAt.HasValue)
existing.CreatedAt = channel.CreatedAt;
existing.SyncedAt = now;
await _botChannelRepository.UpdateAsync(existing);
}
await _memberRepository.UpsertAsync(botId, existing.Id, now);
return existing;
}
public async Task DeleteChannelByTelegramIdAsync(int botId, long telegramId)
{
var ch = await _botChannelRepository.GetByTelegramIdAsync(botId, telegramId);
if (ch == null)
return;
await _memberRepository.DeleteAsync(botId, ch.Id);
// 清理无任何 Bot 关联的“孤儿频道”
var remains = await _memberRepository.CountForChannelAsync(ch.Id);
if (remains == 0)
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);
if (ch == null)
return;
ch.CategoryId = categoryId;
await _botChannelRepository.UpdateAsync(ch);
}
}