V 1.1.2 新增了生图包 以及各种转发和接口
This commit is contained in:
parent
c07369c297
commit
3514cf53f8
@ -13,14 +13,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将UTC时间转换为北京时间
|
/// 智能转换时间为北京时间
|
||||||
|
/// 如果是UTC时间则转换,否则直接返回
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="utcTime"></param>
|
/// <param name="dateTime">输入的时间</param>
|
||||||
/// <returns></returns>
|
/// <returns>北京时间</returns>
|
||||||
public static DateTime TransferUtcToBeijingTime(DateTime utcTime)
|
public static DateTime TransferUtcToBeijingTime(DateTime dateTime)
|
||||||
{
|
{
|
||||||
return TimeZoneInfo.ConvertTimeFromUtc(utcTime,
|
// 只有UTC时间才需要转换
|
||||||
|
if (dateTime.Kind == DateTimeKind.Utc)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 优先使用系统时区信息
|
||||||
|
return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
|
||||||
TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"));
|
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>
|
<ItemGroup>
|
||||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
using LMS.Repository.DB;
|
using LMS.Repository.DB;
|
||||||
|
using LMS.Repository.MJPackage;
|
||||||
using LMS.Repository.Models.DB;
|
using LMS.Repository.Models.DB;
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -46,6 +47,12 @@ namespace LMS.DAO
|
|||||||
|
|
||||||
public DbSet<DataInfo> DataInfo { get; set; }
|
public DbSet<DataInfo> DataInfo { get; set; }
|
||||||
|
|
||||||
|
public DbSet<MJApiTokens> MJApiTokens { get; set; }
|
||||||
|
|
||||||
|
public DbSet<MJApiTokenUsage> MJApiTokenUsage { get; set; }
|
||||||
|
|
||||||
|
public DbSet<MJApiTasks> MJApiTasks { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
@ -81,6 +88,30 @@ namespace LMS.DAO
|
|||||||
)
|
)
|
||||||
.HasColumnType("json"); // 指定MySQL字段类型为JSON
|
.HasColumnType("json"); // 指定MySQL字段类型为JSON
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<MJApiTokens>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("MJApiTokens");
|
||||||
|
entity.Property(e => e.Token).IsRequired().HasMaxLength(64);
|
||||||
|
entity.Property(e => e.DailyLimit).HasDefaultValue(0);
|
||||||
|
entity.Property(e => e.TotalLimit).HasDefaultValue(0);
|
||||||
|
entity.Property(e => e.ConcurrencyLimit).HasDefaultValue(1);
|
||||||
|
entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
entity.HasIndex(e => e.Token).IsUnique();
|
||||||
|
entity.HasIndex(e => e.ExpiresAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<MJApiTokenUsage>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("MJApiTokenUsage");
|
||||||
|
entity.HasKey(e => new { e.TokenId, e.Date });
|
||||||
|
|
||||||
|
entity.HasOne<MJApiTokens>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(e => e.TokenId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
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)
|
||||||
{
|
{
|
||||||
// 注册 Quartz 服务
|
|
||||||
services.AddQuartz(q =>
|
services.AddQuartz(q =>
|
||||||
{
|
{
|
||||||
|
// 时区配置
|
||||||
|
var chinaTimeZone = GetChinaTimeZone();
|
||||||
|
|
||||||
// 配置作业
|
// 每月任务配置
|
||||||
var jobKey = new JobKey("MonthlyTask", "DefaultGroup");
|
ConfigureMonthlyTask(q, chinaTimeZone);
|
||||||
|
|
||||||
// 方法1:通过配置属性设置时区
|
// 每日任务配置
|
||||||
// 获取中国时区
|
ConfigureDailyTask(q, chinaTimeZone);
|
||||||
TimeZoneInfo 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
|
try
|
||||||
{
|
{
|
||||||
// 尝试获取 Windows 时区名称
|
return TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");
|
||||||
chinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 尝试获取 Linux 时区名称
|
return TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai");
|
||||||
chinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai");
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// 如果都不可用,使用 UTC+8
|
return TimeZoneInfo.CreateCustomTimeZone(
|
||||||
chinaTimeZone = TimeZoneInfo.CreateCustomTimeZone(
|
|
||||||
"China_Custom",
|
"China_Custom",
|
||||||
new TimeSpan(8, 0, 0),
|
new TimeSpan(8, 0, 0),
|
||||||
"China Custom Time",
|
"China Custom Time",
|
||||||
"China Standard 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.AddJob<ResetUserFreeCount>(opts => opts.WithIdentity(jobKey));
|
||||||
|
|
||||||
// 添加触发器 - 每月1号凌晨0点执行
|
|
||||||
q.AddTrigger(opts => opts
|
q.AddTrigger(opts => opts
|
||||||
.ForJob(jobKey)
|
.ForJob(jobKey)
|
||||||
.WithIdentity("MonthlyTaskTrigger", "DefaultGroup")
|
.WithIdentity("MonthlyTaskTrigger", "DefaultGroup")
|
||||||
.WithCronSchedule("0 0 0 1 * ?")); // 每月1号凌晨0点
|
.WithCronSchedule("0 0 0 1 * ?", x => x.InTimeZone(timeZone)));
|
||||||
});
|
}
|
||||||
|
|
||||||
// 添加 Quartz 托管服务
|
private static void ConfigureDailyTask(IServiceCollectionQuartzConfigurator q, TimeZoneInfo timeZone)
|
||||||
services.AddQuartzHostedService(options =>
|
|
||||||
{
|
{
|
||||||
options.WaitForJobsToComplete = true;
|
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)
|
||||||
services.AddTransient<ResetUserFreeCount>();
|
{
|
||||||
}
|
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.Configuration.InitConfiguration;
|
||||||
using LMS.service.Extensions.Mail;
|
using LMS.service.Extensions.Mail;
|
||||||
using LMS.service.Service;
|
using LMS.service.Service;
|
||||||
|
using LMS.service.Service.MJPackage;
|
||||||
using LMS.service.Service.Other;
|
using LMS.service.Service.Other;
|
||||||
using LMS.service.Service.PermissionService;
|
using LMS.service.Service.PermissionService;
|
||||||
using LMS.service.Service.PromptService;
|
using LMS.service.Service.PromptService;
|
||||||
using LMS.service.Service.RoleService;
|
using LMS.service.Service.RoleService;
|
||||||
using LMS.service.Service.SoftwareService;
|
using LMS.service.Service.SoftwareService;
|
||||||
using LMS.service.Service.UserService;
|
using LMS.service.Service.UserService;
|
||||||
|
using LMS.Tools.MJPackage;
|
||||||
|
|
||||||
namespace Lai_server.Configuration
|
namespace Lai_server.Configuration
|
||||||
{
|
{
|
||||||
@ -38,6 +40,8 @@ namespace Lai_server.Configuration
|
|||||||
services.AddScoped<SoftwareService>();
|
services.AddScoped<SoftwareService>();
|
||||||
services.AddScoped<MachineAuthorizationService>();
|
services.AddScoped<MachineAuthorizationService>();
|
||||||
services.AddScoped<DataInfoService>();
|
services.AddScoped<DataInfoService>();
|
||||||
|
services.AddScoped<ITokenManagementService, TokenManagementService>();
|
||||||
|
services.AddScoped<IMJPackageService, MJPackageService>();
|
||||||
|
|
||||||
// 注入 DAO
|
// 注入 DAO
|
||||||
services.AddScoped<UserBasicDao>();
|
services.AddScoped<UserBasicDao>();
|
||||||
@ -53,6 +57,20 @@ namespace Lai_server.Configuration
|
|||||||
|
|
||||||
// 添加分布式缓存(用于存储验证码)
|
// 添加分布式缓存(用于存储验证码)
|
||||||
services.AddDistributedMemoryCache();
|
services.AddDistributedMemoryCache();
|
||||||
|
|
||||||
|
// 注册自定义服务
|
||||||
|
services.AddSingleton<TokenUsageTracker>();
|
||||||
|
services.AddScoped<ITokenService, TokenService>();
|
||||||
|
services.AddScoped<ITaskConcurrencyManager, TaskConcurrencyManager>();
|
||||||
|
services.AddScoped<ITaskService, TaskService>();
|
||||||
|
|
||||||
|
|
||||||
|
// 注册后台服务
|
||||||
|
services.AddHostedService<RsaConfigurattions>();
|
||||||
|
services.AddHostedService<DatabaseConfiguration>();
|
||||||
|
|
||||||
|
//services.AddHostedService<TokenSyncService>();
|
||||||
|
//services.AddHostedService<DailyResetService>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,
|
HttpOnly = true,
|
||||||
Secure = true, // 如果使用 HTTPS
|
Secure = true, // 如果使用 HTTPS
|
||||||
SameSite = SameSiteMode.None,
|
SameSite = SameSiteMode.None,
|
||||||
Expires = DateTime.UtcNow.AddDays(7),
|
Expires = BeijingTimeExtension.GetBeijingTime().AddDays(7),
|
||||||
};
|
};
|
||||||
Response.Cookies.Append("refreshToken", ((LoginResponse)apiResponse.Data).RefreshToken, cookieOptions);
|
Response.Cookies.Append("refreshToken", ((LoginResponse)apiResponse.Data).RefreshToken, cookieOptions);
|
||||||
return apiResponse;
|
return apiResponse;
|
||||||
|
|||||||
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;
|
||||||
using LMS.service.Configuration.InitConfiguration;
|
using LMS.service.Configuration.InitConfiguration;
|
||||||
using LMS.service.Extensions.Middleware;
|
using LMS.service.Extensions.Middleware;
|
||||||
|
using LMS.Tools.MJPackage;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@ -89,9 +90,6 @@ builder.Services.AddEndpointsApiExplorer();
|
|||||||
// 注入Swagger
|
// 注入Swagger
|
||||||
builder.Services.AddSwaggerService();
|
builder.Services.AddSwaggerService();
|
||||||
|
|
||||||
builder.Services.AddHostedService<RsaConfigurattions>();
|
|
||||||
builder.Services.AddHostedService<DatabaseConfiguration>();
|
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@ -120,6 +118,7 @@ app.MapControllers();
|
|||||||
// 在管道中使用IP速率限制中间件
|
// 在管道中使用IP速率限制中间件
|
||||||
app.UseIpRateLimiting();
|
app.UseIpRateLimiting();
|
||||||
|
|
||||||
|
// 添加动态权限的中间件
|
||||||
app.UseMiddleware<DynamicPermissionMiddleware>();
|
app.UseMiddleware<DynamicPermissionMiddleware>();
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -115,6 +115,16 @@ public class ForwardWordService(ApplicationDbContext context)
|
|||||||
throw new Exception("参数错误");
|
throw new Exception("参数错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断请求的url是不是满足条件
|
||||||
|
if (string.IsNullOrEmpty(request.GptUrl))
|
||||||
|
{
|
||||||
|
throw new Exception("请求的url为空");
|
||||||
|
}
|
||||||
|
if (!request.GptUrl.StartsWith("https://ark.cn-beijing.volces.com") && !request.GptUrl.StartsWith("https://api.moonshot.cn") && !request.GptUrl.StartsWith("https://laitool.net") && !request.GptUrl.StartsWith("https://api.laitool.cc") && !request.GptUrl.StartsWith("https://laitool.cc"))
|
||||||
|
{
|
||||||
|
throw new Exception("请求的url不合法");
|
||||||
|
}
|
||||||
|
|
||||||
// 获取提示词预设
|
// 获取提示词预设
|
||||||
Prompt? prompt = await _context.Prompt.FirstOrDefaultAsync(x => x.PromptTypeId == request.PromptTypeId && x.Id == request.PromptId);
|
Prompt? prompt = await _context.Prompt.FirstOrDefaultAsync(x => x.PromptTypeId == request.PromptTypeId && x.Id == request.PromptId);
|
||||||
if (prompt == null)
|
if (prompt == null)
|
||||||
|
|||||||
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.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using static LMS.Common.Enums.MachineEnum;
|
using static LMS.Common.Enums.MachineEnum;
|
||||||
using static LMS.Common.Enums.ResponseCodeEnum;
|
using static LMS.Common.Enums.ResponseCodeEnum;
|
||||||
using static LMS.Repository.DTO.MachineDto;
|
using static LMS.Repository.DTO.MachineDto;
|
||||||
|
|||||||
@ -1,25 +1,22 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using LinqKit;
|
||||||
using LMS.Common.Dictionary;
|
using LMS.Common.Dictionary;
|
||||||
using LMS.Common.Enums;
|
using LMS.Common.Enums;
|
||||||
|
using LMS.Common.Extensions;
|
||||||
using LMS.Common.Templates;
|
using LMS.Common.Templates;
|
||||||
using LMS.DAO;
|
using LMS.DAO;
|
||||||
using LMS.DAO.UserDAO;
|
using LMS.DAO.UserDAO;
|
||||||
using LMS.Repository.DB;
|
|
||||||
using LMS.Repository.DTO;
|
using LMS.Repository.DTO;
|
||||||
|
using LMS.Repository.DTO.OptionDto;
|
||||||
using LMS.Repository.Models.DB;
|
using LMS.Repository.Models.DB;
|
||||||
using LMS.Repository.Options;
|
using LMS.Repository.Options;
|
||||||
using LMS.service.Extensions.Mail;
|
using LMS.service.Extensions.Mail;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using System.Linq.Dynamic.Core;
|
||||||
using System.Linq;
|
|
||||||
using static LMS.Common.Enums.ResponseCodeEnum;
|
using static LMS.Common.Enums.ResponseCodeEnum;
|
||||||
using Options = LMS.Repository.DB.Options;
|
using Options = LMS.Repository.DB.Options;
|
||||||
using System.Linq.Dynamic.Core;
|
|
||||||
using LinqKit;
|
|
||||||
using LMS.Repository.DTO.OptionDto;
|
|
||||||
using LMS.Common.Extensions;
|
|
||||||
|
|
||||||
namespace LMS.service.Service
|
namespace LMS.service.Service
|
||||||
{
|
{
|
||||||
|
|||||||
@ -68,6 +68,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Version": "1.1.1",
|
"Version": "1.1.2",
|
||||||
"AllowedHosts": "*"
|
"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