V 1.1.2 新增了生图包 以及各种转发和接口

This commit is contained in:
lq1405 2025-06-14 22:12:37 +08:00
parent c07369c297
commit 3514cf53f8
42 changed files with 3971 additions and 74 deletions

View File

@ -13,14 +13,40 @@
} }
/// <summary> /// <summary>
/// 将UTC时间转换为北京时间 /// 智能转换时间为北京时间
/// 如果是UTC时间则转换否则直接返回
/// </summary> /// </summary>
/// <param name="utcTime"></param> /// <param name="dateTime">输入的时间</param>
/// <returns></returns> /// <returns>北京时间</returns>
public static DateTime TransferUtcToBeijingTime(DateTime utcTime) public static DateTime TransferUtcToBeijingTime(DateTime dateTime)
{ {
return TimeZoneInfo.ConvertTimeFromUtc(utcTime, // 只有UTC时间才需要转换
TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")); if (dateTime.Kind == DateTimeKind.Utc)
{
try
{
// 优先使用系统时区信息
return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"));
}
catch (TimeZoneNotFoundException)
{
try
{
// Linux系统可能使用这个ID
return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai"));
}
catch (TimeZoneNotFoundException)
{
// 找不到时区就手动加8小时
return dateTime.AddHours(8);
}
}
}
// 非UTC时间直接返回
return dateTime;
} }
} }
} }

View File

@ -8,6 +8,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MailKit" Version="4.11.0" /> <PackageReference Include="MailKit" Version="4.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,6 +1,7 @@
 
using LMS.Repository.DB; using LMS.Repository.DB;
using LMS.Repository.MJPackage;
using LMS.Repository.Models.DB; using LMS.Repository.Models.DB;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -46,6 +47,12 @@ namespace LMS.DAO
public DbSet<DataInfo> DataInfo { get; set; } public DbSet<DataInfo> DataInfo { get; set; }
public DbSet<MJApiTokens> MJApiTokens { get; set; }
public DbSet<MJApiTokenUsage> MJApiTokenUsage { get; set; }
public DbSet<MJApiTasks> MJApiTasks { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
@ -81,6 +88,30 @@ namespace LMS.DAO
) )
.HasColumnType("json"); // 指定MySQL字段类型为JSON .HasColumnType("json"); // 指定MySQL字段类型为JSON
}); });
modelBuilder.Entity<MJApiTokens>(entity =>
{
entity.ToTable("MJApiTokens");
entity.Property(e => e.Token).IsRequired().HasMaxLength(64);
entity.Property(e => e.DailyLimit).HasDefaultValue(0);
entity.Property(e => e.TotalLimit).HasDefaultValue(0);
entity.Property(e => e.ConcurrencyLimit).HasDefaultValue(1);
entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP");
entity.HasIndex(e => e.Token).IsUnique();
entity.HasIndex(e => e.ExpiresAt);
});
modelBuilder.Entity<MJApiTokenUsage>(entity =>
{
entity.ToTable("MJApiTokenUsage");
entity.HasKey(e => new { e.TokenId, e.Date });
entity.HasOne<MJApiTokens>()
.WithMany()
.HasForeignKey(e => e.TokenId)
.OnDelete(DeleteBehavior.Cascade);
});
} }
} }
} }

View File

@ -0,0 +1,41 @@
using LMS.Repository.MJPackage;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.DB
{
public class MJApiTasks
{
[Key]
public string TaskId { get; set; }
public string Token { get; set; }
public long TokenId { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
public string Status { get; set; }
public string ThirdPartyTaskId { get; set; } // 第三方任务ID
public string? Properties { get; set; }
}
public class MJApiTaskCollection : MJApiTasks
{
public long TokenId { get; set; }
public string Token { get; set; }
}
public class MJTaskStatus
{
public const string NOT_START = "NOT_START";
public const string SUBMITTED = "SUBMITTED";
public const string IN_PROGRESS = "IN_PROGRESS";
public const string FAILURE = "FAILURE";
public const string SUCCESS = "SUCCESS";
public const string MODAL = "MODAL";
public const string CANCEL = "CANCEL";
}
}

View File

@ -0,0 +1,22 @@
using LMS.Common.Extensions;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.DB
{
public class MJApiTokenUsage
{
[Key]
public long TokenId { get; set; }
[Key]
public DateTime Date { get; set; }
public int DailyUsage { get; set; } = 0;
public int TotalUsage { get; set; } = 0;
public DateTime LastActivityAt { get; set; } = BeijingTimeExtension.GetBeijingTime();
public string? HistoryUse { get; set; } = null;
}
}

View File

@ -0,0 +1,29 @@
using LMS.Common.Extensions;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace LMS.Repository.DB
{
public class MJApiTokens
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
[StringLength(64)]
public required string Token { get; set; }
[Required]
public required string UseToken { get; set; } // 实际使用的Token
public int DailyLimit { get; set; } = 0;
public int TotalLimit { get; set; } = 0;
public int ConcurrencyLimit { get; set; } = 1;
public DateTime CreatedAt { get; set; } = BeijingTimeExtension.GetBeijingTime();
public DateTime? ExpiresAt { get; set; }
}
}

View File

@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.MJPackage
{
public class AddOrModifyTokenModel
{
/// <summary>
/// token
/// </summary>
[Required]
[StringLength(64)]
public required string Token { get; set; }
/// <summary>
/// 实际使用的Token
/// </summary>
public required string UseToken { get; set; }
/// <summary>
/// 日限制
/// </summary>
[Required]
public required int DailyLimit { get; set; }
/// <summary>
/// 总限制
/// </summary>
[Required]
public required int TotalLimit { get; set; }
/// <summary>
/// 并发限制
/// </summary>
[Required]
public required int ConcurrencyLimit { get; set; }
/// <summary>
/// 使用天数
/// </summary>
[Required]
public required int UseDayCount { get; set; }
}
}

View File

@ -0,0 +1,74 @@
namespace LMS.Repository.MJPackage
{
public class MJSubmitImageModel
{
/// <summary>
/// bot 类型mj(默认)或niji
/// MID_JOURNEY | 枚举值: NIJI_JOURNEY
/// </summary>
public string? BotType { get; set; }
/// <summary>
/// 提示词。
/// </summary>
public string Prompt { get; set; }
/// <summary>
/// 垫图base64数组。
/// </summary>
public List<string>? Base64Array { get; set; }
/// <summary>
/// 账号过滤
/// </summary>
public AccountFilter? AccountFilter { get; set; }
/// <summary>
/// 自定义参数。
/// </summary>
public string? State { get; set; }
/// <summary>
/// 回调地址, 为空时使用全局notifyHook。
/// </summary>
public string? NotifyHook { get; set; }
}
public class AccountFilter
{
/// <summary>
/// 过滤指定实例的账号
/// </summary>
public string? InstanceId { get; set; }
/// <summary>
/// 账号模式 RELAX | FAST | TURBO
/// </summary>
public List<GenerationSpeedMode>? Modes { get; set; } = new List<GenerationSpeedMode>();
/// <summary>
/// 账号是否 remixMidjourney Remix
/// </summary>
public bool? Remix { get; set; }
/// <summary>
/// 账号是否 remixNijiourney Remix
/// </summary>
public bool? NijiRemix { get; set; }
/// <summary>
/// 账号过滤时remix 自动提交视为账号的 remix 为 false
/// </summary>
public bool? RemixAutoConsidered { get; set; }
}
/// <summary>
/// 生成速度模式枚举.
/// </summary>
public enum GenerationSpeedMode
{
RELAX,
FAST,
TURBO
}
}

View File

@ -0,0 +1,39 @@
using LMS.Repository.DB;
using System.Text.Json.Serialization;
namespace LMS.Repository.MJPackage
{
public class MJTaskCallbackModel
{
public string Id { get; set; }
public string? Action { get; set; }
public MJTaskStatus? Status { get; set; }
public string? Prompt { get; set; }
public string? PromptEn { get; set; }
public string? Description { get; set; }
public long? SubmitTime { get; set; }
public long? StartTime { get; set; }
public long? FinishTime { get; set; }
public string? Progress { get; set; }
public string? ImageUrl { get; set; }
public string? FailReason { get; set; }
public ResponseProperties? Properties { get; set; }
}
public class ResponseProperties
{
public string? FinalPrompt { get; set; }
}
}

View File

@ -0,0 +1,18 @@
namespace LMS.Repository.MJPackage
{
// Models/SyncResult.cs
public class SyncResult
{
public int TotalTokenCount { get; set; }
public int ActiveTokenCount { get; set; }
public int RecordsUpdated { get; set; }
}
public class TaskStatistics
{
public int TotalTasks { get; set; } = 0;
public int CompletedTasks { get; set; } = 0;
public int FailedTasks { get; set; } = 0;
public int InProgressTasks { get; set; } = 0;
}
}

View File

@ -0,0 +1,29 @@
using LMS.Common.Extensions;
using LMS.Repository.DB;
namespace LMS.Repository.MJPackage
{
// Models/TokenCacheItem.cs
public class TokenCacheItem
{
public long Id { get; set; }
public string Token { get; set; }
public string UseToken { get; set; } // 实际请求使用的Token
public int DailyLimit { get; set; }
public int TotalLimit { get; set; }
public int ConcurrencyLimit { get; set; } // 新增:并发限制
public DateTime CreatedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public int DailyUsage { get; set; }
public int TotalUsage { get; set; }
public DateTime LastActivityTime { get; set; } = BeijingTimeExtension.GetBeijingTime();
public string? HistoryUse { get; set; } // 历史使用记录
public int CurrentlyExecuting { get; set; } = 0;
}
public class TokenAndTaskCollection : TokenCacheItem
{
public List<MJApiTasks> TaskCollections { get; set; } = [];
}
}

View File

@ -0,0 +1,11 @@
namespace LMS.Repository.MJPackage
{
public class TokenCacheStats
{
public int TotalTokens { get; set; }
public int ActiveTokens { get; set; }
public int InactiveTokens { get; set; }
public int TotalDailyUsage { get; set; }
public int TotalUsage { get; set; }
}
}

View File

@ -0,0 +1,20 @@
namespace LMS.Repository.MJPackage
{
// 查询结果映射类
public class TokenQueryResult
{
public long Id { get; set; }
public string Token { get; set; }
public string UseToken { get; set; } // 实际请求使用的Token
public int DailyLimit { get; set; }
public int TotalLimit { get; set; }
public int ConcurrencyLimit { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public int DailyUsage { get; set; }
public int TotalUsage { get; set; }
public DateTime LastActivityTime { get; set; }
public string? HistoryUse { get; set; } // 历史使用记录
}
}

View File

@ -0,0 +1,12 @@
namespace LMS.Repository.MJPackage
{
public class TokenUsageData
{
public long TokenId { get; set; }
public DateTime Date { get; set; }
public int DailyUsage { get; set; }
public int TotalUsage { get; set; }
public DateTime LastActivityTime { get; set; }
public string? HistoryUse { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using LMS.Repository.DB;
namespace LMS.Tools.MJPackage
{
// Services/ITaskConcurrencyManager.cs
public interface ITaskConcurrencyManager
{
Task CreateTaskAsync(string token, string thirdPartyTaskId);
Task UpdateTaskInDatabase(MJApiTasks mJApiTasks);
Task<MJApiTasks> GetTaskInfoAsync(string taskId);
Task<MJApiTasks> GetTaskInfoByThirdPartyIdAsync(string taskId);
Task<IEnumerable<MJApiTasks>> GetRunningTasksAsync(string token = null);
Task<(int maxConcurrency, int running, int available)> GetConcurrencyStatusAsync(string token);
Task CleanupTimeoutTasksAsync(TimeSpan timeout);
}
}

View File

@ -0,0 +1,10 @@
using LMS.Repository.DB;
using Microsoft.AspNetCore.Mvc;
namespace LMS.Tools.MJPackage
{
public interface ITaskService
{
Task<Dictionary<string, object>?> FetchTaskAsync(MJApiTasks mJApiTasks);
}
}

View File

@ -0,0 +1,22 @@
using LMS.Repository.DB;
using LMS.Repository.MJPackage;
namespace LMS.Tools.MJPackage
{
public interface ITokenService
{
Task<TokenCacheItem> GetTokenAsync(string token);
Task<TokenCacheItem?> GetDatabaseTokenAsync(string token, bool hasHistory = false);
Task<MJApiTokens?> GetMJapiTokenByIdAsync(long tokenId);
Task ResetDailyUsage();
void IncrementUsage(string token);
Task<string> LoadOriginTokenAsync();
Task<string> GetOriginToken();
}
}

View File

@ -0,0 +1,216 @@
using LMS.Common.Extensions;
using LMS.DAO;
using LMS.Repository.DB;
using LMS.Repository.MJPackage;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Text;
namespace LMS.Tools.MJPackage
{
public class TaskConcurrencyManager : ITaskConcurrencyManager
{
private readonly ConcurrentDictionary<string, MJApiTasks> _activeTasks = new();
private readonly ConcurrentDictionary<string, string> _thirdPartyTaskMap = new(); // ThirdPartyTaskId -> TaskId
private readonly TokenUsageTracker _usageTracker;
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<TaskConcurrencyManager> _logger;
private readonly ApplicationDbContext _dbContext;
private readonly ITokenService _tokenService;
public TaskConcurrencyManager(
TokenUsageTracker usageTracker,
IServiceScopeFactory scopeFactory,
ILogger<TaskConcurrencyManager> logger,
ApplicationDbContext dbContext,
ITokenService tokenService)
{
_usageTracker = usageTracker;
_scopeFactory = scopeFactory;
_logger = logger;
_dbContext = dbContext;
_tokenService = tokenService;
}
/// <summary>
/// 尝试开始新任务(获取并发许可)
/// </summary>
public async Task CreateTaskAsync(
string token,
string thirdPartyTaskId)
{
try
{
TokenCacheItem? tokenConfig = await _tokenService.GetTokenAsync(token);
if (tokenConfig == null || string.IsNullOrWhiteSpace(tokenConfig.UseToken))
{
_logger.LogWarning($"无效的Token: {token}");
return;
}
// 创建任务信息
var taskId = Guid.NewGuid().ToString("N");
var mJApiTasks = new MJApiTasks
{
TaskId = taskId,
Token = token,
TokenId = tokenConfig.Id,
StartTime = BeijingTimeExtension.GetBeijingTime(),
Status = MJTaskStatus.NOT_START,
ThirdPartyTaskId = thirdPartyTaskId,
Properties = null
};
// 5. 持久化任务信息到数据库
await SaveTaskToDatabase(mJApiTasks);
}
catch (Exception ex)
{
_logger.LogError(ex, $"开始任务时发生错误: Token={token}");
}
}
/// <summary>
/// 获取任务信息
/// </summary>
public async Task<MJApiTasks> GetTaskInfoAsync(string taskId)
{
if (_activeTasks.TryGetValue(taskId, out var taskInfo))
{
return taskInfo;
}
// 如果内存中没有,尝试从数据库加载
return await LoadTaskFromDatabase(taskId);
}
/// <summary>
/// 通过第三方ID获取数据
/// </summary>
/// <param name="thirdPartyId"></param>
/// <returns></returns>
public async Task<MJApiTasks> GetTaskInfoByThirdPartyIdAsync(string thirdPartyId)
{
if (string.IsNullOrWhiteSpace(thirdPartyId))
{
_logger.LogWarning("第三方任务ID为空");
return null;
}
MJApiTasks? mJApiTasks = await _dbContext.MJApiTasks.FirstOrDefaultAsync(x => x.ThirdPartyTaskId == thirdPartyId);
return mJApiTasks;
}
/// <summary>
/// 获取运行中的任务列表
/// </summary>
public async Task<IEnumerable<MJApiTasks>> GetRunningTasksAsync(string token = null)
{
var runningTasks = _activeTasks.Values
.Where(t => t.Status != MJTaskStatus.SUCCESS && t.Status != MJTaskStatus.FAILURE && t.Status != MJTaskStatus.CANCEL)
.Where(t => string.IsNullOrEmpty(token) || t.Token == token)
.OrderBy(t => t.StartTime)
.ToList();
_logger.LogDebug($"当前运行中的任务数: {runningTasks.Count}" + (string.IsNullOrEmpty(token) ? "" : $", Token={token}"));
return await Task.FromResult(runningTasks);
}
/// <summary>
/// 获取Token的并发状态
/// </summary>
public async Task<(int maxConcurrency, int running, int available)> GetConcurrencyStatusAsync(string token)
{
var status = _usageTracker.GetConcurrencyStatus(token);
return await Task.FromResult((status.maxCount, status.currentlyExecuting, status.available));
}
/// <summary>
/// 清理超时任务
/// </summary>
public async Task CleanupTimeoutTasksAsync(TimeSpan timeout)
{
_logger.LogInformation($"开始清理超时任务,超时阈值: {timeout.TotalMinutes}分钟");
var cutoffTime = BeijingTimeExtension.GetBeijingTime() - timeout;
var timeoutTasks = _activeTasks.Values
.Where(t => t.StartTime < cutoffTime && t.Status != MJTaskStatus.SUCCESS && t.Status != MJTaskStatus.FAILURE && t.Status != MJTaskStatus.CANCEL)
.ToList();
_logger.LogInformation($"发现 {timeoutTasks.Count} 个超时任务");
foreach (var task in timeoutTasks)
{
_logger.LogWarning($"清理超时任务: TaskId={task.TaskId}, Token={task.Token}, 开始时间={task.StartTime:yyyy-MM-dd HH:mm:ss}");
_usageTracker.ReleaseConcurrencyPermit(task.Token);
}
}
/// <summary>
/// 保存任务到数据库
/// </summary>
private async Task SaveTaskToDatabase(MJApiTasks mJApiTasks)
{
try
{
await _dbContext.MJApiTasks.AddAsync(mJApiTasks);
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"任务已保存到数据库: TaskId={mJApiTasks.TaskId}, Token={mJApiTasks.Token}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"保存任务到数据库失败: TaskId={mJApiTasks.TaskId}");
}
}
/// <summary>
/// 更新数据库中的任务状态
/// </summary>
public async Task UpdateTaskInDatabase(MJApiTasks mJApiTasks)
{
try
{
MJApiTasks? apiTasks = await _dbContext.MJApiTasks.FirstOrDefaultAsync(x => x.ThirdPartyTaskId == mJApiTasks.ThirdPartyTaskId);
if (apiTasks == null)
{
_logger.LogWarning($"未找到任务: TaskId={mJApiTasks.TaskId}");
return;
}
apiTasks.Status = mJApiTasks.Status;
apiTasks.EndTime = mJApiTasks.EndTime;
apiTasks.Properties = mJApiTasks.Properties;
_dbContext.MJApiTasks.Update(apiTasks);
await _dbContext.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"更新任务状态到数据库失败: TaskId={mJApiTasks.TaskId}");
}
}
/// <summary>
/// 从数据库加载任务
/// </summary>
private async Task<MJApiTasks> LoadTaskFromDatabase(string taskId)
{
try
{
MJApiTasks? mJApiTasks = await _dbContext.MJApiTasks.FirstOrDefaultAsync(x => x.TaskId == taskId);
if (mJApiTasks == null)
{
_logger.LogWarning($"未找到任务: TaskId={taskId}");
return null;
}
return mJApiTasks;
}
catch (Exception ex)
{
_logger.LogError(ex, $"从数据库加载任务失败: TaskId={taskId}");
return null;
}
}
}
}

View File

@ -0,0 +1,147 @@

using LMS.Common.Extensions;
using LMS.DAO;
using LMS.Repository.DB;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace LMS.Tools.MJPackage
{
public class TaskService(ITokenService tokenService, ILogger<TaskService> logger, ApplicationDbContext dbContext, ITaskConcurrencyManager taskConcurrencyManager) : ITaskService
{
private readonly ITokenService _tokenService = tokenService;
private readonly ILogger<TaskService> _logger = logger;
private readonly ApplicationDbContext _dbContext = dbContext;
private readonly ITaskConcurrencyManager _taskConcurrencyManager = taskConcurrencyManager;
public async Task<Dictionary<string, object>?> FetchTaskAsync(MJApiTasks mJApiTasks)
{
try
{
// 获取UseToken先尝试 Token再尝试 TokenId
var tokenConfig = await _tokenService.GetTokenAsync(mJApiTasks.Token);
string useToken = string.Empty;
if (tokenConfig == null)
{
// Token 没找到 尝试用 TokenId 查找
MJApiTokens? mJApiTokens = await _tokenService.GetMJapiTokenByIdAsync(mJApiTasks.TokenId);
if (mJApiTokens == null)
{
return null;
}
useToken = mJApiTokens.UseToken;
}
else
{
useToken = tokenConfig.UseToken;
}
if (string.IsNullOrWhiteSpace(useToken))
{
_logger.LogInformation($"Token is empty for task ID: {mJApiTasks.TaskId}");
return null;
}
// 尝试备用API
var backupResult = await TryBackupApiAsync(mJApiTasks.ThirdPartyTaskId, useToken);
if (string.IsNullOrWhiteSpace(backupResult))
{
// 没有找到数据
_logger.LogInformation($"备用API没有返回数据TaskId: {mJApiTasks.TaskId}");
return null;
}
var properties = new Dictionary<string, object>();
try
{
// 不为空 开始解析数据
properties = JsonConvert.DeserializeObject<Dictionary<string, object>>(backupResult);
}
catch (JsonException ex)
{
_logger.LogError($"解析备用API返回数据失败: {ex.Message}");
return null;
}
if (properties == null)
{
_logger.LogInformation($"备用API返回数据为空TaskId: {mJApiTasks.TaskId}");
return null;
}
return properties;
}
catch (Exception ex)
{
// 记录异常日志
_logger.LogError($"Error fetching task: {ex.Message}");
return null;
}
}
private async Task<string?> TryBackupApiAsync(string id, string useToken)
{
const string backupUrlTemplate = "https://api.laitool.cc/mj/task/{0}/fetch";
const int maxRetries = 3;
const int baseDelayMs = 1000;
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "sk-" + useToken);
client.Timeout = TimeSpan.FromSeconds(30);
var backupUrl = string.Format(backupUrlTemplate, id);
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
var response = await client.GetAsync(backupUrl);
var content = await response.Content.ReadAsStringAsync();
// 判断请求是不是报错
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("备用API调用返回错误状态码TaskId: {TaskId}, Attempt: {Attempt}, StatusCode: {StatusCode}",
id, attempt, response.StatusCode);
return null;
}
return content;
}
catch (Exception ex) when (IsRetriableException(ex))
{
if (attempt < maxRetries)
{
var delay = baseDelayMs * (int)Math.Pow(2, attempt - 1);
_logger.LogWarning(ex, "备用API调用失败TaskId: {TaskId}, Attempt: {Attempt}, 将在{Delay}ms后重试",
id, attempt, delay);
await Task.Delay(delay);
}
else
{
_logger.LogError(ex, "备用API调用最终失败TaskId: {TaskId}, MaxAttempts: {MaxAttempts}",
id, maxRetries);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "备用API调用发生不可重试异常TaskId: {TaskId}, Attempt: {Attempt}",
id, attempt);
break;
}
}
return null;
}
private static bool IsRetriableException(Exception ex)
{
return ex is HttpRequestException ||
ex is TaskCanceledException ||
ex is SocketException;
}
}
}

View File

@ -0,0 +1,114 @@
using LMS.Common.Extensions;
using LMS.DAO;
using LMS.Repository.DB;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Quartz;
namespace LMS.Tools.MJPackage
{
[DisallowConcurrentExecution]
public class TaskStatusCheckService(ITokenService tokenService, ApplicationDbContext dbContext, ILogger<TaskStatusCheckService> logger, ITaskService taskService, ITaskConcurrencyManager taskConcurrencyManager) : IJob
{
private readonly ITokenService _tokenService = tokenService;
private readonly ApplicationDbContext _dbContext = dbContext;
private readonly ILogger<TaskStatusCheckService> _logger = logger;
private readonly ITaskService _taskService = taskService;
private readonly ITaskConcurrencyManager _taskConcurrencyManager = taskConcurrencyManager;
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation($"开始检查TASK信息 - 检查间隔: 5 分钟,同步加载 原始请求的Token");
var startTime = BeijingTimeExtension.GetBeijingTime();
try
{
// 强制同步数据库数据
await _tokenService.LoadOriginTokenAsync();
// 检查Task状态和返回值
// 获取所有超过五分钟没有完成的人物
List<MJApiTasks> tasks = await _dbContext.MJApiTasks.Where(t => t.Status != MJTaskStatus.CANCEL && t.Status != MJTaskStatus.SUCCESS && t.Status != MJTaskStatus.FAILURE && t.StartTime < BeijingTimeExtension.GetBeijingTime().AddMinutes(-5)).ToListAsync();
if (tasks.Count == 0)
{
_logger.LogInformation("没有需要检查的任务!");
return;
}
// 开始每个请求
foreach (MJApiTasks task in tasks)
{
try
{
Dictionary<string, object>? properties = await _taskService.FetchTaskAsync(task);
// 没有找到数据的
if (properties == null)
{
// 没有找到数据 直接把任务失败
task.Status = MJTaskStatus.FAILURE;
var newProperties = new
{
failReason = "任务丢失或未找到"
};
task.EndTime = BeijingTimeExtension.GetBeijingTime();
task.Properties = JsonConvert.SerializeObject(newProperties);
}
else
{
// 尝试获取状态字段
string status = MJTaskStatus.SUBMITTED;
if (properties.TryGetValue("status", out var statusElement))
{
status = statusElement.ToString() ?? MJTaskStatus.SUBMITTED;
}
else if (properties.TryGetValue("Status", out var statusElementCap))
{
status = statusElementCap.ToString() ?? MJTaskStatus.SUBMITTED;
}
task.Status = status;
if (status == MJTaskStatus.SUCCESS || status == MJTaskStatus.FAILURE || status == MJTaskStatus.CANCEL)
{
// 当前任务已经被释放过了
// 开始修改数据
task.EndTime = BeijingTimeExtension.GetBeijingTime();
task.Properties = JsonConvert.SerializeObject(properties);
}
else
{
// 任务还在处理中
task.EndTime = null; // 处理中没有结束时间
task.Properties = JsonConvert.SerializeObject(properties);
}
}
// 开始修改数据
await _taskConcurrencyManager.UpdateTaskInDatabase(task);
}
catch (Exception ex)
{
// 报错
_logger.LogError(ex, "检查任务 {TaskId} 时发生错误", task.TaskId);
task.Status = MJTaskStatus.FAILURE;
var newProperties = new
{
failReason = "任务报错"
};
task.EndTime = BeijingTimeExtension.GetBeijingTime();
task.Properties = JsonConvert.SerializeObject(newProperties);
// 开始修改数据
await _taskConcurrencyManager.UpdateTaskInDatabase(task);
}
}
var duration = BeijingTimeExtension.GetBeijingTime() - startTime;
_logger.LogInformation($"Task状态检查完成影响的Task {tasks.Count},耗时: {duration}ms", duration.TotalMilliseconds);
}
catch (Exception ex)
{
var duration = BeijingTimeExtension.GetBeijingTime() - startTime;
_logger.LogError(ex, "Token同步失败耗时: {Duration}ms", duration.TotalMilliseconds);
}
}
}
}

View File

@ -0,0 +1,24 @@
using LMS.Common.Extensions;
using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LMS.Tools.MJPackage
{
[DisallowConcurrentExecution]
public class TokenResetService(ITokenService tokenService, ILogger<TokenResetService> logger) : IJob
{
private readonly ITokenService _tokenService = tokenService;
private readonly ILogger<TokenResetService> _logger = logger;
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("开始每天重置 Token 日使用 统计数据, 执行时间 " + BeijingTimeExtension.GetBeijingTime());
await _tokenService.ResetDailyUsage();
}
}
}

View File

@ -0,0 +1,352 @@
using LMS.Common.Extensions;
using LMS.DAO;
using LMS.Repository.DB;
using LMS.Repository.MJPackage;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Data;
using System.Runtime.CompilerServices;
namespace LMS.Tools.MJPackage
{
public class TokenService(
ApplicationDbContext dbContext,
IMemoryCache memoryCache,
TokenUsageTracker usageTracker,
ILogger<TokenService> logger) : ITokenService
{
private readonly ApplicationDbContext _dbContext = dbContext;
private readonly IMemoryCache _memoryCache = memoryCache;
private readonly TokenUsageTracker _usageTracker = usageTracker;
private readonly ILogger<TokenService> _logger = logger;
/// <summary>
/// 从数据库获取token
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task<TokenCacheItem?> GetDatabaseTokenAsync(string token, bool hasHistory = false)
{
try
{
var today = BeijingTimeExtension.GetBeijingTime().Date;
// 使用EF Core的FromSqlRaw执行原生SQL
var dbResult = await _dbContext.Database
.SqlQuery<TokenQueryResult>($@"
SELECT
t.Id, t.Token, t.DailyLimit, t.TotalLimit, t.ConcurrencyLimit,
t.CreatedAt, t.ExpiresAt, t.UseToken,
COALESCE(u.DailyUsage, 0) as DailyUsage,
COALESCE(u.TotalUsage, 0) as TotalUsage,
COALESCE(u.HistoryUse, '') as HistoryUse,
COALESCE(u.LastActivityAt, t.CreatedAt) as LastActivityTime
FROM MJApiTokens t
LEFT JOIN MJApiTokenUsage u ON t.Id = u.TokenId
WHERE t.Token = {token}")
.FirstOrDefaultAsync();
if (dbResult == null)
{
return null;
}
// 3. 转换为TokenCacheItem
var tokenItem = new TokenCacheItem
{
Id = dbResult.Id,
Token = dbResult.Token,
UseToken = dbResult.UseToken ?? string.Empty, // 确保UseToken不为null
DailyLimit = dbResult.DailyLimit,
TotalLimit = dbResult.TotalLimit,
ConcurrencyLimit = dbResult.ConcurrencyLimit,
CreatedAt = dbResult.CreatedAt,
ExpiresAt = dbResult.ExpiresAt,
DailyUsage = dbResult.DailyUsage,
TotalUsage = dbResult.TotalUsage,
LastActivityTime = dbResult.LastActivityTime,
HistoryUse = hasHistory ? dbResult.HistoryUse : string.Empty
};
return tokenItem;
}
catch (Exception ex)
{
_logger.LogError(ex, $"从数据库获取Token时发生错误: {token}");
throw;
}
}
public async Task<MJApiTokens?> GetMJapiTokenByIdAsync(long tokenId)
{
try
{
MJApiTokens? mJApiTokens = await _dbContext.MJApiTokens
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == tokenId);
return mJApiTokens;
}
catch (Exception ex)
{
_logger.LogError(ex, $"获取Token ID {tokenId} 时发生错误");
throw;
}
}
/// <summary>
/// 异步获取Token信息先从缓存获取缓存未命中则从数据库加载
/// </summary>
/// <param name="token">Token字符串</param>
/// <returns>Token缓存项未找到返回null</returns>
public async Task<TokenCacheItem> GetTokenAsync(string token)
{
_logger.LogDebug($"开始获取Token: {token}");
// 1. 检查内存缓存
if (_usageTracker.TryGetToken(token, out var cacheItem))
{
_logger.LogDebug($"Token从内存缓存中获取成功: {token}");
return cacheItem;
}
// 2. 从数据库加载 - 使用EF Core原生SQL查询
_logger.LogDebug($"Token不在缓存中从数据库加载: {token}");
try
{
TokenCacheItem? tokenItem = await GetDatabaseTokenAsync(token);
if (tokenItem == null)
{
_logger.LogWarning($"Token未找到: {token}");
return null;
}
// 更新最后活动时间,从数据库中获取得话 设置最后活跃时间为当前时间
tokenItem.LastActivityTime = BeijingTimeExtension.GetBeijingTime();
// 4. 加入内存缓存
_usageTracker.AddOrUpdateToken(tokenItem);
// 5. 设置内存缓存 (30分钟)
_memoryCache.Set($"Token_{token}", tokenItem, TimeSpan.FromMinutes(30));
_logger.LogInformation($"Token从数据库加载成功: {token}, ID: {tokenItem.Id}, 日限制: {tokenItem.DailyLimit}, 并发限制: {tokenItem.ConcurrencyLimit}");
return tokenItem;
}
catch (Exception ex)
{
_logger.LogError(ex, $"从数据库获取Token时发生错误: {token}");
throw;
}
}
/// <summary>
/// 增加Token使用量
/// </summary>
/// <param name="token">Token字符串</param>
public void IncrementUsage(string token)
{
_logger.LogDebug($"递增Token使用量: {token}");
_usageTracker.IncrementUsage(token);
}
public async Task<string> LoadOriginTokenAsync()
{
// 没找到 从数据库中获取
Options? oprions = await _dbContext.Options.Where(x => x.Key == "MJPackageOriginToken").FirstOrDefaultAsync();
if (oprions == null)
{
_logger.LogWarning("未找到原始Token配置");
return string.Empty;
}
// 处理数据
string originToken = oprions.GetValueObject<string>() ?? string.Empty;
if (string.IsNullOrWhiteSpace(originToken))
{
_logger.LogWarning("未找到原始Token配置");
return string.Empty;
}
_usageTracker.OriginToken = originToken;
return originToken;
}
public async Task<string> GetOriginToken()
{
// 缓存中就有 直接返回
if (!string.IsNullOrWhiteSpace(_usageTracker.OriginToken))
{
return _usageTracker.OriginToken;
}
// 缓存中没有 从数据库中获取
return await LoadOriginTokenAsync();
}
/// <summary>
/// 重置Token的使用数据
/// </summary>
/// <returns></returns>
public async Task ResetDailyUsage()
{
var startTime = BeijingTimeExtension.GetBeijingTime();
try
{
// 批量重置数据库数据
int totalTokenCount = await BatchResetTokenDailyUsage();
// 删除不活跃的token
var (act, nact) = _usageTracker.RemoveNotActiveTokens(TimeSpan.FromMinutes(5));
// 重置缓存中的数据
_usageTracker.ResetDailyUsage();
var duration = BeijingTimeExtension.GetBeijingTime() - startTime;
_logger.LogInformation($"Token日使用量重置完成: {totalTokenCount} 个Token, 活跃Token: {act}, 耗时: {duration.TotalMilliseconds}ms");
}
catch (Exception ex)
{
var duration = BeijingTimeExtension.GetBeijingTime() - startTime;
_logger.LogError(ex, "Token同步失败耗时: {Duration}ms", duration.TotalMilliseconds);
}
}
/// <summary>
/// 批量重置当日使用限制
/// </summary>
/// <returns></returns>
private async Task<int> BatchResetTokenDailyUsage()
{
var beijingTime = BeijingTimeExtension.GetBeijingTime();
_logger.LogInformation($"重置token日限制开始批量重置 - 北京时间: {beijingTime:yyyy-MM-dd HH:mm:ss}");
// 修复SQL查询 - 只查询有使用记录且需要重置的Token
string sql = @"
SELECT
t.Id, t.Token, t.DailyLimit, t.TotalLimit, t.ConcurrencyLimit,
t.CreatedAt, t.ExpiresAt, t.UseToken,
COALESCE(u.DailyUsage, 0) as DailyUsage,
COALESCE(u.HistoryUse, '') as HistoryUse,
COALESCE(u.TotalUsage, 0) as TotalUsage,
COALESCE(u.LastActivityAt, t.CreatedAt) as LastActivityTime
FROM MJApiTokens t
LEFT JOIN MJApiTokenUsage u ON t.Id = u.TokenId
WHERE u.DailyUsage > 0
AND (t.ExpiresAt IS NULL OR t.ExpiresAt > UTC_TIMESTAMP())";
var dbResult = await _dbContext.Database
.SqlQuery<TokenQueryResult>(FormattableStringFactory.Create(sql))
.ToListAsync();
if (dbResult.Count == 0)
{
_logger.LogInformation("重置token日限制没有需要重置的token");
return 0;
}
_logger.LogInformation($"找到 {dbResult.Count} 个需要重置的Token");
// 统计重置前的总使用量
var totalDailyUsageBeforeReset = dbResult.Sum(x => x.DailyUsage);
_logger.LogInformation($"重置前总日使用量: {totalDailyUsageBeforeReset}");
var updatedCount = 0;
const int batchSize = 100; // 分批处理,避免内存过大
// 分批处理Token重置
for (int batchStart = 0; batchStart < dbResult.Count; batchStart += batchSize)
{
var batch = dbResult.Skip(batchStart).Take(batchSize).ToList();
var batchTokenIds = batch.Select(x => x.Id).ToList();
// 批量查询当前批次的使用记录
var tokenUsageList = await _dbContext.MJApiTokenUsage
.Where(x => batchTokenIds.Contains(x.TokenId))
.ToListAsync();
if (!tokenUsageList.Any())
{
_logger.LogWarning($"批次 {batchStart / batchSize + 1}: 没有找到使用记录");
continue;
}
// 使用事务确保数据一致性
using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
foreach (var tokenUsage in tokenUsageList)
{
var tokenInfo = batch.FirstOrDefault(x => x.Id == tokenUsage.TokenId);
if (tokenInfo == null || tokenUsage.DailyUsage == 0)
continue;
// 处理历史记录
ProcessHistoryAndResetUsage(tokenUsage, tokenInfo);
}
// 批量保存
var batchUpdated = await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
updatedCount += batchUpdated;
_logger.LogInformation($"批次 {batchStart / batchSize + 1} 完成: 处理 {batch.Count} 个Token更新 {batchUpdated} 条记录");
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, $"批次 {batchStart / batchSize + 1} 重置失败");
throw;
}
}
_logger.LogInformation($"✅ 批量重置完成 - 总共更新 {updatedCount} 条记录");
_logger.LogInformation($"📊 重置统计 - 重置前日使用量: {totalDailyUsageBeforeReset} → 重置后: 0");
return updatedCount;
}
/// <summary>
/// 处理历史记录并重置使用量
/// </summary>
private void ProcessHistoryAndResetUsage(MJApiTokenUsage tokenUsage, TokenQueryResult tokenInfo)
{
try
{
// 解析现有历史记录
List<MJApiTokenUsage> historyList;
try
{
historyList = string.IsNullOrEmpty(tokenUsage.HistoryUse)
? []
: JsonConvert.DeserializeObject<List<MJApiTokenUsage>>(tokenUsage.HistoryUse) ?? new List<MJApiTokenUsage>();
}
catch (JsonException ex)
{
_logger.LogWarning(ex, $"Token {tokenInfo.Token} 历史记录JSON解析失败将创建新的历史记录");
historyList = [];
}
// 添加当前记录到历史
historyList.Add(new MJApiTokenUsage
{
TokenId = tokenUsage.TokenId,
Date = BeijingTimeExtension.GetBeijingTime().Date.AddDays(-1),
DailyUsage = tokenUsage.DailyUsage,
TotalUsage = tokenUsage.TotalUsage,
LastActivityAt = tokenUsage.LastActivityAt,
HistoryUse = ""
});
// 重置使用量
tokenUsage.DailyUsage = 0;
tokenUsage.HistoryUse = JsonConvert.SerializeObject(historyList);
_logger.LogDebug($"Token {tokenInfo.Token} 重置: 日使用量 {tokenUsage.DailyUsage} → 0, 历史记录数: {historyList.Count}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"处理Token {tokenInfo.Token} 的历史记录时发生错误");
}
}
}
}

View File

@ -0,0 +1,198 @@
using LMS.Common.Extensions;
using LMS.DAO;
using LMS.Repository.MJPackage;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Quartz;
using System.Data;
using System.Text;
namespace LMS.Tools.MJPackage
{
[DisallowConcurrentExecution]
public class TokenSyncService : IJob
{
private readonly TokenUsageTracker _usageTracker;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<TokenSyncService> _logger;
// 活动阈值5分钟内有活动的Token才同步
private readonly TimeSpan _activityThreshold = TimeSpan.FromMinutes(5);
public TokenSyncService(
TokenUsageTracker usageTracker,
IServiceProvider serviceProvider,
ILogger<TokenSyncService> logger)
{
_usageTracker = usageTracker;
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation($"开始同步Token信息 - 同步间隔: 30 秒, 活动阈值: {_activityThreshold.TotalMinutes}分钟 (使用EF Core)");
var startTime = BeijingTimeExtension.GetBeijingTime();
try
{
var syncResult = await SyncActiveTokensToDatabase();
var duration = BeijingTimeExtension.GetBeijingTime() - startTime;
if (syncResult.ActiveTokenCount > 0)
{
_logger.LogInformation(
"Token同步完成: {ActiveTokens}/{TotalTokens} 个活跃Token已同步, 耗时: {Duration}ms, 更新记录: {RecordsUpdated}",
syncResult.ActiveTokenCount,
syncResult.TotalTokenCount,
duration.TotalMilliseconds,
syncResult.RecordsUpdated);
}
else
{
_logger.LogDebug(
"Token同步跳过: 无活跃Token (总计: {TotalTokens}, 耗时: {Duration}ms)",
syncResult.TotalTokenCount,
duration.TotalMilliseconds);
}
}
catch (Exception ex)
{
var duration = BeijingTimeExtension.GetBeijingTime() - startTime;
_logger.LogError(ex, "Token同步失败耗时: {Duration}ms", duration.TotalMilliseconds);
}
}
/// <summary>
/// 同步活跃Token数据到数据库
/// </summary>
/// <returns>同步结果</returns>
private async Task<SyncResult> SyncActiveTokensToDatabase()
{
// 先 删除10分钟内不活跃得Token
var (act, nact) = _usageTracker.RemoveNotActiveTokens(TimeSpan.FromMinutes(10));
_logger.LogInformation($"删除不活跃的 Token 数 {nact},删除后活跃 Token 数:{act},判断不活跃时间:{10} 分钟");
// 1. 获取活跃Token最近5分钟内有活动的Token
var activeTokens = _usageTracker.GetActiveTokens(_activityThreshold).ToList();
var totalTokens = _usageTracker.GetAllTokens().Count();
if (!activeTokens.Any())
{
_logger.LogInformation("0 条活跃Token跳过同步");
return new SyncResult
{
TotalTokenCount = totalTokens,
ActiveTokenCount = 0,
RecordsUpdated = 0
};
}
// 2. 创建数据库上下文
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var today = BeijingTimeExtension.GetBeijingTime().Date;
var recordsUpdated = 0;
// 3. 构造批量数据
var batchData = activeTokens.Select(token => new TokenUsageData
{
TokenId = token.Id,
Date = today,
DailyUsage = token.DailyUsage,
TotalUsage = token.TotalUsage,
LastActivityTime = token.LastActivityTime
}).ToList();
// 4. 使用EF Core事务批量更新数据库
using var transaction = await dbContext.Database.BeginTransactionAsync();
try
{
recordsUpdated = await BatchUpdateTokenUsageWithEfCore(dbContext, batchData);
await transaction.CommitAsync();
_logger.LogDebug(
"批量更新完成: {RecordsUpdated} 条活跃Token记录已更新跳过 {SkippedCount} 条非活跃Token",
recordsUpdated,
totalTokens - activeTokens.Count);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "数据库事务失败,已回滚");
throw;
}
return new SyncResult
{
TotalTokenCount = totalTokens,
ActiveTokenCount = activeTokens.Count,
RecordsUpdated = recordsUpdated
};
}
/// <summary>
/// 使用EF Core批量更新Token使用数据
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="batchData">批量数据</param>
/// <returns>更新的记录数</returns>
private async Task<int> BatchUpdateTokenUsageWithEfCore(ApplicationDbContext dbContext, List<TokenUsageData> batchData)
{
int batchSize = 500;
if (!batchData.Any()) return 0;
var recordsUpdated = 0;
// 分批处理
for (int i = 0; i < batchData.Count; i += batchSize)
{
var batch = batchData.Skip(i).Take(batchSize).ToList();
// 构建真正的批量 SQL
var sqlBuilder = new StringBuilder();
var parameters = new List<object>();
sqlBuilder.AppendLine("INSERT INTO MJApiTokenUsage (TokenId, Date, DailyUsage, TotalUsage, LastActivityAt) VALUES ");
// 为每条记录构建 VALUES 子句
for (int j = 0; j < batch.Count; j++)
{
if (j > 0) sqlBuilder.Append(", ");
var paramIndex = j * 5;
sqlBuilder.Append($"({{{paramIndex}}}, {{{paramIndex + 1}}}, {{{paramIndex + 2}}}, {{{paramIndex + 3}}}, {{{paramIndex + 4}}})");
parameters.AddRange(new object[]
{
batch[j].TokenId,
batch[j].Date,
batch[j].DailyUsage,
batch[j].TotalUsage,
batch[j].LastActivityTime
});
}
sqlBuilder.AppendLine(@"
ON DUPLICATE KEY UPDATE
Date = VALUES(Date),
DailyUsage = VALUES(DailyUsage),
TotalUsage = VALUES(TotalUsage),
LastActivityAt = VALUES(LastActivityAt)");
// 一次性执行整个批次
var affectedRows = await dbContext.Database.ExecuteSqlRawAsync(
sqlBuilder.ToString(),
parameters.ToArray());
recordsUpdated += affectedRows;
_logger.LogInformation($"批量更新完成: 批次 {i / batchSize + 1}, 记录数: {batch.Count}, 影响行数: {affectedRows}");
}
return recordsUpdated;
}
}
}

View File

@ -0,0 +1,504 @@
using LMS.Common.Extensions;
using LMS.Repository.DB;
using LMS.Repository.MJPackage;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
namespace LMS.Tools.MJPackage
{
public class TokenUsageTracker
{
private readonly ConcurrentDictionary<string, TokenCacheItem> _tokenCache = new();
private readonly ConcurrentDictionary<string, Lazy<ConcurrencyController>> _concurrencyControllers = new();
private readonly ReaderWriterLockSlim _cacheLock = new(LockRecursionPolicy.SupportsRecursion);
private string _originToken = string.Empty;
private readonly ILogger<TokenUsageTracker> _logger;
public TokenUsageTracker(ILogger<TokenUsageTracker> logger)
{
_logger = logger;
_logger.LogInformation("TokenUsageTracker服务已初始化");
}
/// <summary>
/// 并发控制器 - 支持平滑调整并发限制
/// </summary>
private class ConcurrencyController
{
private SemaphoreSlim _semaphore;
private int _maxCount;
private int _currentlyExecuting;
private readonly object _lock = new object();
private readonly ILogger _logger;
public ConcurrencyController(int initialLimit, ILogger logger)
{
_maxCount = initialLimit;
_semaphore = new SemaphoreSlim(initialLimit, initialLimit);
_currentlyExecuting = 0;
_logger = logger;
}
/// <summary>
/// 获取当前最大并发数
/// </summary>
public int MaxCount => _maxCount;
/// <summary>
/// 获取当前正在执行的任务数
/// </summary>
public int CurrentlyExecuting => _currentlyExecuting;
/// <summary>
/// 获取当前可用的并发槽位
/// </summary>
public int AvailableCount => _semaphore.CurrentCount;
/// <summary>
/// 等待获取执行许可
/// </summary>
public async Task<bool> WaitAsync(string token)
{
var acquired = await _semaphore.WaitAsync(0);
if (acquired)
{
lock (_lock)
{
_currentlyExecuting++;
}
_logger.LogInformation($"Token获取并发许可: {token}, 当前执行中: {_currentlyExecuting}/{_maxCount}");
}
return acquired;
}
/// <summary>
/// 释放执行许可
/// </summary>
public void Release(string token)
{
lock (_lock)
{
if (_currentlyExecuting > 0)
{
_currentlyExecuting--;
_semaphore.Release();
_logger.LogInformation($"Token释放并发许可: {token}, 当前执行中: {_currentlyExecuting}/{_maxCount}");
}
else
{
_logger.LogWarning($"Token释放并发许可: {token}, 尝试释放许可但当前执行数已为0: {token}");
}
}
}
/// <summary>
/// 平滑调整并发限制
/// </summary>
public bool AdjustLimitAsync(int newLimit, string token)
{
if (newLimit <= 0)
{
throw new ArgumentException("并发限制必须大于0", nameof(newLimit));
}
lock (_lock)
{
if (_maxCount == newLimit)
{
return false; // 无需调整
}
var oldLimit = _maxCount;
_maxCount = newLimit;
if (newLimit > oldLimit)
{
// 扩大并发限制:释放额外的许可
var additionalPermits = newLimit - oldLimit;
for (int i = 0; i < additionalPermits; i++)
{
_semaphore.Release();
}
_logger.LogInformation($"Token并发限制已扩大: {token}, {oldLimit} -> {newLimit}, 当前执行: {_currentlyExecuting}");
}
else
{
// 缩小并发限制:等待现有任务完成
var excessExecuting = _currentlyExecuting - newLimit;
if (excessExecuting > 0)
{
_logger.LogWarning($"Token并发限制缩小但有超额任务: {token}, {oldLimit} -> {newLimit}, 超额: {excessExecuting}, 将等待任务自然完成");
}
else
{
_logger.LogInformation($"Token并发限制已缩小: {token}, {oldLimit} -> {newLimit}, 当前执行: {_currentlyExecuting}");
}
}
return true;
}
}
/// <summary>
/// 销毁资源
/// </summary>
public void Dispose()
{
_semaphore?.Dispose();
}
}
/// <summary>
/// 尝试从缓存中获取Token
/// </summary>
public bool TryGetToken(string token, out TokenCacheItem cacheItem)
{
var found = _tokenCache.TryGetValue(token, out cacheItem);
if (found)
{
_logger.LogDebug($"从缓存中找到Token: {token}");
}
return found;
}
/// <summary>
/// 添加或更新Token到缓存支持平滑并发限制调整
/// </summary>
public void AddOrUpdateTokenAsync(TokenCacheItem tokenItem)
{
_cacheLock.EnterWriteLock();
try
{
_tokenCache[tokenItem.Token] = tokenItem;
// 获取或创建并发控制器
var lazyController = _concurrencyControllers.GetOrAdd(
tokenItem.Token,
_ => new Lazy<ConcurrencyController>(() =>
new ConcurrencyController(tokenItem.ConcurrencyLimit, _logger)));
var controller = lazyController.Value;
// 平滑调整并发限制
var adjusted = controller.AdjustLimitAsync(tokenItem.ConcurrencyLimit, tokenItem.Token);
if (adjusted)
{
_logger.LogInformation($"Token并发限制已调整: {tokenItem.Token}, 新限制: {tokenItem.ConcurrencyLimit}");
}
_logger.LogDebug($"Token已添加到缓存: {tokenItem.Token}, 日限制: {tokenItem.DailyLimit}, 总限制: {tokenItem.TotalLimit}, 并发限制: {tokenItem.ConcurrencyLimit}");
}
finally
{
_cacheLock.ExitWriteLock();
}
}
/// <summary>
/// 同步版本(保持向后兼容)
/// </summary>
public void AddOrUpdateToken(TokenCacheItem tokenItem)
{
// 使用异步版本,但同步等待
AddOrUpdateTokenAsync(tokenItem);
}
/// <summary>
/// 增加Token使用量
/// </summary>
public void IncrementUsage(string token)
{
_cacheLock.EnterUpgradeableReadLock();
try
{
if (_tokenCache.TryGetValue(token, out var cacheItem))
{
_cacheLock.EnterWriteLock();
try
{
cacheItem.DailyUsage++;
cacheItem.TotalUsage++;
cacheItem.LastActivityTime = BeijingTimeExtension.GetBeijingTime();
_logger.LogDebug($"Token使用量已更新: {token}, 今日使用: {cacheItem.DailyUsage}, 总使用: {cacheItem.TotalUsage}");
}
finally
{
_cacheLock.ExitWriteLock();
}
}
else
{
_logger.LogWarning($"尝试更新未缓存的Token使用量: {token}");
}
}
finally
{
_cacheLock.ExitUpgradeableReadLock();
}
}
/// <summary>
/// 获取Token的并发控制器
/// </summary>
public async Task<bool> WaitForConcurrencyPermitAsync(string token)
{
if (_concurrencyControllers.TryGetValue(token, out var controller))
{
return await controller.Value.WaitAsync(token);
}
_logger.LogWarning($"未找到Token的并发控制器: {token}");
return false;
}
/// <summary>
/// 释放Token的并发许可
/// </summary>
public void ReleaseConcurrencyPermit(string token)
{
if (_concurrencyControllers.TryGetValue(token, out var controller))
{
controller.Value.Release(token);
}
else
{
_logger.LogWarning($"未找到Token的并发控制器无法释放: {token}");
}
}
/// <summary>
/// 获取Token的并发状态信息
/// </summary>
public (int maxCount, int currentlyExecuting, int available) GetConcurrencyStatus(string token)
{
_cacheLock.EnterReadLock();
try
{
if (_concurrencyControllers.TryGetValue(token, out var controller))
{
return (controller.Value.MaxCount, controller.Value.CurrentlyExecuting, controller.Value.AvailableCount);
}
return (0, 0, 0);
}
finally
{
_cacheLock.ExitReadLock();
}
}
/// <summary>
/// 获取活跃Token列表
/// </summary>
public List<TokenCacheItem> GetActiveTokens(TimeSpan activityThreshold)
{
_cacheLock.EnterReadLock();
try
{
var cutoffTime = BeijingTimeExtension.GetBeijingTime() - activityThreshold;
var activeTokens = _tokenCache.Values
.Where(t => t.LastActivityTime > cutoffTime)
.ToList();
_logger.LogDebug($"找到 {activeTokens.Count} 个活跃Token (阈值: {activityThreshold.TotalMinutes} 分钟)");
return activeTokens;
}
finally
{
_cacheLock.ExitReadLock();
}
}
/// <summary>
/// 移除不活跃的Token
/// </summary>
/// <param name="activityThreshold">活跃时间阈值</param>
/// <returns>移除的Token数量</returns>
public (int activateTokenCount, int notActivateTokenCount) RemoveNotActiveTokens(TimeSpan activityThreshold)
{
_cacheLock.EnterWriteLock();
try
{
var cutoffTime = BeijingTimeExtension.GetBeijingTime() - activityThreshold;
var initialCount = _tokenCache.Count;
// 找出需要移除的不活跃Token
var tokensToRemove = _tokenCache
.Where(kvp => kvp.Value.LastActivityTime <= cutoffTime)
.Select(kvp => kvp.Key)
.ToList();
if (tokensToRemove.Count == 0)
{
_logger.LogDebug($"没有找到需要移除的不活跃Token (阈值: {activityThreshold.TotalMinutes} 分钟)");
return (initialCount, 0);
}
// 移除不活跃的Token缓存
var removedCount = 0;
foreach (var tokenKey in tokensToRemove)
{
if (_tokenCache.TryRemove(tokenKey, out var removedToken))
{
// 同时清理对应的并发控制器
if (_concurrencyControllers.TryRemove(tokenKey, out var controller))
{
// 如果并发控制器已经被创建,需要释放资源
if (controller.IsValueCreated)
{
controller.Value.Dispose();
}
}
removedCount++;
_logger.LogDebug($"移除不活跃Token: {tokenKey}, 最后活跃时间: {removedToken.LastActivityTime:yyyy-MM-dd HH:mm:ss}");
}
}
_logger.LogInformation($"清理不活跃Token完成: 移除 {removedCount} 个Token (阈值: {activityThreshold.TotalMinutes} 分钟), 剩余: {_tokenCache.Count} 个");
return (_tokenCache.Count, removedCount);
}
finally
{
_cacheLock.ExitWriteLock();
}
}
/// <summary>
/// 移除指定的token
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public int RemoveToken(string token)
{
_cacheLock.EnterWriteLock();
try
{
// 找出需要移除的不活跃Token
var tokensToRemove = _tokenCache
.Where(kvp => kvp.Value.Token == token)
.Select(kvp => kvp.Key)
.ToList();
if (tokensToRemove.Count == 0)
{
// 没有找到
return 0;
}
// 移除不活跃的Token缓存
var removedCount = 0;
foreach (var tokenKey in tokensToRemove)
{
if (_tokenCache.TryRemove(tokenKey, out var removedToken))
{
// 同时清理对应的并发控制器
if (_concurrencyControllers.TryRemove(tokenKey, out var controller))
{
// 如果并发控制器已经被创建,需要释放资源
if (controller.IsValueCreated)
{
controller.Value.Dispose();
}
}
removedCount++;
}
}
return _tokenCache.Count;
}
finally
{
_cacheLock.ExitWriteLock();
}
}
/// <summary>
/// 获取所有Token列表
/// </summary>
public IEnumerable<TokenCacheItem> GetAllTokens()
{
_cacheLock.EnterReadLock();
try
{
return _tokenCache.Values.ToList();
}
finally
{
_cacheLock.ExitReadLock();
}
}
/// <summary>
/// 获取缓存统计信息(包含并发状态)
/// </summary>
public TokenCacheStats GetCacheStats()
{
_cacheLock.EnterReadLock();
try
{
var now = BeijingTimeExtension.GetBeijingTime();
var tokens = _tokenCache.Values.ToList();
var stats = new TokenCacheStats
{
TotalTokens = tokens.Count,
ActiveTokens = tokens.Count(t => t.LastActivityTime > now.AddMinutes(-5)),
InactiveTokens = tokens.Count(t => t.LastActivityTime <= now.AddMinutes(-5)),
TotalDailyUsage = tokens.Sum(t => t.DailyUsage),
TotalUsage = tokens.Sum(t => t.TotalUsage)
};
return stats;
}
finally
{
_cacheLock.ExitReadLock();
}
}
/// <summary>
/// 重置所有Token的日使用量
/// </summary>
public void ResetDailyUsage()
{
_cacheLock.EnterWriteLock();
try
{
// 重置缓存中的使用量
foreach (var token in _tokenCache.Values)
{
token.DailyUsage = 0;
}
}
finally
{
_cacheLock.ExitWriteLock();
}
}
public string OriginToken
{
get => _originToken;
set
{
if (!string.IsNullOrWhiteSpace(value))
{
_originToken = value;
}
else
{
// 如果尝试设置为空值,记录警告日志,可能请求原始的请求不可用
_logger.LogWarning("尝试设置OriginToken为空值可能请求原始的请求不可用");
}
}
}
}
}

View File

@ -1,62 +1,100 @@
using LMS.Tools.TaskScheduler; using LMS.Tools.MJPackage;
using LMS.Tools.TaskScheduler;
using Quartz; using Quartz;
namespace LMS.service.Configuration
public static class QuartzTaskSchedulerConfig
{ {
public static class QuartzTaskSchedulerConfig public static void AddQuartzTaskSchedulerService(this IServiceCollection services)
{ {
public static void AddQuartzTaskSchedulerService(this IServiceCollection services) services.AddQuartz(q =>
{ {
// 注册 Quartz 服务 // 时区配置
services.AddQuartz(q => var chinaTimeZone = GetChinaTimeZone();
// 每月任务配置
ConfigureMonthlyTask(q, chinaTimeZone);
// 每日任务配置
ConfigureDailyTask(q, chinaTimeZone);
// 每30秒任务配置
ConfigureThirtySecondTask(q, chinaTimeZone);
ConfigureFiveMinuteTask(q, chinaTimeZone);
});
services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});
// 注册作业类
services.AddTransient<ResetUserFreeCount>();
services.AddTransient<TokenResetService>();
services.AddTransient<TokenSyncService>();
services.AddTransient<TaskStatusCheckService>();
}
private static TimeZoneInfo GetChinaTimeZone()
{
try
{
return TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");
}
catch
{
try
{ {
return TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai");
// 配置作业 }
var jobKey = new JobKey("MonthlyTask", "DefaultGroup"); catch
// 方法1通过配置属性设置时区
// 获取中国时区
TimeZoneInfo chinaTimeZone;
try
{
// 尝试获取 Windows 时区名称
chinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");
}
catch
{
try
{
// 尝试获取 Linux 时区名称
chinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai");
}
catch
{
// 如果都不可用,使用 UTC+8
chinaTimeZone = TimeZoneInfo.CreateCustomTimeZone(
"China_Custom",
new TimeSpan(8, 0, 0),
"China Custom Time",
"China Standard Time");
}
}
// 添加作业
q.AddJob<ResetUserFreeCount>(opts => opts.WithIdentity(jobKey));
// 添加触发器 - 每月1号凌晨0点执行
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("MonthlyTaskTrigger", "DefaultGroup")
.WithCronSchedule("0 0 0 1 * ?")); // 每月1号凌晨0点
});
// 添加 Quartz 托管服务
services.AddQuartzHostedService(options =>
{ {
options.WaitForJobsToComplete = true; return TimeZoneInfo.CreateCustomTimeZone(
}); "China_Custom",
new TimeSpan(8, 0, 0),
// 注册作业 "China Custom Time",
services.AddTransient<ResetUserFreeCount>(); "China Standard Time");
}
} }
} }
}
private static void ConfigureMonthlyTask(IServiceCollectionQuartzConfigurator q, TimeZoneInfo timeZone)
{
var jobKey = new JobKey("MonthlyTask", "DefaultGroup");
q.AddJob<ResetUserFreeCount>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("MonthlyTaskTrigger", "DefaultGroup")
.WithCronSchedule("0 0 0 1 * ?", x => x.InTimeZone(timeZone)));
}
private static void ConfigureDailyTask(IServiceCollectionQuartzConfigurator q, TimeZoneInfo timeZone)
{
var jobKey = new JobKey("DailyTask", "DefaultGroup");
q.AddJob<TokenResetService>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("DailyTaskTrigger", "DefaultGroup")
.WithCronSchedule("0 10 0 * * ?", x => x.InTimeZone(timeZone))); // 每天凌晨0点10分执行
}
private static void ConfigureThirtySecondTask(IServiceCollectionQuartzConfigurator q, TimeZoneInfo timeZone)
{
var jobKey = new JobKey("ThirtySecondTask", "DefaultGroup");
q.AddJob<TokenSyncService>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("ThirtySecondTaskTrigger", "DefaultGroup")
.WithCronSchedule("*/30 * * * * ?", x => x.InTimeZone(timeZone))); // 每30秒执行一次
}
private static void ConfigureFiveMinuteTask(IServiceCollectionQuartzConfigurator q, TimeZoneInfo timeZone)
{
var jobKey = new JobKey("FiveMinuteTask", "DefaultGroup");
q.AddJob<TaskStatusCheckService>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("FiveMinuteTaskTrigger", "DefaultGroup")
.WithCronSchedule("0 */5 * * * ?", x => x.InTimeZone(timeZone))); // 每5分钟执行一次
}
}

View File

@ -5,12 +5,14 @@ using LMS.DAO.UserDAO;
using LMS.service.Configuration.InitConfiguration; using LMS.service.Configuration.InitConfiguration;
using LMS.service.Extensions.Mail; using LMS.service.Extensions.Mail;
using LMS.service.Service; using LMS.service.Service;
using LMS.service.Service.MJPackage;
using LMS.service.Service.Other; using LMS.service.Service.Other;
using LMS.service.Service.PermissionService; using LMS.service.Service.PermissionService;
using LMS.service.Service.PromptService; using LMS.service.Service.PromptService;
using LMS.service.Service.RoleService; using LMS.service.Service.RoleService;
using LMS.service.Service.SoftwareService; using LMS.service.Service.SoftwareService;
using LMS.service.Service.UserService; using LMS.service.Service.UserService;
using LMS.Tools.MJPackage;
namespace Lai_server.Configuration namespace Lai_server.Configuration
{ {
@ -38,6 +40,8 @@ namespace Lai_server.Configuration
services.AddScoped<SoftwareService>(); services.AddScoped<SoftwareService>();
services.AddScoped<MachineAuthorizationService>(); services.AddScoped<MachineAuthorizationService>();
services.AddScoped<DataInfoService>(); services.AddScoped<DataInfoService>();
services.AddScoped<ITokenManagementService, TokenManagementService>();
services.AddScoped<IMJPackageService, MJPackageService>();
// 注入 DAO // 注入 DAO
services.AddScoped<UserBasicDao>(); services.AddScoped<UserBasicDao>();
@ -53,6 +57,20 @@ namespace Lai_server.Configuration
// 添加分布式缓存(用于存储验证码) // 添加分布式缓存(用于存储验证码)
services.AddDistributedMemoryCache(); services.AddDistributedMemoryCache();
// 注册自定义服务
services.AddSingleton<TokenUsageTracker>();
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<ITaskConcurrencyManager, TaskConcurrencyManager>();
services.AddScoped<ITaskService, TaskService>();
// 注册后台服务
services.AddHostedService<RsaConfigurattions>();
services.AddHostedService<DatabaseConfiguration>();
//services.AddHostedService<TokenSyncService>();
//services.AddHostedService<DailyResetService>();
} }
} }
} }

View File

@ -0,0 +1,115 @@
using Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels;
using LMS.Repository.MJPackage;
using LMS.service.Extensions.Attributes;
using LMS.service.Service.MJPackage;
using LMS.Tools.MJPackage;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Text;
using System.Text.Json;
namespace LMS.service.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MJPackageController(TokenUsageTracker usageTracker, ITaskConcurrencyManager taskConcurrencyManager, ILogger<MJPackageController> logger, IMJPackageService mJPackageService) : ControllerBase
{
private readonly TokenUsageTracker _usageTracker = usageTracker;
private readonly ILogger<MJPackageController> _logger = logger;
private readonly ITaskConcurrencyManager _taskConcurrencyManager = taskConcurrencyManager;
private readonly IMJPackageService _mJPackageService = mJPackageService;
[HttpPost("mj/submit/imagine")]
[RateLimit]
public async Task<IActionResult> Imagine([FromBody] MJSubmitImageModel model)
{
string token = (string)(HttpContext.Items["UseToken"] ?? string.Empty);
string requestToken = (string)(HttpContext.Items["RequestToken"] ?? string.Empty);
if (string.IsNullOrWhiteSpace(token))
{
return Unauthorized("API token is empty");
}
if (string.IsNullOrWhiteSpace(requestToken))
{
return Unauthorized("API token is empty");
}
using HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer sk-" + token);
model.NotifyHook = "https://lms.laitool.cn/api/MJPackage/mj/mj-notify-hook";
string body = JsonConvert.SerializeObject(model);
client.Timeout = Timeout.InfiniteTimeSpan;
string mjUrl = "https://api.laitool.cc/mj/submit/imagine";
var response = await client.PostAsync(mjUrl, new StringContent(body, Encoding.UTF8, "application/json"));
// 读取响应内容
string content = await response.Content.ReadAsStringAsync();
// 直接返回原始响应,包括状态码和内容
var res = new ContentResult
{
Content = content,
ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/json",
StatusCode = (int)response.StatusCode
};
// 判断请求任务的状态,判断是不是需要释放
if (res.StatusCode != 200 && res.StatusCode != 201)
{
// 释放
_usageTracker.ReleaseConcurrencyPermit(requestToken);
_logger.LogInformation($"请求失败Token并发许可已释放: {token}, 状态码: {res.StatusCode}");
return res;
}
// 判断是不是提交成功并且记录请求返回的ID
// 把 Content 转换为 匿名 对象
var result = JsonConvert.DeserializeAnonymousType(content, new
{
code = 1,
description = "提交成功",
result = 1320098173412546
});
if (result == null)
{
// 失败
_usageTracker.ReleaseConcurrencyPermit(requestToken);
_logger.LogInformation($"请求失败返回的请求体为空Token并发许可已释放: {token}, 状态码: {res.StatusCode}");
return res;
}
else
{
if (result.code != 1 && result.code != 22)
{
_usageTracker.ReleaseConcurrencyPermit(requestToken);
_logger.LogInformation($"请求失败code: {result.code}Token并发许可已释放: {token}, 状态码: {res.StatusCode}");
return res;
}
}
// 开始写入任务
await _taskConcurrencyManager.CreateTaskAsync(requestToken, result.result.ToString());
return res;
}
[HttpGet("mj/task/{id}/fetch")]
public async Task<ActionResult<object>> Fetch(string id)
{
string token = HttpContext.Request.Headers.Authorization.ToString()
.Replace("Bearer ", "", StringComparison.OrdinalIgnoreCase)
.Trim();
var res = await _mJPackageService.FetchTaskAsync(id, token);
return res;
}
[HttpPost("mj/mj-notify-hook")]
//[Route("mjPackage/mj/mj-notify-hook")]
public async Task<IActionResult> MJNotifyHook([FromBody] JsonElement model)
{
return await _mJPackageService.MJNotifyHookAsync(model);
}
}
}

View File

@ -0,0 +1,212 @@
using LMS.Common.Extensions;
using LMS.Repository.DB;
using LMS.Repository.DTO;
using LMS.Repository.MJPackage;
using LMS.service.Service.MJPackage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
namespace LMS.service.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class TokenManagementController : ControllerBase
{
private readonly ITokenManagementService _tokenManagementService;
private readonly ILogger<TokenManagementController> _logger;
public TokenManagementController(
ITokenManagementService tokenManagementService,
ILogger<TokenManagementController> logger)
{
_tokenManagementService = tokenManagementService;
_logger = logger;
}
#region -使Token查询对应的任务
[HttpGet("{token}")]
public async Task<ActionResult<APIResponseModel<CollectionResponse<TokenAndTaskCollection>>>> QueryTokenTaskCollection(string token, [Required] int page, [Required] int pageSize, string? thirdPartyTaskId)
{
return await _tokenManagementService.QueryTokenTaskCollection(token, page, pageSize, thirdPartyTaskId);
}
#endregion
#region -Token是不是存在
[HttpGet("{token}")]
public async Task<ActionResult<APIResponseModel<TokenCacheItem>>> GetTokenItem(string token)
{
return await _tokenManagementService.GetTokenItem(token);
}
#endregion
#region -Token
[HttpPost]
[Authorize]
public async Task<ActionResult<APIResponseModel<string>>> AddToken([FromBody] AddOrModifyTokenModel model)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.AddToken(requestUserId, model);
}
#endregion
#region -token
[HttpPost("{tokenId}")]
[Authorize]
public async Task<ActionResult<APIResponseModel<string>>> ModifyToken(long tokenId, [FromBody] AddOrModifyTokenModel model)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.ModifyToken(requestUserId, tokenId, model);
}
#endregion
#region -token
[HttpDelete("{tokenId}")]
[Authorize]
public async Task<ActionResult<APIResponseModel<string>>> DeleteToken(long tokenId)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.DeleteToken(requestUserId, tokenId);
}
#endregion
#region -Token
[HttpGet]
[Authorize]
public async Task<ActionResult<APIResponseModel<CollectionResponse<TokenCacheItem>>>> QueryTokenCollection([Required] int page, [Required] int pageSize, string? token, long? tokenId, bool? efficient)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.QueryTokenCollection(page, pageSize, token, tokenId, efficient, requestUserId);
}
#endregion
#region -ID得Token
[HttpGet("{tokenId}")]
[Authorize]
public async Task<ActionResult<APIResponseModel<MJApiTokens>>> QueryTokenById(long tokenId)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.QueryTokenById(tokenId, requestUserId);
}
#endregion
#region -
[HttpGet]
[Authorize]
public async Task<ActionResult<APIResponseModel<CollectionResponse<MJApiTaskCollection>>>> QueryTaskCollection([Required] int page, [Required] int pageSize, string? thirdPartyTaskId, string? token, long? tokenId)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.QueryTaskCollection(requestUserId, page, pageSize, thirdPartyTaskId, token, tokenId);
}
#endregion
#region -
[HttpDelete("{timestamp}")]
[Authorize]
public async Task<ActionResult<APIResponseModel<string>>> DeleteMJTaskEarlyTimestamp(long timestamp)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.DeleteMJTaskEarlyTimestamp(requestUserId, timestamp);
}
#endregion
#region -ID的任务
[HttpDelete("{taskId}")]
[Authorize]
public async Task<ActionResult<APIResponseModel<string>>> DeleteMJTask(string taskId)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.DeleteMJTask(requestUserId, taskId);
}
#endregion
#region -token缓存
/// <summary>
/// 手动刷新Token缓存
/// </summary>
/// <param name="token">Token字符串</param>
/// <returns>刷新结果</returns>
[HttpPost("{token}")]
[Authorize]
public async Task<ActionResult<APIResponseModel<string>>> RefreshToken(string token)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.RefreshToken(requestUserId, token);
}
#endregion
#region -token
[HttpGet]
[Authorize]
public async Task<ActionResult<APIResponseModel<CollectionResponse<TokenCacheItem>>>> GetActiveTokens([FromQuery] int minutes = 5)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.GetActiveTokens(requestUserId, minutes);
}
#endregion
#region -token
[HttpGet]
[Authorize]
public async Task<ActionResult<APIResponseModel<string>>> RemoveNotActiveTokens([FromQuery] int minutes = 5)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.RemoveNotActiveTokens(requestUserId, minutes);
}
#endregion
#region
/// <summary>
/// 健康检查端点
/// </summary>
/// <returns>系统健康状态</returns>
[HttpGet]
[Authorize]
public async Task<ActionResult<APIResponseModel<object>>> GetHealth()
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.GetHealth(requestUserId);
}
#endregion
#region
[HttpGet]
[Authorize]
public async Task<ActionResult<APIResponseModel<TaskStatistics>>> GetDayTaskStatistics()
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _tokenManagementService.GetDayTaskStatistics(requestUserId);
}
#endregion
}
}

View File

@ -80,7 +80,7 @@ namespace LMS.service.Controllers
HttpOnly = true, HttpOnly = true,
Secure = true, // 如果使用 HTTPS Secure = true, // 如果使用 HTTPS
SameSite = SameSiteMode.None, SameSite = SameSiteMode.None,
Expires = DateTime.UtcNow.AddDays(7), Expires = BeijingTimeExtension.GetBeijingTime().AddDays(7),
}; };
Response.Cookies.Append("refreshToken", ((LoginResponse)apiResponse.Data).RefreshToken, cookieOptions); Response.Cookies.Append("refreshToken", ((LoginResponse)apiResponse.Data).RefreshToken, cookieOptions);
return apiResponse; return apiResponse;

View File

@ -0,0 +1,176 @@
using LMS.Common.Extensions;
using LMS.Tools.MJPackage;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace LMS.service.Extensions.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public class RateLimitAttribute : ActionFilterAttribute, IAsyncActionFilter
{
private string _token;
private DateTime _startTime;
private bool _concurrencyAcquired = false;
public RateLimitAttribute()
{
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
_startTime = BeijingTimeExtension.GetBeijingTime();
// 从服务容器获取需要的服务
var serviceProvider = context.HttpContext.RequestServices;
var logger = serviceProvider.GetRequiredService<ILogger<RateLimitAttribute>>();
var tokenService = serviceProvider.GetRequiredService<ITokenService>();
var usageTracker = serviceProvider.GetRequiredService<TokenUsageTracker>();
try
{
// 1. 获取Token
_token = context.HttpContext.Request.Headers.Authorization.ToString()
.Replace("Bearer ", "", StringComparison.OrdinalIgnoreCase)
.Trim();
if (string.IsNullOrEmpty(_token))
{
logger.LogWarning($"请求缺少Token, IP: {context.HttpContext.Connection.RemoteIpAddress}");
context.Result = new UnauthorizedObjectResult("Missing API token");
return;
}
// 2. 获取Token配置
var tokenConfig = await tokenService.GetTokenAsync(_token);
if (tokenConfig == null)
{
context.Result = new ObjectResult("Invalid token")
{
StatusCode = StatusCodes.Status403Forbidden
};
return;
}
// 3. 检查Token是不是到期
if (tokenConfig.ExpiresAt != null && tokenConfig.ExpiresAt < BeijingTimeExtension.GetBeijingTime())
{
context.Result = new ObjectResult("expired token")
{
StatusCode = StatusCodes.Status403Forbidden
};
return;
}
// 4. 判断当前Token得上一次使用时间是否超过了10分钟超过了重新从数据库获取
if (tokenConfig.LastActivityTime < BeijingTimeExtension.GetBeijingTime().AddMinutes(-10))
{
logger.LogInformation($"Token {_token} 上次活动时间超过10分钟重新从数据库获取配置");
tokenConfig = await tokenService.GetDatabaseTokenAsync(_token);
if (tokenConfig == null)
{
context.Result = new ObjectResult("Invalid or expired token")
{
StatusCode = StatusCodes.Status403Forbidden
};
return;
}
}
// 5. 检查日使用限制
if (tokenConfig.DailyLimit > 0 && tokenConfig.DailyUsage >= tokenConfig.DailyLimit)
{
logger.LogWarning($"Token日限制已达上限: {_token}, 当前使用: {tokenConfig.DailyUsage}, 限制: {tokenConfig.DailyLimit}");
context.Result = new ObjectResult("Daily limit exceeded")
{
StatusCode = StatusCodes.Status403Forbidden
};
return;
}
// 6. 检查总使用限制
if (tokenConfig.TotalLimit > 0 && tokenConfig.TotalUsage >= tokenConfig.TotalLimit)
{
logger.LogWarning($"Token总限制已达上限: {_token}, 当前使用: {tokenConfig.TotalUsage}, 限制: {tokenConfig.TotalLimit}");
context.Result = new ObjectResult("Total limit exceeded")
{
StatusCode = StatusCodes.Status403Forbidden
};
return;
}
// 7. 并发控制
var (maxCount, currentlyExecuting, available) = usageTracker.GetConcurrencyStatus(_token);
logger.LogInformation($"Token并发状态: {_token}, 最大: {maxCount}, 执行中: {currentlyExecuting}, 可用: {available}");
// 等待获取并发许可
_concurrencyAcquired = await usageTracker.WaitForConcurrencyPermitAsync(_token);
if (!_concurrencyAcquired)
{
logger.LogInformation($"Token并发限制超出: {_token}, 并发限制: {tokenConfig.ConcurrencyLimit}");
context.Result = new ObjectResult($"Concurrency limit exceeded (max: {tokenConfig.ConcurrencyLimit})")
{
StatusCode = StatusCodes.Status429TooManyRequests
};
return;
}
logger.LogInformation($"Token验证成功开始处理请求: {_token}, 并发限制: {tokenConfig.ConcurrencyLimit}");
if (string.IsNullOrWhiteSpace(tokenConfig.UseToken))
{
context.Result = new ObjectResult($"Token Error")
{
StatusCode = StatusCodes.Status401Unauthorized
};
return;
}
// 将新token存储在HttpContext.Items中
context.HttpContext.Items["UseToken"] = tokenConfig.UseToken;
context.HttpContext.Items["RequestToken"] = _token;
// 执行 Action
var executedContext = await next();
// 6. 更新使用计数 (仅成功请求)
if (executedContext.HttpContext.Response.StatusCode < 400)
{
tokenService.IncrementUsage(_token);
var duration = BeijingTimeExtension.GetBeijingTime() - _startTime;
logger.LogInformation($"请求处理成功: Token={_token}, 状态码={executedContext.HttpContext.Response.StatusCode}, 耗时={duration.TotalMilliseconds}ms");
}
else
{
logger.LogWarning($"请求处理失败: Token={_token}, 状态码={executedContext.HttpContext.Response.StatusCode}");
}
}
catch (Exception ex)
{
// 在异常情况下也要释放并发许可
if (_concurrencyAcquired)
{
usageTracker.ReleaseConcurrencyPermit(_token);
}
logger.LogError(ex, $"处理Token请求时发生错误: {_token}已释放Token许可");
context.Result = new ObjectResult("Internal server error")
{
StatusCode = StatusCodes.Status500InternalServerError
};
}
finally
{
// 7. 释放并发许可
//if (_concurrencyAcquired)
//{
// usageTracker.ReleaseConcurrencyPermit(_token);
// var newStatus = usageTracker.GetConcurrencyStatus(_token);
// logger.LogInformation($"Token并发许可已释放: {_token}, 执行中: {newStatus.currentlyExecuting}, 可用: {newStatus.available}");
//}
}
}
}
}

View File

@ -5,6 +5,7 @@ using LMS.Repository.Models.DB;
using LMS.service.Configuration; using LMS.service.Configuration;
using LMS.service.Configuration.InitConfiguration; using LMS.service.Configuration.InitConfiguration;
using LMS.service.Extensions.Middleware; using LMS.service.Extensions.Middleware;
using LMS.Tools.MJPackage;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Serilog; using Serilog;
@ -89,9 +90,6 @@ builder.Services.AddEndpointsApiExplorer();
// 注入Swagger // 注入Swagger
builder.Services.AddSwaggerService(); builder.Services.AddSwaggerService();
builder.Services.AddHostedService<RsaConfigurattions>();
builder.Services.AddHostedService<DatabaseConfiguration>();
var app = builder.Build(); var app = builder.Build();
@ -120,6 +118,7 @@ app.MapControllers();
// 在管道中使用IP速率限制中间件 // 在管道中使用IP速率限制中间件
app.UseIpRateLimiting(); app.UseIpRateLimiting();
// 添加动态权限的中间件
app.UseMiddleware<DynamicPermissionMiddleware>(); app.UseMiddleware<DynamicPermissionMiddleware>();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {

View File

@ -115,6 +115,16 @@ public class ForwardWordService(ApplicationDbContext context)
throw new Exception("参数错误"); throw new Exception("参数错误");
} }
// 判断请求的url是不是满足条件
if (string.IsNullOrEmpty(request.GptUrl))
{
throw new Exception("请求的url为空");
}
if (!request.GptUrl.StartsWith("https://ark.cn-beijing.volces.com") && !request.GptUrl.StartsWith("https://api.moonshot.cn") && !request.GptUrl.StartsWith("https://laitool.net") && !request.GptUrl.StartsWith("https://api.laitool.cc") && !request.GptUrl.StartsWith("https://laitool.cc"))
{
throw new Exception("请求的url不合法");
}
// 获取提示词预设 // 获取提示词预设
Prompt? prompt = await _context.Prompt.FirstOrDefaultAsync(x => x.PromptTypeId == request.PromptTypeId && x.Id == request.PromptId); Prompt? prompt = await _context.Prompt.FirstOrDefaultAsync(x => x.PromptTypeId == request.PromptTypeId && x.Id == request.PromptId);
if (prompt == null) if (prompt == null)

View File

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
namespace LMS.service.Service.MJPackage
{
public interface IMJPackageService
{
/// <summary>
/// mj 得 /mj/task/{id}/fetch 转发接口得实现
/// 内含重试请求,全部重试失败 会尝试充数据库中读取消息
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<ActionResult<object>> FetchTaskAsync(string id, string token);
Task<ActionResult> MJNotifyHookAsync(JsonElement model);
}
}

View File

@ -0,0 +1,26 @@
using LMS.Repository.DB;
using LMS.Repository.DTO;
using LMS.Repository.MJPackage;
using Microsoft.AspNetCore.Mvc;
namespace LMS.service.Service.MJPackage
{
public interface ITokenManagementService
{
Task<ActionResult<APIResponseModel<CollectionResponse<TokenAndTaskCollection>>>> QueryTokenTaskCollection(string token, int page, int pageSize, string? thirdPartyTaskId);
Task<ActionResult<APIResponseModel<TokenCacheItem>>> GetTokenItem(string token);
Task<ActionResult<APIResponseModel<string>>> AddToken(long requestUserId, AddOrModifyTokenModel model);
Task<ActionResult<APIResponseModel<string>>> ModifyToken(long requestUserId, long tokenId, AddOrModifyTokenModel model);
Task<ActionResult<APIResponseModel<string>>> DeleteToken(long requestUserId, long tokenId);
Task<ActionResult<APIResponseModel<CollectionResponse<MJApiTaskCollection>>>> QueryTaskCollection(long requestUserId, int page, int pageSize, string? thirdPartyTaskId, string? token, long? tokenId);
Task<ActionResult<APIResponseModel<CollectionResponse<TokenCacheItem>>>> QueryTokenCollection(int page, int pageSize, string? token, long? tokenId, bool? efficient, long requestUserId);
Task<ActionResult<APIResponseModel<string>>> DeleteMJTaskEarlyTimestamp(long requestUserId, long timestamp);
Task<ActionResult<APIResponseModel<string>>> DeleteMJTask(long requestUserId, string taskId);
Task<ActionResult<APIResponseModel<string>>> RefreshToken(long requestUserId, string token);
Task<ActionResult<APIResponseModel<CollectionResponse<TokenCacheItem>>>> GetActiveTokens(long requestUserId, int minutes);
Task<ActionResult<APIResponseModel<string>>> RemoveNotActiveTokens(long requestUserId, int minutes);
Task<ActionResult<APIResponseModel<object>>> GetHealth(long requestUserId);
Task<ActionResult<APIResponseModel<MJApiTokens>>> QueryTokenById(long tokenId, long requestUserId);
Task<ActionResult<APIResponseModel<TaskStatistics>>> GetDayTaskStatistics(long requestUserId);
}
}

View File

@ -0,0 +1,295 @@
using LMS.Common.Extensions;
using LMS.DAO;
using LMS.Repository.DB;
using LMS.Tools.MJPackage;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Net.Sockets;
using System.Text.Json;
using static Betalgo.Ranul.OpenAI.ObjectModels.StaticValues.AssistantsStatics.MessageStatics;
namespace LMS.service.Service.MJPackage
{
public class MJPackageService(ILogger<MJPackageService> logger, ApplicationDbContext dbContext, TokenUsageTracker usageTracker, ITaskConcurrencyManager taskConcurrencyManager, ITokenService tokenService) : IMJPackageService
{
private readonly ILogger<MJPackageService> _logger = logger;
private readonly ApplicationDbContext _dbContext = dbContext;
private readonly TokenUsageTracker _usageTracker = usageTracker;
private readonly ITaskConcurrencyManager _taskConcurrencyManager = taskConcurrencyManager;
private readonly ITokenService _tokenService = tokenService;
public async Task<ActionResult<object>> FetchTaskAsync(string id, string token)
{
// 参数验证
var validationResult = ValidateParameters(id, token);
if (validationResult != null) return validationResult;
// 获取UseToken
// 2. 获取Token配置
var tokenConfig = await _tokenService.GetTokenAsync(token);
if (tokenConfig == null || string.IsNullOrWhiteSpace(tokenConfig.UseToken))
{
return new UnauthorizedObjectResult(new { error = "无效或过期的Token" });
}
// 尝试原始API
var originResult = await TryOriginApiAsync(id);
if (originResult != null) return originResult;
// 尝试备用API
var backupResult = await TryBackupApiAsync(id, tokenConfig.UseToken);
if (backupResult != null) return backupResult;
// 从数据库获取缓存数据
return await GetTaskFromDatabaseAsync(id, token);
}
#region MJ Package
public async Task<ActionResult> MJNotifyHookAsync(JsonElement model)
{
try
{
string rawJson = model.GetRawText();
// 尝试获取ID字段
string mjId = string.Empty;
if (model.TryGetProperty("id", out var idElement))
{
mjId = idElement.ToString();
}
else if (model.TryGetProperty("Id", out var idElementCap))
{
mjId = idElementCap.ToString();
}
if (string.IsNullOrWhiteSpace(mjId))
{
_logger.LogWarning("MJNotifyHook: 接收到的回调数据中缺少ID");
return new BadRequestObjectResult("缺少ID");
}
// 获取任务
var mjTask = await _taskConcurrencyManager.GetTaskInfoByThirdPartyIdAsync(mjId);
if (mjTask == null)
{
return new NotFoundObjectResult($"未找到ID为 {mjId} 的任务");
}
// 尝试获取状态字段
string status = MJTaskStatus.SUBMITTED;
if (model.TryGetProperty("status", out var statusElement))
{
status = statusElement.GetString() ?? MJTaskStatus.SUBMITTED;
}
else if (model.TryGetProperty("Status", out var statusElementCap))
{
status = statusElementCap.GetString() ?? MJTaskStatus.SUBMITTED;
}
MJApiTasks mJApiTasks = new()
{
TaskId = mjTask.TaskId,
Token = mjTask.Token,
Status = status,
StartTime = mjTask.StartTime,
EndTime = null,
ThirdPartyTaskId = mjId,
Properties = rawJson // 或者直接存储 model
};
if (mjTask.Status == MJTaskStatus.SUCCESS || mjTask.Status == MJTaskStatus.FAILURE || mjTask.Status == MJTaskStatus.CANCEL)
{
// 当前任务已经被释放过了
// 开始修改数据
mJApiTasks.EndTime = BeijingTimeExtension.GetBeijingTime();
await _taskConcurrencyManager.UpdateTaskInDatabase(mJApiTasks);
return new OkObjectResult(null);
}
if (status == MJTaskStatus.SUCCESS || status == MJTaskStatus.FAILURE || status == MJTaskStatus.CANCEL)
{
mJApiTasks.EndTime = BeijingTimeExtension.GetBeijingTime();
_usageTracker.ReleaseConcurrencyPermit(mjTask.Token);
}
// 开始修改数据
await _taskConcurrencyManager.UpdateTaskInDatabase(mJApiTasks);
return new OkObjectResult(null);
}
catch (Exception ex)
{
_logger.LogError(ex, "MJNotifyHook 处理回调数据时发生异常");
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
}
}
#endregion
#region
private static ActionResult<object>? ValidateParameters(string id, string token)
{
if (string.IsNullOrWhiteSpace(id))
return new BadRequestObjectResult(new { error = "任务ID不能为空" });
if (string.IsNullOrWhiteSpace(token))
return new UnauthorizedObjectResult(new { error = "缺少授权Token" });
return null;
}
private async Task<ActionResult<object>?> TryOriginApiAsync(string id)
{
const string originUrl = "https://mjapi.bzu.cn/mj/task/{0}/fetch";
// 判断 原始token 不存在 直接 返回空
string orginToken = await _tokenService.GetOriginToken();
if (string.IsNullOrWhiteSpace(orginToken))
{
return null;
}
try
{
using var client = CreateHttpClient(orginToken, false);
var response = await client.GetAsync(string.Format(originUrl, id));
var content = await response.Content.ReadAsStringAsync();
if ((int)response.StatusCode == 204 || string.IsNullOrWhiteSpace(content))
{
throw new Exception("原始API返回204 No Content或空内容");
}
return new ContentResult
{
Content = content,
ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/json",
StatusCode = (int)response.StatusCode
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "原始API调用失败TaskId: {TaskId}准备尝试备用API", id);
return null;
}
}
private static bool IsRetriableException(Exception ex)
{
return ex is HttpRequestException ||
ex is TaskCanceledException ||
ex is SocketException;
}
private async Task<ActionResult<object>?> TryBackupApiAsync(string id, string useToken)
{
const string backupUrlTemplate = "https://api.laitool.cc/mj/task/{0}/fetch";
const int maxRetries = 3;
const int baseDelayMs = 1000;
using var client = CreateHttpClient($"Bearer sk-{useToken}", true);
var backupUrl = string.Format(backupUrlTemplate, id);
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
var response = await client.GetAsync(backupUrl);
var content = await response.Content.ReadAsStringAsync();
_logger.LogInformation("备用API调用成功TaskId: {TaskId}, Attempt: {Attempt}, StatusCode: {StatusCode}",
id, attempt, response.StatusCode);
return new ContentResult
{
Content = content,
ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/json",
StatusCode = (int)response.StatusCode
};
}
catch (Exception ex) when (IsRetriableException(ex))
{
if (attempt < maxRetries)
{
var delay = baseDelayMs * (int)Math.Pow(2, attempt - 1);
_logger.LogWarning(ex, "备用API调用失败TaskId: {TaskId}, Attempt: {Attempt}, 将在{Delay}ms后重试",
id, attempt, delay);
await Task.Delay(delay);
}
else
{
_logger.LogError(ex, "备用API调用最终失败TaskId: {TaskId}, MaxAttempts: {MaxAttempts}",
id, maxRetries);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "备用API调用发生不可重试异常TaskId: {TaskId}, Attempt: {Attempt}",
id, attempt);
break;
}
}
return null;
}
private async Task<ActionResult<object>> GetTaskFromDatabaseAsync(string id, string token)
{
try
{
// 这里需要根据您的数据库结构调整
// 假设您有一个任务表存储MJ任务的状态信息
var taskFromDb = await _dbContext.MJApiTasks // 替换为您实际的表名
.Where(x => x.ThirdPartyTaskId == id && x.Token == token) // 确保用户只能访问自己的任务
.FirstOrDefaultAsync();
if (taskFromDb != null && !string.IsNullOrWhiteSpace(taskFromDb.Properties))
{
return new OkObjectResult(taskFromDb.Properties);
}
else
{
return new ObjectResult(new
{
error = "服务暂时不可用",
message = "无法获取任务状态MJ服务连接失败且本地无缓存数据",
ThirdPartyTaskId = id,
})
{
StatusCode = 502
};
}
}
catch (Exception dbEx)
{
_logger.LogError(dbEx, $"从数据库获取任务数据时发生异常TaskId: {id}");
return new ObjectResult(new
{
error = "系统异常",
message = "获取任务状态失败,请稍后重试",
ThirdPartyTaskId = id
})
{
StatusCode = 500
};
}
}
private static HttpClient CreateHttpClient(string authorization, bool isBearerToken)
{
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", authorization);
client.Timeout = TimeSpan.FromSeconds(30);
return client;
}
#endregion
}
}

View File

@ -0,0 +1,878 @@
using LMS.Common.Extensions;
using LMS.DAO;
using LMS.DAO.UserDAO;
using LMS.Repository.DB;
using LMS.Repository.DTO;
using LMS.Repository.MJPackage;
using LMS.Tools.MJPackage;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Data;
using System.Diagnostics;
using static LMS.Common.Enums.ResponseCodeEnum;
namespace LMS.service.Service.MJPackage
{
public class TokenManagementService(
ApplicationDbContext dbContext,
TokenUsageTracker usageTracker,
ITokenService tokenService,
UserBasicDao userBasicDao,
ILogger<TokenManagementService> logger) : ITokenManagementService
{
private readonly ApplicationDbContext _dbContext = dbContext;
private readonly TokenUsageTracker _usageTracker = usageTracker;
private readonly ITokenService _tokenService = tokenService;
private readonly ILogger<TokenManagementService> _logger = logger;
private readonly UserBasicDao _userBasicDao = userBasicDao;
#region 使Token查询对应的任务-
/// <summary>
/// 查询任务集合 通过参数
/// </summary>
/// <param name="token"></param>
/// <param name="page"></param>
/// <param name="pageSize"></param>
/// <param name="thirdPartyTaskId"></param>
/// <returns></returns>
public async Task<ActionResult<APIResponseModel<CollectionResponse<TokenAndTaskCollection>>>> QueryTokenTaskCollection(string token, int page, int pageSize, string? thirdPartyTaskId)
{
try
{
if (string.IsNullOrWhiteSpace(token))
{
return APIResponseModel<CollectionResponse<TokenAndTaskCollection>>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不能为空");
}
TokenCacheItem? tokenCache = await _tokenService.GetDatabaseTokenAsync(token, true);
if (tokenCache == null)
{
return APIResponseModel<CollectionResponse<TokenAndTaskCollection>>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不存在或已过期");
}
// 处理token数据
TokenAndTaskCollection tokenCacheItem = new()
{
Id = tokenCache.Id,
Token = tokenCache.Token,
DailyLimit = tokenCache.DailyLimit,
TotalLimit = tokenCache.TotalLimit,
ConcurrencyLimit = tokenCache.ConcurrencyLimit,
CreatedAt = tokenCache.CreatedAt,
ExpiresAt = tokenCache.ExpiresAt,
DailyUsage = tokenCache.DailyUsage,
TotalUsage = tokenCache.TotalUsage,
LastActivityTime = tokenCache.LastActivityTime
};
var (maxCount, currentlyExecuting, available) = _usageTracker.GetConcurrencyStatus(tokenCache.Token);
tokenCacheItem.CurrentlyExecuting = currentlyExecuting;
// 开始处理 task 数据
IQueryable<MJApiTasks> query = _dbContext.MJApiTasks.Where(x => x.TokenId == tokenCacheItem.Id);
if (!string.IsNullOrWhiteSpace(thirdPartyTaskId))
{
query = query.Where(x => x.ThirdPartyTaskId == thirdPartyTaskId);
}
int total = await query.CountAsync();
List<MJApiTasks> mJApiTasks = await query.OrderByDescending(x => x.StartTime).Skip((page - 1) * pageSize).Take(pageSize).ToListAsync();
// 处理任务集合
// 将某个属性设置为空值
foreach (var task in mJApiTasks)
{
task.Token = "****";
}
tokenCacheItem.TaskCollections = mJApiTasks;
return APIResponseModel<CollectionResponse<TokenAndTaskCollection>>.CreateSuccessResponseModel(ResponseCode.Success, new CollectionResponse<TokenAndTaskCollection>
{
Total = total,
Collection = [tokenCacheItem],
Current = page
});
}
catch (Exception ex)
{
return APIResponseModel<CollectionResponse<TokenAndTaskCollection>>.CreateErrorResponseModel(Common.Enums.ResponseCodeEnum.ResponseCode.SystemError, ex.Message);
}
}
#endregion
#region Token是不是存在-
/// <summary>
/// 获取Token的数据
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task<ActionResult<APIResponseModel<TokenCacheItem>>> GetTokenItem(string token)
{
try
{
if (string.IsNullOrWhiteSpace(token))
{
return APIResponseModel<TokenCacheItem>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不能为空");
}
TokenCacheItem? tokenCache = await _tokenService.GetDatabaseTokenAsync(token, false);
if (tokenCache == null)
{
return APIResponseModel<TokenCacheItem>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不存在");
}
var (maxCount, currentlyExecuting, available) = _usageTracker.GetConcurrencyStatus(tokenCache.Token);
tokenCache.CurrentlyExecuting = currentlyExecuting;
tokenCache.UseToken = "*******************";
return APIResponseModel<TokenCacheItem>.CreateSuccessResponseModel(ResponseCode.Success, tokenCache);
}
catch (Exception ex)
{
return APIResponseModel<TokenCacheItem>.CreateErrorResponseModel(ResponseCode.SystemError, ex.Message);
}
}
#endregion
#region Token
public async Task<ActionResult<APIResponseModel<string>>> AddToken(long requestUserId, AddOrModifyTokenModel model)
{
try
{
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
// 是 超级管理员 直接添加数据就行
if (string.IsNullOrWhiteSpace(model.Token))
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不能为空");
}
if (string.IsNullOrWhiteSpace(model.UseToken))
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "使用Token不能为空");
}
// 判断token是不是存在
MJApiTokens? exitMJApiTokens = await _dbContext.MJApiTokens
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Token == model.Token || x.UseToken == model.UseToken);
if (exitMJApiTokens != null)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token或使用Token已存在请检查数据");
}
if (model.DailyLimit < 0 || model.TotalLimit < 0 || model.ConcurrencyLimit < 0 || model.UseDayCount < 0)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "限制参数不能小于0");
}
MJApiTokens mJApiTokens = new()
{
Token = model.Token,
UseToken = model.UseToken,
DailyLimit = model.DailyLimit,
TotalLimit = model.TotalLimit,
ConcurrencyLimit = model.ConcurrencyLimit,
CreatedAt = BeijingTimeExtension.GetBeijingTime(),
ExpiresAt = BeijingTimeExtension.GetBeijingTime().AddDays(model.UseDayCount)
};
// 开始新增
await _dbContext.MJApiTokens.AddAsync(mJApiTokens);
await _dbContext.SaveChangesAsync();
string message = $"Token添加成功: {model.Token}, 日限制: {model.DailyLimit}, 总限制: {model.TotalLimit}, 并发限制: {model.ConcurrencyLimit},有效期: {mJApiTokens.ExpiresAt}";
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, message);
}
catch (Exception ex)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, ex.Message);
}
}
#endregion
#region Token
public async Task<ActionResult<APIResponseModel<string>>> ModifyToken(long requestUserId, long tokenId, AddOrModifyTokenModel model)
{
try
{
if (string.IsNullOrWhiteSpace(model.Token))
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不能为空");
}
if (string.IsNullOrWhiteSpace(model.UseToken))
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "使用Token不能为空");
}
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
// 是 超级管理员 直接修改数据就行
MJApiTokens? mJApiTokens = await _dbContext.MJApiTokens
.Where(x => x.Id == tokenId).FirstOrDefaultAsync();
if (mJApiTokens == null)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不存在");
}
// 判断当前传入的token 是不是已经再别的地方使用了
MJApiTokens? exitMJApiTokens = await _dbContext.MJApiTokens
.AsNoTracking()
.FirstOrDefaultAsync(x => (x.Token == model.Token || x.UseToken == model.UseToken) && x.Id != tokenId);
if (exitMJApiTokens != null)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token或者是使用Token已存在请检查数据");
}
// 开始修改
mJApiTokens.Token = model.Token;
mJApiTokens.UseToken = model.UseToken;
if (model.DailyLimit > 0)
{
mJApiTokens.DailyLimit = model.DailyLimit;
}
if (model.TotalLimit > 0)
{
mJApiTokens.TotalLimit = model.TotalLimit;
}
if (model.ConcurrencyLimit > 0)
{
mJApiTokens.ConcurrencyLimit = model.ConcurrencyLimit;
}
if (model.UseDayCount > 0)
{
// 不是 -1 就需要重新设置到期时间
mJApiTokens.ExpiresAt = mJApiTokens.CreatedAt.AddDays(model.UseDayCount);
}
_dbContext.MJApiTokens.Update(mJApiTokens);
await _dbContext.SaveChangesAsync();
// 刷新一下内存中的限制
await RefreshTokenFromDatabaseAsync(model.Token);
string message = $"Token修改成功: {model.Token}, 日限制: {mJApiTokens.DailyLimit}, 总限制: {mJApiTokens.TotalLimit}, 并发限制: {mJApiTokens.ConcurrencyLimit},有效期: {mJApiTokens.ExpiresAt}请注意修改Token后之前的Token将失效。";
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, message);
}
catch (Exception ex)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, ex.Message);
}
}
#endregion
#region Token
public async Task<ActionResult<APIResponseModel<string>>> DeleteToken(long requestUserId, long tokenId)
{
var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
// 是 超级管理员 直接删除数据就行
// 先删除 使用数据
MJApiTokenUsage? tokenUsage = await _dbContext.MJApiTokenUsage
.Where(x => x.TokenId == tokenId)
.FirstOrDefaultAsync();
if (tokenUsage != null)
{
_dbContext.MJApiTokenUsage.Remove(tokenUsage);
}
// 再删除 Token 数据
MJApiTokens? mJApiTokens = await _dbContext.MJApiTokens
.Where(x => x.Id == tokenId).FirstOrDefaultAsync();
if (mJApiTokens == null)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不存在");
}
_dbContext.MJApiTokens.Remove(mJApiTokens);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
// 将内存中的token移除
_usageTracker.RemoveToken(mJApiTokens.Token);
_logger.LogInformation($"Token删除成功: {mJApiTokens.Token}ID: {tokenId}");
string message = $"Token删除成功: {mJApiTokens.Token}ID: {tokenId}请注意删除Token后之前的Token将失效。";
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, message);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, ex.Message);
}
}
#endregion
#region
public async Task<ActionResult<APIResponseModel<CollectionResponse<MJApiTaskCollection>>>> QueryTaskCollection(long requestUserId, int page, int pageSize, string? thirdPartyTaskId, string? token, long? tokenId)
{
try
{
// 权限检查
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<CollectionResponse<MJApiTaskCollection>>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
// 构建查询条件
IQueryable<MJApiTasks> query = _dbContext.MJApiTasks.AsQueryable();
// 根据不同参数组合构建查询
if (tokenId.HasValue)
{
query = query.Where(x => x.TokenId == tokenId);
}
if (!string.IsNullOrWhiteSpace(token))
{
// 如果只提供了token字符串
query = query.Where(x => x.Token.Contains(token));
}
// 添加第三方任务ID过滤
if (!string.IsNullOrWhiteSpace(thirdPartyTaskId))
{
query = query.Where(x => x.ThirdPartyTaskId == thirdPartyTaskId);
}
// 获取总数
int total = await query.CountAsync();
if (total == 0)
{
return APIResponseModel<CollectionResponse<MJApiTaskCollection>>.CreateSuccessResponseModel(
ResponseCode.Success, new CollectionResponse<MJApiTaskCollection>
{
Total = 0,
Collection = new List<MJApiTaskCollection>(),
Current = page
});
}
// 分页查询任务数据
var tasks = await query
.OrderByDescending(x => x.StartTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// 构建返回结果
var result = tasks.Select(task => new MJApiTaskCollection
{
TaskId = task.TaskId,
Token = task.Token,
TokenId = task.TokenId,
StartTime = task.StartTime,
EndTime = task.EndTime,
Status = task.Status,
ThirdPartyTaskId = task.ThirdPartyTaskId,
Properties = task.Properties,
}).ToList();
return APIResponseModel<CollectionResponse<MJApiTaskCollection>>.CreateSuccessResponseModel(
ResponseCode.Success, new CollectionResponse<MJApiTaskCollection>
{
Total = total,
Collection = result, // 直接赋值,不要用数组包装
Current = page
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "查询任务集合时发生错误 - 用户ID: {UserId}, Token: {Token}, TokenId: {TokenId}",
requestUserId, token, tokenId);
return APIResponseModel<CollectionResponse<MJApiTaskCollection>>.CreateErrorResponseModel(
ResponseCode.SystemError, "查询失败,请稍后重试");
}
}
#endregion
#region Token
public async Task<ActionResult<APIResponseModel<CollectionResponse<TokenCacheItem>>>> QueryTokenCollection(int page, int pageSize, string? token, long? tokenId, bool? efficient, long requestUserId)
{
try
{
// 权限检查
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<CollectionResponse<TokenCacheItem>>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
var currentUtcTime = BeijingTimeExtension.GetBeijingTime(); // 2025-06-09 12:20:40
// 构建基础查询
var query = from t in _dbContext.MJApiTokens
join u in _dbContext.MJApiTokenUsage on t.Id equals u.TokenId into tokenUsage
from usage in tokenUsage.DefaultIfEmpty()
select new
{
Token = t,
Usage = usage
};
// 应用过滤条件
if (!string.IsNullOrWhiteSpace(token))
{
query = query.Where(x => x.Token.Token == token);
}
if (tokenId.HasValue)
{
query = query.Where(x => x.Token.Id == tokenId.Value);
}
if (efficient.HasValue)
{
if (efficient.Value)
{
// 查询有效得token
query = query.Where(x =>
(x.Token.ExpiresAt == null || x.Token.ExpiresAt > currentUtcTime));
}
else
{
// 查询无效或不活跃的Token
query = query.Where(x =>
(x.Token.ExpiresAt != null && x.Token.ExpiresAt <= currentUtcTime));
}
}
// 获取总数
var total = await query.CountAsync();
if (total == 0)
{
return APIResponseModel<CollectionResponse<TokenCacheItem>>.CreateSuccessResponseModel(
ResponseCode.Success, new CollectionResponse<TokenCacheItem>
{
Total = 0,
Collection = [],
Current = page
});
}
// 分页查询并投影到TokenCacheItem
var tokenItems = await query
.OrderByDescending(x => x.Token.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(x => new TokenCacheItem
{
Id = x.Token.Id,
Token = x.Token.Token,
UseToken = x.Token.UseToken,
DailyLimit = x.Token.DailyLimit,
TotalLimit = x.Token.TotalLimit,
ConcurrencyLimit = x.Token.ConcurrencyLimit,
CreatedAt = x.Token.CreatedAt,
ExpiresAt = x.Token.ExpiresAt,
DailyUsage = x.Usage != null ? x.Usage.DailyUsage : 0,
TotalUsage = x.Usage != null ? x.Usage.TotalUsage : 0,
LastActivityTime = x.Usage != null ? x.Usage.LastActivityAt : x.Token.CreatedAt,
HistoryUse = x.Usage != null ? x.Usage.HistoryUse : ""
})
.ToListAsync();
for (int i = 0; i < tokenItems.Count; i++)
{
var tokenItem = tokenItems[i];
var (maxCount, currentlyExecuting, available) = usageTracker.GetConcurrencyStatus(tokenItem.Token);
tokenItems[i].CurrentlyExecuting = currentlyExecuting;
}
_logger?.LogInformation($"✅ Token查询完成, 总数: {total}, 当前页: {page}, 页大小: {pageSize}, 返回: {tokenItems.Count} 条");
return APIResponseModel<CollectionResponse<TokenCacheItem>>.CreateSuccessResponseModel(
ResponseCode.Success, new CollectionResponse<TokenCacheItem>
{
Total = total,
Collection = tokenItems,
Current = page
});
}
catch (Exception ex)
{
_logger?.LogError(ex, "❌ 查询Token集合时发生错误 - 用户: qiang-lo, UTC时间: 2025-06-09 12:20:40");
return APIResponseModel<CollectionResponse<TokenCacheItem>>.CreateErrorResponseModel(
ResponseCode.SystemError, "查询失败,请稍后重试");
}
}
#endregion
#region
public async Task<ActionResult<APIResponseModel<string>>> DeleteMJTaskEarlyTimestamp(long requestUserId, long timestamp)
{
const string operationName = "删除早期MJ任务";
try
{
// 1. 参数验证
if (!IsValidTimestamp(timestamp))
{
_logger.LogWarning($"{operationName} - 无效的时间戳: {timestamp}");
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "时间戳格式错误");
}
// 2. 权限检查
if (!await _userBasicDao.CheckUserIsSuperAdmin(requestUserId))
{
_logger.LogWarning($"{operationName} - 用户 {requestUserId} 无权限执行此操作");
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
// 3. 时间戳转换
DateTime targetDateTime = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).DateTime;
_logger.LogInformation($"{operationName} - 开始删除早于 {targetDateTime:yyyy-MM-dd HH:mm:ss} 的任务");
DateTime beijingTargetDateTime = targetDateTime.AddHours(8); // 转换为北京时间
// 4. 使用批量删除,避免加载到内存
int deletedCount = await _dbContext.MJApiTasks
.Where(x => x.StartTime <= beijingTargetDateTime)
.ExecuteDeleteAsync(); // 使用 EF Core 7+ 的批量删除
// 5. 记录结果
string resultMessage = deletedCount == 0
? "没有找到符合条件的任务"
: $"成功删除 {deletedCount} 个任务";
_logger.LogInformation($"{operationName} - {resultMessage},目标时间: {targetDateTime:yyyy-MM-dd HH:mm:ss}");
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, resultMessage);
}
catch (ArgumentOutOfRangeException ex)
{
_logger.LogError(ex, $"{operationName} - 时间戳转换失败: {timestamp}");
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "时间戳格式错误");
}
catch (Exception ex)
{
_logger.LogError(ex, $"{operationName} - 执行失败,时间戳: {timestamp}");
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, "系统错误,请稍后重试");
}
}
#endregion
#region ID的任务
public async Task<ActionResult<APIResponseModel<string>>> DeleteMJTask(long requestUserId, string taskId)
{
try
{
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
// 1. 参数验证
if (string.IsNullOrWhiteSpace(taskId))
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "任务ID不能为空");
}
// 2. 查找任务
var task = await _dbContext.MJApiTasks
.Where(x => x.TaskId == taskId)
.FirstOrDefaultAsync();
if (task == null)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "任务不存在");
}
// 3. 删除任务
_dbContext.MJApiTasks.Remove(task);
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"删除指定ID的任务成功任务ID: {taskId}");
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, "任务删除成功");
}
catch (Exception ex)
{
_logger.LogError(ex, $"删除指定ID的任务失败任务ID: {taskId}, 失败原因:{ex.Message}");
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, "删除任务失败,请稍后重试");
}
}
#endregion
#region Token
public async Task<ActionResult<APIResponseModel<string>>> RefreshToken(long requestUserId, string token)
{
try
{
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
// 1. 参数验证
if (string.IsNullOrWhiteSpace(token))
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不能为空");
}
TokenCacheItem tokenCache = await RefreshTokenFromDatabaseAsync(token);
if (tokenCache == null)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不存在");
}
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, $"Token刷新成功: {token}, 并发限制: {tokenCache.ConcurrencyLimit},日出图限制: {tokenCache.DailyLimit}, 当前日出图总数: {tokenCache.DailyUsage}, 总出图量: {tokenCache.TotalUsage}");
}
catch (Exception ex)
{
string message = $"手动刷新Token失败Token: {token}, 失败原因:{ex.Message}";
_logger.LogError(ex, message);
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, message);
}
}
#endregion
#region Token
public async Task<ActionResult<APIResponseModel<CollectionResponse<TokenCacheItem>>>> GetActiveTokens(long requestUserId, int minutes)
{
try
{
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<CollectionResponse<TokenCacheItem>>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
var threshold = TimeSpan.FromMinutes(minutes);
List<TokenCacheItem> activeTokens = _usageTracker.GetActiveTokens(threshold);
return APIResponseModel<CollectionResponse<TokenCacheItem>>.CreateSuccessResponseModel(ResponseCode.Success, new CollectionResponse<TokenCacheItem>
{
Total = activeTokens.Count,
Collection = activeTokens,
Current = 1 // 这里可以设置为1因为我们只返回一页
});
}
catch (Exception ex)
{
string message = $"获取活跃的Token失败错误信息{ex.Message}";
_logger.LogError(ex, message);
return APIResponseModel<CollectionResponse<TokenCacheItem>>.CreateErrorResponseModel(ResponseCode.SystemError, message);
}
}
#endregion
#region Token
public async Task<ActionResult<APIResponseModel<string>>> RemoveNotActiveTokens(long requestUserId, int minutes)
{
try
{
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
var threshold = TimeSpan.FromMinutes(minutes);
var (activateTokenCount, notActivateTokenCount) = _usageTracker.RemoveNotActiveTokens(threshold);
string message = $"删除不活跃得 Token 数: {notActivateTokenCount},阈值: {minutes}分钟";
_logger.LogInformation(message);
return APIResponseModel<string>.CreateSuccessResponseModel(ResponseCode.Success, message);
}
catch (Exception ex)
{
string message = $"移除不活跃的Token失败错误信息{ex.Message}";
_logger.LogError(ex, message);
return APIResponseModel<string>.CreateErrorResponseModel(ResponseCode.SystemError, message);
}
}
#endregion
#region
public async Task<ActionResult<APIResponseModel<object>>> GetHealth(long requestUserId)
{
try
{
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
var stats = _usageTracker.GetCacheStats();
var now = BeijingTimeExtension.GetBeijingTime();
return APIResponseModel<object>.CreateSuccessResponseModel(ResponseCode.Success, new
{
Status = "Healthy",
Timestamp = now,
CacheStats = stats,
Uptime = now - Process.GetCurrentProcess().StartTime.ToUniversalTime()
});
}
catch (Exception ex)
{
_logger.LogError(ex, "获取系统健康状态失败");
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.SystemError, "获取系统健康状态失败");
}
}
#endregion
#region ID得Token
/// <summary>
/// 获取指定ID得Token
/// </summary>
/// <param name="tokenId"></param>
/// <param name="requestUserId"></param>
/// <returns></returns>
public async Task<ActionResult<APIResponseModel<MJApiTokens>>> QueryTokenById(long tokenId, long requestUserId)
{
try
{
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<MJApiTokens>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
MJApiTokens? mjApiTokens = await _dbContext.MJApiTokens
.AsNoTracking()
.Where(x => x.Id == tokenId)
.FirstOrDefaultAsync();
if (mjApiTokens == null)
{
return APIResponseModel<MJApiTokens>.CreateErrorResponseModel(ResponseCode.ParameterError, "Token不存在");
}
return APIResponseModel<MJApiTokens>.CreateSuccessResponseModel(ResponseCode.Success, mjApiTokens);
}
catch (Exception ex)
{
_logger.LogError(ex, "查询Token失败TokenId: {TokenId}, 错误信息: {Message}", tokenId, ex.Message);
return APIResponseModel<MJApiTokens>.CreateErrorResponseModel(ResponseCode.SystemError, "查询Token失败请稍后重试");
}
}
#endregion
#region
public async Task<ActionResult<APIResponseModel<TaskStatistics>>> GetDayTaskStatistics(long requestUserId)
{
try
{
bool isSuperAdmin = await _userBasicDao.CheckUserIsSuperAdmin(requestUserId);
if (!isSuperAdmin)
{
return APIResponseModel<TaskStatistics>.CreateErrorResponseModel(ResponseCode.NotPermissionAction);
}
// 获取当前日期
DateTime today = BeijingTimeExtension.GetBeijingTime().Date; // 获取今天的日期,不包含时间部分
// 查询今天的任务
var tasks = await _dbContext.MJApiTasks
.Where(x => x.StartTime.Date == today)
.OrderByDescending(x => x.StartTime)
.ToListAsync();
if (tasks.Count == 0)
{
return APIResponseModel<TaskStatistics>.CreateSuccessResponseModel(
ResponseCode.Success, new TaskStatistics());
}
// 统计任务数量
int totalTasks = tasks.Count;
// 统计成功任务数量
int successfulTasks = tasks.Count(x => x.Status == MJTaskStatus.SUCCESS);
// 统计失败任务数量
int failedTasks = tasks.Count(x => x.Status == MJTaskStatus.FAILURE || x.Status == MJTaskStatus.CANCEL);
// 剩下的都是再执行的
int inProgressTasks = totalTasks - successfulTasks - failedTasks;
return APIResponseModel<TaskStatistics>.CreateSuccessResponseModel(ResponseCode.Success, new TaskStatistics
{
TotalTasks = totalTasks,
CompletedTasks = successfulTasks,
FailedTasks = failedTasks,
InProgressTasks = inProgressTasks
});
}
catch (Exception ex)
{
_logger.LogError(ex, "获取日统计数据失败,错误信息: {Message}", ex.Message);
return APIResponseModel<TaskStatistics>.CreateErrorResponseModel(ResponseCode.SystemError, "获取日统计数据失败,请稍后重试");
}
}
#endregion
/// <summary>
/// 从数据库重新加载Token到内存缓存平滑更新
/// </summary>
private async Task<TokenCacheItem> RefreshTokenFromDatabaseAsync(string token)
{
_logger.LogDebug($"从数据库平滑刷新Token到内存: {token}");
try
{
TokenCacheItem? tokenItem = await _tokenService.GetDatabaseTokenAsync(token);
if (tokenItem == null)
{
// 将内存的中的这个token删掉
_usageTracker.RemoveToken(token);
throw new Exception($"Token不存在");
}
// 3. 平滑更新到内存缓存(这里会自动处理并发限制的平滑调整)
_usageTracker.AddOrUpdateTokenAsync(tokenItem);
_logger.LogInformation($"Token平滑刷新成功: {token}, 并发限制: {tokenItem.ConcurrencyLimit}");
return tokenItem;
}
catch (Exception ex)
{
_logger.LogError(ex, $"平滑刷新Token失败: {token} " + ex.Message);
throw;
}
}
// 辅助方法:验证时间戳
private static bool IsValidTimestamp(long timestamp)
{
// 检查时间戳是否在合理范围内
// Unix时间戳最小值1970-01-01和最大值约2038年或更远
const long minTimestamp = 0;
const long maxTimestamp = 253402300799999; // 9999-12-31的毫秒时间戳
return timestamp >= minTimestamp && timestamp <= maxTimestamp;
}
}
}

View File

@ -9,8 +9,6 @@ using LMS.Repository.Models.DB;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using static LMS.Common.Enums.MachineEnum; using static LMS.Common.Enums.MachineEnum;
using static LMS.Common.Enums.ResponseCodeEnum; using static LMS.Common.Enums.ResponseCodeEnum;
using static LMS.Repository.DTO.MachineDto; using static LMS.Repository.DTO.MachineDto;

View File

@ -1,25 +1,22 @@
using AutoMapper; using AutoMapper;
using LinqKit;
using LMS.Common.Dictionary; using LMS.Common.Dictionary;
using LMS.Common.Enums; using LMS.Common.Enums;
using LMS.Common.Extensions;
using LMS.Common.Templates; using LMS.Common.Templates;
using LMS.DAO; using LMS.DAO;
using LMS.DAO.UserDAO; using LMS.DAO.UserDAO;
using LMS.Repository.DB;
using LMS.Repository.DTO; using LMS.Repository.DTO;
using LMS.Repository.DTO.OptionDto;
using LMS.Repository.Models.DB; using LMS.Repository.Models.DB;
using LMS.Repository.Options; using LMS.Repository.Options;
using LMS.service.Extensions.Mail; using LMS.service.Extensions.Mail;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using System.Linq.Dynamic.Core;
using System.Linq;
using static LMS.Common.Enums.ResponseCodeEnum; using static LMS.Common.Enums.ResponseCodeEnum;
using Options = LMS.Repository.DB.Options; using Options = LMS.Repository.DB.Options;
using System.Linq.Dynamic.Core;
using LinqKit;
using LMS.Repository.DTO.OptionDto;
using LMS.Common.Extensions;
namespace LMS.service.Service namespace LMS.service.Service
{ {

View File

@ -68,6 +68,6 @@
} }
] ]
}, },
"Version": "1.1.1", "Version": "1.1.2",
"AllowedHosts": "*" "AllowedHosts": "*"
} }

41
SQL/v1.1.2/MJApiTasks.sql Normal file
View File

@ -0,0 +1,41 @@
/*
Navicat Premium Dump SQL
Source Server : 亿
Source Server Type : MySQL
Source Server Version : 80018 (8.0.18)
Source Host : yisurds-66dc0b453c05d4.rds.ysydb1.com:14080
Source Schema : LMS_TEST
Target Server Type : MySQL
Target Server Version : 80018 (8.0.18)
File Encoding : 65001
Date: 11/06/2025 13:55:05
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for MJApiTasks
-- ----------------------------
DROP TABLE IF EXISTS `MJApiTasks`;
CREATE TABLE `MJApiTasks` (
`TaskId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`Token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`StartTime` datetime NOT NULL,
`EndTime` datetime NULL DEFAULT NULL,
`Status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '0' COMMENT '0=Pending, 1=NotStart, 2=Running, 3=Completed, 4=Failed, 5=Timeout',
`ThirdPartyTaskId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`Properties` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`CreatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`UpdatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`TaskId`) USING BTREE,
INDEX `idx_token`(`Token` ASC) USING BTREE,
INDEX `idx_third_party_task_id`(`ThirdPartyTaskId` ASC) USING BTREE,
INDEX `idx_status`(`Status` ASC) USING BTREE,
INDEX `idx_start_time`(`StartTime` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '异步任务表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -0,0 +1,36 @@
/*
Navicat Premium Dump SQL
Source Server : 亿
Source Server Type : MySQL
Source Server Version : 80018 (8.0.18)
Source Host : yisurds-66dc0b453c05d4.rds.ysydb1.com:14080
Source Schema : LMS_TEST
Target Server Type : MySQL
Target Server Version : 80018 (8.0.18)
File Encoding : 65001
Date: 11/06/2025 13:55:58
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for MJApiTokenUsage
-- ----------------------------
DROP TABLE IF EXISTS `MJApiTokenUsage`;
CREATE TABLE `MJApiTokenUsage` (
`TokenId` bigint(20) NOT NULL,
`Date` date NOT NULL,
`DailyUsage` int(11) NOT NULL DEFAULT 0 COMMENT '当日使用量',
`TotalUsage` int(11) NOT NULL DEFAULT 0 COMMENT '累计使用量',
`LastActivityAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后活动时间',
`HistoryUse` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '使用记录',
PRIMARY KEY (`TokenId`) USING BTREE,
INDEX `IdxLastActivity`(`LastActivityAt` ASC) USING BTREE,
CONSTRAINT `MJApiTokenUsage_ibfk_1` FOREIGN KEY (`TokenId`) REFERENCES `MJApiTokens` (`Id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '令牌使用统计表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -0,0 +1,37 @@
/*
Navicat Premium Dump SQL
Source Server : 亿
Source Server Type : MySQL
Source Server Version : 80018 (8.0.18)
Source Host : yisurds-66dc0b453c05d4.rds.ysydb1.com:14080
Source Schema : LMS_TEST
Target Server Type : MySQL
Target Server Version : 80018 (8.0.18)
File Encoding : 65001
Date: 11/06/2025 13:55:48
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for MJApiTokens
-- ----------------------------
DROP TABLE IF EXISTS `MJApiTokens`;
CREATE TABLE `MJApiTokens` (
`Id` bigint(20) NOT NULL AUTO_INCREMENT,
`Token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'API令牌',
`DailyLimit` int(11) NULL DEFAULT 0 COMMENT '日限制(0=无限制)',
`TotalLimit` int(11) NULL DEFAULT 0 COMMENT '总限制(0=无限制)',
`ConcurrencyLimit` int(11) NOT NULL DEFAULT 1 COMMENT '并发限制',
`CreatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`ExpiresAt` datetime NULL DEFAULT NULL COMMENT '到期时间(NULL=永不过期)',
PRIMARY KEY (`Id`) USING BTREE,
UNIQUE INDEX `UkToken`(`Token` ASC) USING BTREE,
INDEX `IdxExpires`(`ExpiresAt` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'API令牌表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;