V 1.1.2 新增了生图包 以及各种转发和接口
This commit is contained in:
parent
c07369c297
commit
3514cf53f8
@ -13,14 +13,40 @@
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将UTC时间转换为北京时间
|
||||
/// 智能转换时间为北京时间
|
||||
/// 如果是UTC时间则转换,否则直接返回
|
||||
/// </summary>
|
||||
/// <param name="utcTime"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime TransferUtcToBeijingTime(DateTime utcTime)
|
||||
/// <param name="dateTime">输入的时间</param>
|
||||
/// <returns>北京时间</returns>
|
||||
public static DateTime TransferUtcToBeijingTime(DateTime dateTime)
|
||||
{
|
||||
return TimeZoneInfo.ConvertTimeFromUtc(utcTime,
|
||||
TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"));
|
||||
// 只有UTC时间才需要转换
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
|
||||
|
||||
using LMS.Repository.DB;
|
||||
using LMS.Repository.MJPackage;
|
||||
using LMS.Repository.Models.DB;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -46,6 +47,12 @@ namespace LMS.DAO
|
||||
|
||||
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)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
@ -81,6 +88,30 @@ namespace LMS.DAO
|
||||
)
|
||||
.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
LMS.Repository/DB/MJApiTasks.cs
Normal file
41
LMS.Repository/DB/MJApiTasks.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
22
LMS.Repository/DB/MJApiTokenUsage.cs
Normal file
22
LMS.Repository/DB/MJApiTokenUsage.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
29
LMS.Repository/DB/MJApiTokens.cs
Normal file
29
LMS.Repository/DB/MJApiTokens.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
43
LMS.Repository/MJPackage/AddOrModifyTokenModel.cs
Normal file
43
LMS.Repository/MJPackage/AddOrModifyTokenModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
74
LMS.Repository/MJPackage/MJSubmitImageModel.cs
Normal file
74
LMS.Repository/MJPackage/MJSubmitImageModel.cs
Normal 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>
|
||||
/// 账号是否 remix(Midjourney Remix)
|
||||
/// </summary>
|
||||
public bool? Remix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 账号是否 remix(Nijiourney 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
|
||||
}
|
||||
}
|
||||
39
LMS.Repository/MJPackage/MJTaskCallbackModel.cs
Normal file
39
LMS.Repository/MJPackage/MJTaskCallbackModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
18
LMS.Repository/MJPackage/SyncResult.cs
Normal file
18
LMS.Repository/MJPackage/SyncResult.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
29
LMS.Repository/MJPackage/TokenCacheItem.cs
Normal file
29
LMS.Repository/MJPackage/TokenCacheItem.cs
Normal 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; } = [];
|
||||
}
|
||||
}
|
||||
11
LMS.Repository/MJPackage/TokenCacheStats.cs
Normal file
11
LMS.Repository/MJPackage/TokenCacheStats.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
20
LMS.Repository/MJPackage/TokenQueryResult.cs
Normal file
20
LMS.Repository/MJPackage/TokenQueryResult.cs
Normal 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; } // 历史使用记录
|
||||
}
|
||||
}
|
||||
12
LMS.Repository/MJPackage/TokenUsageData.cs
Normal file
12
LMS.Repository/MJPackage/TokenUsageData.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
19
LMS.Tools/MJPackage/ITaskConcurrencyManager.cs
Normal file
19
LMS.Tools/MJPackage/ITaskConcurrencyManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
10
LMS.Tools/MJPackage/ITaskService.cs
Normal file
10
LMS.Tools/MJPackage/ITaskService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
22
LMS.Tools/MJPackage/ITokenService.cs
Normal file
22
LMS.Tools/MJPackage/ITokenService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
216
LMS.Tools/MJPackage/TaskConcurrencyManager.cs
Normal file
216
LMS.Tools/MJPackage/TaskConcurrencyManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
147
LMS.Tools/MJPackage/TaskService.cs
Normal file
147
LMS.Tools/MJPackage/TaskService.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
114
LMS.Tools/MJPackage/TaskStatusCheckService.cs
Normal file
114
LMS.Tools/MJPackage/TaskStatusCheckService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
LMS.Tools/MJPackage/TokenResetService.cs
Normal file
24
LMS.Tools/MJPackage/TokenResetService.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
352
LMS.Tools/MJPackage/TokenService.cs
Normal file
352
LMS.Tools/MJPackage/TokenService.cs
Normal 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} 的历史记录时发生错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
198
LMS.Tools/MJPackage/TokenSyncService.cs
Normal file
198
LMS.Tools/MJPackage/TokenSyncService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
504
LMS.Tools/MJPackage/TokenUsageTracker.cs
Normal file
504
LMS.Tools/MJPackage/TokenUsageTracker.cs
Normal 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为空值,可能请求原始的请求不可用!!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,62 +1,100 @@
|
||||
using LMS.Tools.TaskScheduler;
|
||||
using LMS.Tools.MJPackage;
|
||||
using LMS.Tools.TaskScheduler;
|
||||
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
|
||||
{
|
||||
|
||||
// 配置作业
|
||||
var jobKey = new JobKey("MonthlyTask", "DefaultGroup");
|
||||
|
||||
// 方法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 =>
|
||||
return TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai");
|
||||
}
|
||||
catch
|
||||
{
|
||||
options.WaitForJobsToComplete = true;
|
||||
});
|
||||
|
||||
// 注册作业
|
||||
services.AddTransient<ResetUserFreeCount>();
|
||||
return TimeZoneInfo.CreateCustomTimeZone(
|
||||
"China_Custom",
|
||||
new TimeSpan(8, 0, 0),
|
||||
"China Custom Time",
|
||||
"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分钟执行一次
|
||||
}
|
||||
}
|
||||
@ -5,12 +5,14 @@ using LMS.DAO.UserDAO;
|
||||
using LMS.service.Configuration.InitConfiguration;
|
||||
using LMS.service.Extensions.Mail;
|
||||
using LMS.service.Service;
|
||||
using LMS.service.Service.MJPackage;
|
||||
using LMS.service.Service.Other;
|
||||
using LMS.service.Service.PermissionService;
|
||||
using LMS.service.Service.PromptService;
|
||||
using LMS.service.Service.RoleService;
|
||||
using LMS.service.Service.SoftwareService;
|
||||
using LMS.service.Service.UserService;
|
||||
using LMS.Tools.MJPackage;
|
||||
|
||||
namespace Lai_server.Configuration
|
||||
{
|
||||
@ -38,6 +40,8 @@ namespace Lai_server.Configuration
|
||||
services.AddScoped<SoftwareService>();
|
||||
services.AddScoped<MachineAuthorizationService>();
|
||||
services.AddScoped<DataInfoService>();
|
||||
services.AddScoped<ITokenManagementService, TokenManagementService>();
|
||||
services.AddScoped<IMJPackageService, MJPackageService>();
|
||||
|
||||
// 注入 DAO
|
||||
services.AddScoped<UserBasicDao>();
|
||||
@ -53,6 +57,20 @@ namespace Lai_server.Configuration
|
||||
|
||||
// 添加分布式缓存(用于存储验证码)
|
||||
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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
LMS.service/Controllers/MJPackageController.cs
Normal file
115
LMS.service/Controllers/MJPackageController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
212
LMS.service/Controllers/TokenManagementController.cs
Normal file
212
LMS.service/Controllers/TokenManagementController.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@ -80,7 +80,7 @@ namespace LMS.service.Controllers
|
||||
HttpOnly = true,
|
||||
Secure = true, // 如果使用 HTTPS
|
||||
SameSite = SameSiteMode.None,
|
||||
Expires = DateTime.UtcNow.AddDays(7),
|
||||
Expires = BeijingTimeExtension.GetBeijingTime().AddDays(7),
|
||||
};
|
||||
Response.Cookies.Append("refreshToken", ((LoginResponse)apiResponse.Data).RefreshToken, cookieOptions);
|
||||
return apiResponse;
|
||||
|
||||
176
LMS.service/Extensions/Attributes/RateLimitAttribute.cs
Normal file
176
LMS.service/Extensions/Attributes/RateLimitAttribute.cs
Normal 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}");
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ using LMS.Repository.Models.DB;
|
||||
using LMS.service.Configuration;
|
||||
using LMS.service.Configuration.InitConfiguration;
|
||||
using LMS.service.Extensions.Middleware;
|
||||
using LMS.Tools.MJPackage;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
@ -89,9 +90,6 @@ builder.Services.AddEndpointsApiExplorer();
|
||||
// 注入Swagger
|
||||
builder.Services.AddSwaggerService();
|
||||
|
||||
builder.Services.AddHostedService<RsaConfigurattions>();
|
||||
builder.Services.AddHostedService<DatabaseConfiguration>();
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@ -120,6 +118,7 @@ app.MapControllers();
|
||||
// 在管道中使用IP速率限制中间件
|
||||
app.UseIpRateLimiting();
|
||||
|
||||
// 添加动态权限的中间件
|
||||
app.UseMiddleware<DynamicPermissionMiddleware>();
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
|
||||
@ -115,6 +115,16 @@ public class ForwardWordService(ApplicationDbContext context)
|
||||
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);
|
||||
if (prompt == null)
|
||||
|
||||
18
LMS.service/Service/MJPackage/IMJPackageService.cs
Normal file
18
LMS.service/Service/MJPackage/IMJPackageService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
26
LMS.service/Service/MJPackage/ITokenManagementService.cs
Normal file
26
LMS.service/Service/MJPackage/ITokenManagementService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
295
LMS.service/Service/MJPackage/MJPackageService.cs
Normal file
295
LMS.service/Service/MJPackage/MJPackageService.cs
Normal 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
|
||||
}
|
||||
}
|
||||
878
LMS.service/Service/MJPackage/TokenManagementService.cs
Normal file
878
LMS.service/Service/MJPackage/TokenManagementService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,8 +9,6 @@ using LMS.Repository.Models.DB;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using static LMS.Common.Enums.MachineEnum;
|
||||
using static LMS.Common.Enums.ResponseCodeEnum;
|
||||
using static LMS.Repository.DTO.MachineDto;
|
||||
|
||||
@ -1,25 +1,22 @@
|
||||
using AutoMapper;
|
||||
using LinqKit;
|
||||
using LMS.Common.Dictionary;
|
||||
using LMS.Common.Enums;
|
||||
using LMS.Common.Extensions;
|
||||
using LMS.Common.Templates;
|
||||
using LMS.DAO;
|
||||
using LMS.DAO.UserDAO;
|
||||
using LMS.Repository.DB;
|
||||
using LMS.Repository.DTO;
|
||||
using LMS.Repository.DTO.OptionDto;
|
||||
using LMS.Repository.Models.DB;
|
||||
using LMS.Repository.Options;
|
||||
using LMS.service.Extensions.Mail;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using static LMS.Common.Enums.ResponseCodeEnum;
|
||||
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
|
||||
{
|
||||
|
||||
@ -68,6 +68,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Version": "1.1.1",
|
||||
"Version": "1.1.2",
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
41
SQL/v1.1.2/MJApiTasks.sql
Normal file
41
SQL/v1.1.2/MJApiTasks.sql
Normal 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;
|
||||
36
SQL/v1.1.2/MJApiTokenUsage.sql
Normal file
36
SQL/v1.1.2/MJApiTokenUsage.sql
Normal 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;
|
||||
37
SQL/v1.1.2/MJApiTokens.sql
Normal file
37
SQL/v1.1.2/MJApiTokens.sql
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user