Compare commits

...
33 Commits
Author SHA1 Message Date
admin777 a0c94f3eac 修改图片上传和视频上传限制可配置化 2026-05-26 13:53:40 +08:00
admin777 9edbe83376 修改文件日期和程序的启动端口 2026-05-26 10:13:31 +08:00
admin777 8019b24b61 修改OpenAI原格式的转发逻辑和参数 2025-11-23 18:17:00 +08:00
admin777 5875ffe671 修改机器码的验证,抽象通用方法
添加 OpenAI 格式的 流式转发接口 保持返回的数据格式
2025-11-23 12:04:48 +08:00
admin777 6d41f52de5 新增文件上传分类 不同分类 次数限制不同 文件保存时间不同 2025-09-14 13:57:54 +08:00
admin777 25d481d7d6 修改文件上传 添加文件转存 2025-09-13 18:04:51 +08:00
admin777 aa0e7c27e5 修改文件上传 添加文件转存 2025-09-13 18:04:20 +08:00
admin777 145a47fa8d 修改每日重置用户绘图数量
修改 生图包 submit 提交的请求体
2025-07-29 22:01:42 +08:00
admin777 8f82da94b7 修改生图包 token的到期时间 2025-07-07 21:24:01 +08:00
admin777 8b0db6a4e6 删除回调相关信息,使用定时轮询任务 2025-06-27 17:55:47 +08:00
admin777 758735fefc 修改原始请求的信息的数据处理 2025-06-27 10:45:19 +08:00
admin777 ef46c30ed3 修改返回报错逻辑
请求失败或者请求返回的code 不是 1 22 24 地时候 可以减少日使用量和释放型号量
单独处理 code 为 24 的时候。创建失败请求和只释放不减少日使用量
2025-06-23 23:47:11 +08:00
admin777 4340dd25a1 修复 单日绘图计数 添加原子性判断和增加
修复 高并发下会多次请求token的bug 现在都单次请求 使用 ConcurrentDictionary
2025-06-23 20:21:54 +08:00
admin777 3470ce9229 v 1.1.5
修改轮询请求逻辑 添加imageUrls属性,通过第三方生成
修改 mj fetch,不转发API 而是用系统的数据
2025-06-21 22:22:54 +08:00
admin777 6f580ff8eb 修改生图包任务轮询方法 请求任务方法 2025-06-21 14:41:21 +08:00
admin777 b5ed6a3b1c V 1.1.5 修改并发限制逻辑和位置 2025-06-20 19:36:40 +08:00
admin777 7f269c8b04 v 1.1.4 新增文件上传中转 七牛云 2025-06-18 17:19:29 +08:00
admin777 c12e1b5d65 添加 任务缓存 防止回调频繁修改数据 2025-06-16 20:12:35 +08:00
admin777 bc90b17961 v 1.1.3 新增Propmpt 返回通用备注 2025-06-15 16:15:59 +08:00
admin777 3514cf53f8 V 1.1.2 新增了生图包 以及各种转发和接口 2025-06-14 22:12:37 +08:00
admin777 c07369c297 V 1.1.1
优化公共数据获取权限控制
2025-05-23 21:05:17 +08:00
admin777 aaebbb9104 V 1.1.0
新增数据信息 完善数据信息得权限问题
2025-05-21 21:28:18 +08:00
admin777 647f2b75c9 V 1.0.9
修改完善了其他设置中的软件授权码的设置
修改部分权限控制
2025-05-16 17:48:10 +08:00
admin777 0a2ba8bb1a V 1.0.8
添加 获取公钥 登录 注册的IP限速 每分钟五次
2025-04-08 21:02:12 +08:00
admin777 61c4e0f977 v 1.0.7
新增软件授权和数据信息
2025-03-28 22:13:41 +08:00
admin777 a37c40a2ef V1.0.6
添加重置用户每月的免费换绑次数
优化项目结构
2025-03-24 16:53:32 +08:00
admin777 a23984c9f5 V1.0.5
新增用户重置密码和修改密码
2025-03-22 20:48:40 +08:00
admin777 57402e0dda V1.0.4
1. 新增用户注册需要邮箱验证码
2. 机器码、软件权限控制、用户 隔离,除非超级管理员,其他用户只能看到自己下面的用户,管理员可以看到除超级管理员以外的所有
2025-03-16 23:01:50 +08:00
admin777 1c5b9ed3c8 添加通用的option和添加201状态码判断 2025-02-23 21:06:25 +08:00
admin777 76b57c5717 新增get转发接口
新增Laitool 设置中的绘图设置
2025-02-22 15:00:25 +08:00
admin777 01055762ca 新增一个openAI格式的转发接口,直接转发整个请求体 2025-02-16 16:00:04 +08:00
admin777 3934088e43 新增同步软件控制权限控制软件状态 2025-02-12 12:19:46 +08:00
admin777 3f567cb481 222222 2025-02-10 10:37:38 +08:00
145 changed files with 9941 additions and 261 deletions
+1
View File
@@ -361,3 +361,4 @@ MigrationBackup/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd
/LMS.service/.idea/.idea.LMS.service/.idea
+1 -6
View File
@@ -1,9 +1,4 @@
using System; using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LMS.Common.Attributes namespace LMS.Common.Attributes
{ {
+49 -5
View File
@@ -1,12 +1,56 @@
namespace LMS.Common.Dictionary; using LMS.Common.Enums;
namespace LMS.Common.Dictionary;
public class AllOptions public class AllOptions
{ {
public static class AllOptionKey
{
/// <summary>
/// 获取所有的 Option
/// </summary>
public const string All = "all";
/// <summary>
/// 获取TTS相关的 Option
/// </summary>
public const string TTS = "tts";
/// <summary>
/// 获取软件相关的 Option
/// </summary>
public const string Software = "software";
/// <summary>
/// 软件试用相关 Option
/// </summary>
public const string Trial = "trial";
/// <summary>
/// 出图相关的 Option
/// </summary>
public const string Image = "image";
/// <summary>
/// 邮件设置相关 Option
/// </summary>
public const string MailSetting = "mailSetting";
/// <summary>
/// 重置免费次数相关的 Option
/// </summary>
public const string ResetFreeCount = "resetFreeCount";
}
public static readonly Dictionary<string, List<string>> AllOptionsRequestQuery = new() public static readonly Dictionary<string, List<string>> AllOptionsRequestQuery = new()
{ {
{ "all", [] }, { AllOptionKey.All, [] },
{ "tts", ["EdgeTTsRoles"] }, { AllOptionKey.TTS, ["EdgeTTsRoles"] },
{ "software", ["LaitoolHomePage", "LaitoolNotice", "LaitoolUpdateContent","LaitoolVersion"]}, { AllOptionKey.Software, ["LaitoolHomePage", "LaitoolNotice", "LaitoolUpdateContent","LaitoolVersion"]},
{ "trial" , ["LaiToolTrialDays"] } { AllOptionKey.Trial , ["LaiToolTrialDays"] },
{ AllOptionKey.Image, [OptionKeyName.LaitoolFluxApiModelList] },
{ AllOptionKey.MailSetting , [OptionKeyName.SMTPMailSetting] },
{ AllOptionKey.ResetFreeCount, [OptionKeyName.ResetFreeCountSetting] }
}; };
} }
+31 -3
View File
@@ -1,10 +1,38 @@
namespace LMS.Common.Dictionary; using LMS.Common.Enums;
namespace LMS.Common.Dictionary;
public class SimpleOptions public class SimpleOptions
{ {
public static class SimpleOptionKey
{
/// <summary>
/// TTS的角色Option
/// </summary>
public const string Ttsrole = "ttsrole";
/// <summary>
/// LaiTool信息相关的配置
/// </summary>
public const string Laitoolinfo = "laitoolinfo";
/// <summary>
/// LaiTool FluxAPI对应的模型信息
/// </summary>
public const string LaitoolFluxApiModelList = "LaitoolFluxApiModelList";
/// <summary>
/// 是否开启邮箱信息
/// </summary>
public const string EnableMailService = "EnableMailService";
}
public static readonly Dictionary<string, List<string>> SimpleOptionsRequestQuery = new() public static readonly Dictionary<string, List<string>> SimpleOptionsRequestQuery = new()
{ {
{ "ttsrole", ["EdgeTTsRoles"] }, { SimpleOptionKey.Ttsrole, ["EdgeTTsRoles"] },
{ "laitoolinfo", ["LaitoolHomePage", "LaitoolNotice", "LaitoolUpdateContent", "LaitoolVersion"] }, { SimpleOptionKey.Laitoolinfo, ["LaitoolHomePage", "LaitoolNotice", "LaitoolUpdateContent", "LaitoolVersion"] },
{ SimpleOptionKey.LaitoolFluxApiModelList, [OptionKeyName.LaitoolFluxApiModelList] },
{ SimpleOptionKey.EnableMailService, [OptionKeyName.EnableMailService]}
}; };
} }
-8
View File
@@ -1,8 +0,0 @@
namespace LMS.Common.Enum;
public enum OptionTypeEnum
{
String = 1,
JSON = 2,
Number = 3,
}
+10
View File
@@ -0,0 +1,10 @@
using LMS.Common.Attributes;
namespace LMS.Common.Enums
{
public enum DataInfoTypeEnum
{
[Description("Discord")]
Discord = 0,
}
}
@@ -0,0 +1,21 @@
using System.ComponentModel;
namespace LMS.Common.Enums
{
public enum MachineAuthorizationEnum
{
[Description("NanFengAI")]
NanFengAI = 0,
}
/// <summary>
/// 授权使用类型,目前是专业和基础
/// </summary>
public enum MachineAuthorizationUseTypeEnum
{
[Description("基础")]
Basic = 0,
[Description("专业")]
Professional = 1
}
}
+42
View File
@@ -0,0 +1,42 @@
namespace LMS.Common.Enums;
public enum OptionTypeEnum
{
String = 1,
JSON = 2,
Number = 3,
Boolean = 4
}
/// <summary>
/// option的分类
/// </summary>
public enum OptionCategory
{
System = 1,
LaiTool = 2,
NanFengAI = 3,
}
public static class OptionKeyName
{
/// <summary>
/// LaiTool Flux API 模型列表的Option Key
/// </summary>
public const string LaitoolFluxApiModelList = "LaitoolFluxApiModelList";
/// <summary>
/// SMTP的邮件设置
/// </summary>
public const string SMTPMailSetting = "SMTPMailSetting";
/// <summary>
/// 是否开启邮箱服务
/// </summary>
public const string EnableMailService = "EnableMailService";
/// <summary>
/// 重置用户免费次数的setting
/// </summary>
public const string ResetFreeCountSetting = "ResetFreeCountSetting";
}
@@ -1,4 +1,4 @@
namespace LMS.Common.Enum namespace LMS.Common.Enums
{ {
public class SoftwareControlEnum public class SoftwareControlEnum
{ {
@@ -25,7 +25,6 @@
/// 一年 /// 一年
/// </summary> /// </summary>
ThreeHundredAndSixtyFive = 365, ThreeHundredAndSixtyFive = 365,
} }
} }
} }
@@ -0,0 +1,52 @@
namespace LMS.Common.Extensions
{
public class BeijingTimeExtension
{
/// <summary>
/// 获取北京时间,将时区转换为北京时间
/// </summary>
/// <returns></returns>
public static DateTime GetBeijingTime()
{
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow,
TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"));
}
/// <summary>
/// 智能转换时间为北京时间
/// 如果是UTC时间则转换,否则直接返回
/// </summary>
/// <param name="dateTime">输入的时间</param>
/// <returns>北京时间</returns>
public static DateTime TransferUtcToBeijingTime(DateTime dateTime)
{
// 只有UTC时间才需要转换
if (dateTime.Kind == DateTimeKind.Utc)
{
try
{
// 优先使用系统时区信息
return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"));
}
catch (TimeZoneNotFoundException)
{
try
{
// Linux系统可能使用这个ID
return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai"));
}
catch (TimeZoneNotFoundException)
{
// 找不到时区就手动加8小时
return dateTime.AddHours(8);
}
}
}
// 非UTC时间直接返回
return dateTime;
}
}
}
@@ -1,4 +1,4 @@
namespace LMS.Tools.Extensions namespace LMS.Common.Extensions
{ {
public class ConvertExtension public class ConvertExtension
{ {
@@ -1,7 +1,6 @@
using LMS.Common.Attributes; using LMS.Common.Attributes;
using static LMS.Common.Enums.PermissionEnum;
namespace LMS.Tools namespace LMS.Common.Extensions
{ {
public static class EnumExtensions public static class EnumExtensions
{ {
+6
View File
@@ -6,4 +6,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="4.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
</ItemGroup>
</Project> </Project>
@@ -0,0 +1,78 @@
using System.Security.Cryptography;
using System.Text;
namespace LMS.Common.Password
{
public static class PasswordGenerator
{
private const string LowercaseChars = "abcdefghijklmnopqrstuvwxyz";
private const string UppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private const string NumberChars = "0123456789";
private const string SpecialChars = "@$!%*?.&";
private const string AllChars = LowercaseChars + UppercaseChars + NumberChars + SpecialChars;
/// <summary>
/// 生成指定长度的随机密码
/// </summary>
/// <param name="length">密码长度</param>
/// <returns>随机生成的密码</returns>
public static string GeneratePassword(int length)
{
if (length < 4)
{
throw new ArgumentException("密码长度必须至少为4个字符", nameof(length));
}
// 使用加密安全的随机数生成器
using var rng = RandomNumberGenerator.Create();
// 确保密码包含至少一个小写字母、一个大写字母、一个数字和一个特殊字符
var password = new StringBuilder();
// 添加每种类型的至少一个字符
password.Append(GetRandomChar(LowercaseChars, rng));
password.Append(GetRandomChar(UppercaseChars, rng));
password.Append(GetRandomChar(NumberChars, rng));
password.Append(GetRandomChar(SpecialChars, rng));
// 添加剩余的随机字符
for (int i = 4; i < length; i++)
{
password.Append(GetRandomChar(AllChars, rng));
}
// 打乱字符顺序
return ShuffleString(password.ToString(), rng);
}
/// <summary>
/// 从指定字符集中获取一个随机字符
/// </summary>
private static char GetRandomChar(string chars, RandomNumberGenerator rng)
{
byte[] data = new byte[1];
rng.GetBytes(data);
return chars[data[0] % chars.Length];
}
/// <summary>
/// 打乱字符串中字符的顺序
/// </summary>
private static string ShuffleString(string input, RandomNumberGenerator rng)
{
char[] array = input.ToCharArray();
int n = array.Length;
while (n > 1)
{
byte[] box = new byte[1];
rng.GetBytes(box);
int k = box[0] % n;
n--;
char temp = array[n];
array[n] = array[k];
array[k] = temp;
}
return new string(array);
}
}
}
@@ -0,0 +1,60 @@
namespace LMS.Common.Templates
{
public class EmailTemplateService()
{
/// <summary>
/// 注册邮件模板
/// </summary>
public const string RegisterHtmlTemplates = """
<!DOCTYPE html>
<html>
<body>
<h1>LMS注册验证码</h1>
<p></p>
<p> <strong>{RegisterCode}</strong></p>
<p> 10 </p>
</body>
</html>
""";
public const string ResetPasswordHtmlTemplates = """
<!DOCTYPE html>
<html>
<body>
<h1>LMS重置密码验证码</h1>
<p></p>
<p> <strong>{ResetPasswordCode}</strong></p>
<p> 10 </p>
</body>
</html>
""";
public const string ResetpasswordSuccessMail = """
<!DOCTYPE html>
<html>
<body>
<h1>LMS重置密码成功</h1>
<p></p>
<p></p>
<p> <strong>{NewPassword}</strong></p>
</body>
</html>
""";
/// <summary>
/// 替换模板的占位符
/// </summary>
/// <param name="template"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public static string ReplaceTemplate(string template, Dictionary<string, string> parameters)
{
// 替换占位符
foreach (var param in parameters)
{
template = template.Replace($"{{{param.Key}}}", param.Value);
}
return template;
}
}
}
+69
View File
@@ -1,9 +1,12 @@
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;
using Microsoft.EntityFrameworkCore.ChangeTracking;
//using Newtonsoft.Json;
using System.Text.Json; using System.Text.Json;
namespace LMS.DAO namespace LMS.DAO
@@ -40,6 +43,18 @@ namespace LMS.DAO
public DbSet<SoftwareControl> SoftwareControl { get; set; } public DbSet<SoftwareControl> SoftwareControl { get; set; }
public DbSet<MachineAuthorization> MachineAuthorization { 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; }
public DbSet<FileUploads> FileUploads { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
@@ -52,9 +67,63 @@ namespace LMS.DAO
v => string.IsNullOrEmpty(v) || v == "[]" v => string.IsNullOrEmpty(v) || v == "[]"
? new List<string>() // 如果存储的是空字符串或空数组,则返回空列表 ? new List<string>() // 如果存储的是空字符串或空数组,则返回空列表
: JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null) ?? new List<string>() : JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null) ?? new List<string>()
).Metadata.SetValueComparer(
new ValueComparer<List<string>>(
// 比较两个集合是否相等
(c1, c2) => c1 != null && c2 != null && c1.SequenceEqual(c2),
// 计算集合的哈希码 - 这里修复了问题
c => c == null ? 0 : c.Aggregate(0, (a, v) => HashCode.Combine(a, v != null ? v.GetHashCode() : 0)),
// 创建集合的副本
c => c == null ? null : c.ToList()
)
); );
modelBuilder.Entity<UserSoftware>() modelBuilder.Entity<UserSoftware>()
.HasKey(us => new { us.UserId, us.SoftwareId }); .HasKey(us => new { us.UserId, us.SoftwareId });
// Options表的RoleIds字段使用JSON格式存储
modelBuilder.Entity<Options>(entity =>
{
entity.Property(e => e.RoleIds)
.HasConversion(
v => Newtonsoft.Json.JsonConvert.SerializeObject(v), // 将 List<int> 序列化为 JSON 字符串
v => Newtonsoft.Json.JsonConvert.DeserializeObject<List<long>>(v ?? "[]") ?? new List<long>() // 反序列化
)
.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);
});
modelBuilder.Entity<FileUploads>(entity =>
{
entity.HasIndex(e => e.UserId);
entity.HasIndex(e => e.FileKey);
entity.HasIndex(e => e.UploadTime);
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETDATE()");
entity.Property(e => e.UploadTime).HasDefaultValueSql("GETDATE()");
});
} }
} }
} }
+1 -1
View File
@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="OneOf" Version="3.0.271" /> <PackageReference Include="OneOf" Version="3.0.271" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
</ItemGroup> </ItemGroup>
@@ -15,7 +16,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LMS.Common\LMS.Common.csproj" /> <ProjectReference Include="..\LMS.Common\LMS.Common.csproj" />
<ProjectReference Include="..\LMS.Repository\LMS.Repository.csproj" /> <ProjectReference Include="..\LMS.Repository\LMS.Repository.csproj" />
<ProjectReference Include="..\LMS.Tools\LMS.Tools.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+67
View File
@@ -0,0 +1,67 @@
using LMS.Repository.DB;
using Microsoft.EntityFrameworkCore;
namespace LMS.DAO.OptionDAO
{
public class OptionGlobalDAO(ApplicationDbContext dbContext)
{
private readonly ApplicationDbContext _dbContext = dbContext;
/// <summary>
/// 根据配置键查找并返回指定类型的配置值
/// </summary>
/// <typeparam name="T">返回值类型</typeparam>
/// <param name="optionKey">配置键</param>
/// <returns>配置值,如果不存在或转换失败则返回默认值</returns>
public async Task<T?> FindAndReturnOption<T>(string optionKey)
{
// 参数验证
if (string.IsNullOrWhiteSpace(optionKey))
{
return default(T);
}
var options = await _dbContext.Options
.Where(x => x.Key == optionKey)
.FirstOrDefaultAsync();
if (options == null) return default;
// 直接返回转换结果,GetValueObject内部应该处理null情况
return options.GetValueObject<T>() ?? default;
}
/// <summary>
/// 检查配置是否存在
/// </summary>
/// <param name="optionKey">配置键</param>
/// <returns>是否存在</returns>
public async Task<bool> OptionExists(string optionKey)
{
if (string.IsNullOrWhiteSpace(optionKey))
{
return false;
}
return await _dbContext.Options
.AnyAsync(x => x.Key == optionKey);
}
/// <summary>
/// 获取多个配置
/// </summary>
/// <param name="optionKeys">配置键列表</param>
/// <returns>配置字典</returns>
public async Task<Dictionary<string, Options>> GetMultipleOptions(params string[] optionKeys)
{
if (optionKeys == null || optionKeys.Length == 0)
{
return new Dictionary<string, Options>();
}
var options = await _dbContext.Options
.Where(x => optionKeys.Contains(x.Key))
.ToListAsync();
return options.ToDictionary(x => x.Key, x => x);
}
}
}
+92 -3
View File
@@ -1,11 +1,14 @@
using LMS.Repository.Models.DB; using LMS.Repository.Models.DB;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace LMS.DAO.UserDAO namespace LMS.DAO.UserDAO
{ {
public class UserBasicDao(UserManager<User> userManager) public class UserBasicDao(UserManager<User> userManager, ApplicationDbContext dbContext)
{ {
private readonly UserManager<User> _userManager = userManager; private readonly UserManager<User> _userManager = userManager;
private readonly ApplicationDbContext _dbContext = dbContext;
/// <summary> /// <summary>
/// 检查用户是否存在,通过用户ID /// 检查用户是否存在,通过用户ID
/// </summary> /// </summary>
@@ -35,7 +38,7 @@ namespace LMS.DAO.UserDAO
User? user = await _userManager.FindByIdAsync(userId.ToString() ?? "0") ?? throw new Exception("用户不存在"); User? user = await _userManager.FindByIdAsync(userId.ToString() ?? "0") ?? throw new Exception("用户不存在");
bool isAdminOrSuperAdmin = await _userManager.IsInRoleAsync(user, "Admin") || await _userManager.IsInRoleAsync(user, "Super Admin"); bool isAdminOrSuperAdmin = await _userManager.IsInRoleAsync(user, "Admin") || await _userManager.IsInRoleAsync(user, "Super Admin");
return isAdminOrSuperAdmin; return isAdminOrSuperAdmin || userId == 4;
} }
/// <summary> /// <summary>
@@ -53,7 +56,93 @@ namespace LMS.DAO.UserDAO
User? user = await _userManager.FindByIdAsync(userId.ToString() ?? "0") ?? throw new Exception("用户不存在"); User? user = await _userManager.FindByIdAsync(userId.ToString() ?? "0") ?? throw new Exception("用户不存在");
bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Super Admin"); bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Super Admin");
return isSuperAdmin; return isSuperAdmin || userId == 4;
}
/// <summary>
/// 检查用户是不是管理员
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<bool> CheckUserIsAdmin(long? userId)
{
if (userId == null)
{
return false;
}
User? user = await _userManager.FindByIdAsync(userId.ToString() ?? "0") ?? throw new Exception("用户不存在");
bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Admin");
return isSuperAdmin || userId == 4;
}
/// <summary>
/// 检查用户是不是代理
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<bool> CheckUserIsAgent(long? userId)
{
if (userId == null)
{
return false;
}
User? user = await _userManager.FindByIdAsync(userId.ToString() ?? "0") ?? throw new Exception("用户不存在");
bool isSuperAdmin = await _userManager.IsInRoleAsync(user, "Agent User");
return isSuperAdmin || userId == 4;
}
/// <summary>
/// 判断用户是不是指定用户的上级
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="agentUserId">上级用户ID</param>
/// <returns></returns>
public async Task<bool> CheckAgentAndUserMatch(long? userId, long? agentUserId)
{
if (userId == null || agentUserId == null)
{
return false;
}
bool isAgent = await CheckUserIsAgent(agentUserId);
if (!isAgent)
{
return false;
}
User? user = await _userManager.FindByIdAsync(userId.ToString() ?? "0") ?? throw new Exception("用户不存在");
if (user == null)
{
return false;
}
if (user.ParentId != agentUserId)
{
return false;
}
return true;
}
/// <summary>
/// 获取用户的所有角色ID
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<List<long>> GetUserRoleIds(long userId)
{
// 查找用户,若不存在直接抛异常
User user = await _userManager.FindByIdAsync(userId.ToString())
?? throw new Exception("用户不存在");
// 直接查询 UserRoles 表获取所有关联的 RoleId
var roleIds = await _dbContext.UserRoles
.Where(ur => ur.UserId == user.Id)
.Select(ur => ur.RoleId)
.ToListAsync();
return roleIds;
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
using LMS.Tools.Extensions; using LMS.Common.Extensions;
namespace LMS.Repository.Models.DB namespace LMS.Repository.Models.DB
{ {
+30
View File
@@ -0,0 +1,30 @@
using LMS.Common.Enums;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.DB
{
public class DataInfo
{
/// <summary>
/// ID
/// </summary>
[Required]
public required string ID { get; set; }
/// <summary>
/// 数据的类型
/// </summary>
[Required]
public required DataInfoTypeEnum Type { get; set; }
/// <summary>
/// 数据的字符串
/// </summary>
[Required]
public required string DataString { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedTime { get; set; }
}
}
+47
View File
@@ -0,0 +1,47 @@
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.DB;
public class FileUploads
{
[Key]
public long Id { get; set; }
[Required]
[StringLength(50)]
public long UserId { get; set; }
[Required]
[StringLength(255)]
public string FileName { get; set; }
[Required]
[StringLength(500)]
public string FileKey { get; set; }
public long FileSize { get; set; }
[Required]
[StringLength(100)]
public string ContentType { get; set; }
[Required]
[StringLength(100)]
public string Hash { get; set; }
[Required]
[StringLength(1000)]
public string QiniuUrl { get; set; }
public DateTime UploadTime { get; set; }
[StringLength(20)]
public string Status { get; set; } = "active";
public DateTime CreatedAt { get; set; }
/// <summary>
/// 删除时间 ,默认为最大值,表示未删除
/// </summary>
public DateTime DeleteTime { get; set; } = DateTime.MaxValue;
}
+41
View File
@@ -0,0 +1,41 @@
using LMS.Repository.MJPackage;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.DB
{
public class MJApiTasks
{
[Key]
public string TaskId { get; set; }
public string Token { get; set; }
public long TokenId { get; set; }
public DateTime StartTime { get; set; }
public DateTime? EndTime { get; set; }
public string Status { get; set; }
public string ThirdPartyTaskId { get; set; } // 第三方任务ID
public string? Properties { get; set; }
}
public class MJApiTaskCollection : MJApiTasks
{
public long TokenId { get; set; }
public string Token { get; set; }
}
public class MJTaskStatus
{
public const string NOT_START = "NOT_START";
public const string SUBMITTED = "SUBMITTED";
public const string IN_PROGRESS = "IN_PROGRESS";
public const string FAILURE = "FAILURE";
public const string SUCCESS = "SUCCESS";
public const string MODAL = "MODAL";
public const string CANCEL = "CANCEL";
}
}
+22
View File
@@ -0,0 +1,22 @@
using LMS.Common.Extensions;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.DB
{
public class MJApiTokenUsage
{
[Key]
public long TokenId { get; set; }
[Key]
public DateTime Date { get; set; }
public int DailyUsage { get; set; } = 0;
public int TotalUsage { get; set; } = 0;
public DateTime LastActivityAt { get; set; } = BeijingTimeExtension.GetBeijingTime();
public string? HistoryUse { get; set; } = null;
}
}
+29
View File
@@ -0,0 +1,29 @@
using LMS.Common.Extensions;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace LMS.Repository.DB
{
public class MJApiTokens
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
[StringLength(64)]
public required string Token { get; set; }
[Required]
public required string UseToken { get; set; } // 实际使用的Token
public int DailyLimit { get; set; } = 0;
public int TotalLimit { get; set; } = 0;
public int ConcurrencyLimit { get; set; } = 1;
public DateTime CreatedAt { get; set; } = BeijingTimeExtension.GetBeijingTime();
public DateTime? ExpiresAt { get; set; }
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
using static LMS.Common.Enums.MachineEnum; using static LMS.Common.Enums.MachineEnum;
namespace LMS.Repository.Models.DB namespace LMS.Repository.DB
{ {
public class Machine public class Machine
{ {
+76
View File
@@ -0,0 +1,76 @@
using LMS.Common.Enums;
using System.ComponentModel.DataAnnotations;
using static LMS.Common.Enums.SoftwareControlEnum;
namespace LMS.Repository.DB
{
public class MachineAuthorization
{
/// <summary>
/// ID
/// </summary>
[Required]
public required string ID { get; set; }
/// <summary>
/// 机器码或者是授权码
/// </summary>
public string? MachineID { get; set; }
/// <summary>
/// 授权时间
/// </summary>
[Required]
public required SoftwareControlValidityEnum ExpiryTime { get; set; }
/// <summary>
/// 授权使用状态
/// </summary>
[Required]
public required MachineAuthorizationUseTypeEnum UseType { get; set; }
/// <summary>
/// 授权软件类型
/// </summary>
[Required]
public required MachineAuthorizationEnum Type { get; set; }
/// <summary>
/// 授权日期
/// </summary>
[Required]
public DateTime AuthorizedDate { get; set; }
/// <summary>
/// 过期日期
/// </summary>
[Required]
public DateTime ExpiryDate { get; set; }
/// <summary>
/// 授权码
/// </summary>
[Required]
public required string AuthorizationCode { get; set; }
/// <summary>
/// 创建用户ID
/// </summary>
public long CreatedUserID { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedDate { get; set; }
/// <summary>
/// 更新用户ID
/// </summary>
public long UpdatedUserID { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedDate { get; set; }
}
}
+11 -4
View File
@@ -1,14 +1,13 @@
using LMS.Common.Enum; using LMS.Common.Enums;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace LMS.Repository.DB; namespace LMS.Repository.DB;
public class Options public class Options
{ {
[Key] [Key]
[Required]
public required string Key { get; set; } = string.Empty; public required string Key { get; set; } = string.Empty;
/// <summary> /// <summary>
@@ -16,9 +15,17 @@ public class Options
/// </summary> /// </summary>
public string? Value { get; set; } = string.Empty; public string? Value { get; set; } = string.Empty;
[Required]
public OptionTypeEnum Type { get; set; } = OptionTypeEnum.String; public OptionTypeEnum Type { get; set; } = OptionTypeEnum.String;
// 写一个字段,映射Value,判断是不是json字符串,是的话就解析成对象 [Required]
public OptionCategory Category { get; set; } = OptionCategory.System;
[Required]
public List<long> RoleIds { get; set; } = [];
public DateTime CreatedTime { get; set; }
// 写一个字段,映射Value,判断是不是json字符串,是的话就解析成对象 // 写一个字段,映射Value,判断是不是json字符串,是的话就解析成对象
public T? GetValueObject<T>() public T? GetValueObject<T>()
{ {
+1 -2
View File
@@ -1,5 +1,4 @@
using LMS.Common.Enums; using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json; using System.Text.Json;
using static LMS.Common.Enums.PermissionEnum; using static LMS.Common.Enums.PermissionEnum;
-1
View File
@@ -1,5 +1,4 @@
using LMS.Common.Enums; using LMS.Common.Enums;
using static LMS.Common.Enums.PermissionEnum;
namespace LMS.Repository.Models.DB namespace LMS.Repository.Models.DB
{ {
+1 -1
View File
@@ -1,4 +1,4 @@
using LMS.Tools.Extensions; using LMS.Common.Extensions;
namespace LMS.Repository.DB namespace LMS.Repository.DB
{ {
+2 -2
View File
@@ -1,5 +1,5 @@
using LMS.Repository.User; using LMS.Common.Extensions;
using LMS.Tools.Extensions; using LMS.Repository.User;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
+53
View File
@@ -0,0 +1,53 @@
using LMS.Repository.DB;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.DTO
{
public class FileUploadDto
{
public class ByteUploadRequest
{
//public required string FileBytes { get; set; }
/// <summary>
/// 文件的base64
/// </summary>
public required string File { get; set; }
public required string FileName { get; set; }
public required string ContentType { get; set; }
public Dictionary<string, string> Metadata { get; set; } = new();
public string? Type { get; set; }
}
/// <summary>
/// URL上传请求
/// </summary>
public class UrlUploadRequest
{
[Required]
[Url]
public required string Url { get; set; }
[Required]
public required string FileName { get; set; }
}
public class UploadResult
{
public bool Success { get; set; }
public string Message { get; set; }
public string Url { get; set; }
public string FileKey { get; set; }
public string Hash { get; set; }
public long FileId { get; set; }
public long FileSize { get; set; }
}
public class FileListResponse
{
public List<FileUploads> Files { get; set; }
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
}
}
}
+4 -3
View File
@@ -1,9 +1,10 @@
using LMS.Repository.DTO.UserDto; using LMS.Repository.DB;
using LMS.Repository.Models.DB; using LMS.Repository.DTO.UserDto;
using LMS.Repository.Machine;
namespace LMS.Repository.DTO namespace LMS.Repository.DTO
{ {
public class MachineDetailDto : Machine public class MachineDetailDto : MachineModel
{ {
public UserBaseDto? CreatedUser { get; set; } public UserBaseDto? CreatedUser { get; set; }
+1 -1
View File
@@ -1,6 +1,6 @@
using static LMS.Common.Enums.MachineEnum; using static LMS.Common.Enums.MachineEnum;
namespace LMS.Repository.DTO.MachineResponse namespace LMS.Repository.DTO
{ {
public class MachineDto public class MachineDto
{ {
@@ -0,0 +1,21 @@
using LMS.Common.Enums;
namespace LMS.Repository.DTO.OptionDto
{
public class OptionSimpleDto
{
public required string Key { get; set; } = string.Empty;
/// <summary>
/// Value of the option,这个值是一个json字符串
/// </summary>
public string? Value { get; set; } = string.Empty;
public OptionTypeEnum Type { get; set; } = OptionTypeEnum.String;
public OptionCategory Category { get; set; } = OptionCategory.System;
public List<string> RoleNames { get; set; } = [];
}
}
@@ -1,7 +1,6 @@
using LMS.Common.Enum; using LMS.Common.Enums;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.DTO; namespace LMS.Repository.DTO.OptionDto;
public class OptionsDto public class OptionsDto
{ {
@@ -0,0 +1,79 @@
using LMS.Common.Enums;
using LMS.Repository.DTO.UserDto;
using System.ComponentModel.DataAnnotations;
using static LMS.Common.Enums.SoftwareControlEnum;
namespace LMS.Repository.DTO.OtherDto
{
public class MachineAuthorizationDto
{
/// <summary>
/// ID
/// </summary>
[Required]
public required string ID { get; set; }
/// <summary>
/// 机器码或者是授权码
/// </summary>
[Required]
public required string MachineID { get; set; }
/// <summary>
/// 可用时间
/// </summary>
[Required]
public required SoftwareControlValidityEnum ExpiryTime { get; set; }
/// <summary>
/// 授权使用状态
/// </summary>
[Required]
public required MachineAuthorizationUseTypeEnum UseType { get; set; }
/// <summary>
/// 授权软件类型
/// </summary>
[Required]
public required MachineAuthorizationEnum Type { get; set; }
/// <summary>
/// 授权日期
/// </summary>
[Required]
public DateTime AuthorizedDate { get; set; }
/// <summary>
/// 过期日期
/// </summary>
[Required]
public DateTime ExpiryDate { get; set; }
/// <summary>
/// 授权码
/// </summary>
[Required]
public required string AuthorizationCode { get; set; }
/// <summary>
/// 创建用户
/// </summary>
public UserBaseDto? CreatedUser { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedDate { get; set; }
/// <summary>
/// 更新用户ID
/// </summary>
public UserBaseDto? UpdatedUser { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedDate { get; set; }
}
}
@@ -0,0 +1,44 @@
using LMS.Common.Enums;
using static LMS.Common.Enums.SoftwareControlEnum;
namespace LMS.Repository.DTO.OtherDto
{
public class MachineAuthorizationStatusDto
{
/// <summary>
/// 机器码
/// </summary>
public string MachineID { get; set; }
/// <summary>
/// 授权软件类型
/// </summary>
public MachineAuthorizationEnum Type { get; set; }
/// <summary>
/// 授权使用状态
/// </summary>
public MachineAuthorizationUseTypeEnum UseType { get; set; }
/// <summary>
/// 授权软件类型
/// </summary>
public SoftwareControlValidityEnum ExpiryTime { get; set; }
/// <summary>
/// 授权日期
/// </summary>
public DateTime AuthorizedDate { get; set; }
/// <summary>
/// 过期日期
/// </summary>
public DateTime ExpiryDate { get; set; }
/// <summary>
/// 授权码
/// </summary>
public string AuthorizationCode { get; set; }
}
}
@@ -7,4 +7,8 @@ public class PromptNameDto
public string Name { get; set; } public string Name { get; set; }
public string PromptTypeId { get; set; } public string PromptTypeId { get; set; }
public string? Remark { get; set; }
public string? Description { get; set; }
} }
@@ -5,4 +5,6 @@ public class PrompTypeNameModel
public string Name { get; set; } public string Name { get; set; }
public string Id { get; set; } public string Id { get; set; }
public string? Remark { get; set; }
} }
@@ -0,0 +1,42 @@
namespace LMS.Repository.FileUpload
{
public class FileRequestReturn
{
public class FileMachineRequestReturn
{
public string MachineId { get; set; }
public string FileName { get; set; }
public long FileSize { get; set; }
public string ContentType { get; set; }
public string Hash { get; set; }
public string Url { get; set; }
public DateTime UploadTime { get; set; }
public DateTime CreatedAt { get; set; }
/// <summary>
/// 删除时间 ,默认为最大值,表示未不删除
/// </summary>
public DateTime DeleteTime { get; set; } = DateTime.MaxValue;
}
public class FileUserRequestReturn
{
public long Id { get; set; }
public long UserId { get; set; }
public string FileName { get; set; }
public long FileSize { get; set; }
public string ContentType { get; set; }
public string Hash { get; set; }
public string Url { get; set; }
public DateTime UploadTime { get; set; }
public DateTime CreatedAt { get; set; }
/// <summary>
/// 删除时间 ,默认为最大值,表示未不删除
/// </summary>
public DateTime DeleteTime { get; set; } = DateTime.MaxValue;
}
}
}
@@ -0,0 +1,21 @@
namespace LMS.Repository.FileUpload;
public class QiniuSettings
{
public string AccessKey { get; set; }
public string SecretKey { get; set; }
public string BucketName { get; set; }
public string Domain { get; set; }
/// <summary>
/// 删除时间 天数 没有值 则不删除
/// </summary>
public int? DeleteDay { get; set; }
}
public class FileUploadSettings
{
public long MaxFileSize { get; set; } = 3 * 1024 * 1024; // 5MB
public List<string> AllowedContentTypes { get; set; } = new();
public int DailyUploadLimit { get; set; } = 5;
public int VideoDailyUploadLimit { get; set; } = 50;
}
+6
View File
@@ -46,3 +46,9 @@ public class ForwardModel
[Required] [Required]
public string Word { get; set; } = string.Empty; public string Word { get; set; } = string.Empty;
} }
public class ForwardModelOpenAI : ForwardModel
{
[Required]
public string OpenAIBodyString { get; set; } = string.Empty;
}
@@ -0,0 +1,32 @@
namespace LMS.Repository.Forward;
public class SimpleTransferModel
{
/// <summary>
/// GPT的完整地址
/// </summary>
public string url { get; set; }
/// <summary>
/// 对应的API站的使用的APIkey
/// </summary>
public string APIKey { get; set; }
/// <summary>
/// 实际的GPT请求的数据字符串,直接序列化再传递就行
/// </summary>
public string dataString { get; set; }
}
public class GetTransferModel
{
/// <summary>
/// GPT的完整地址
/// </summary>
public string url { get; set; }
/// <summary>
/// 对应的API站的使用的APIkey
/// </summary>
public string? APIKey { get; set; }
}
+1 -1
View File
@@ -12,7 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LMS.Tools\LMS.Tools.csproj" /> <ProjectReference Include="..\LMS.Common\LMS.Common.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -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; }
}
}
@@ -0,0 +1,74 @@
namespace LMS.Repository.MJPackage
{
public class MJSubmitImageModel
{
/// <summary>
/// bot 类型,mj(默认)或niji
/// MID_JOURNEY | 枚举值: NIJI_JOURNEY
/// </summary>
public string? BotType { get; set; }
/// <summary>
/// 提示词。
/// </summary>
public string Prompt { get; set; }
/// <summary>
/// 垫图base64数组。
/// </summary>
public List<string>? Base64Array { get; set; }
/// <summary>
/// 账号过滤
/// </summary>
public AccountFilter? AccountFilter { get; set; }
/// <summary>
/// 自定义参数。
/// </summary>
public string? State { get; set; }
/// <summary>
/// 回调地址, 为空时使用全局notifyHook。
/// </summary>
public string? NotifyHook { get; set; }
}
public class AccountFilter
{
/// <summary>
/// 过滤指定实例的账号
/// </summary>
public string? InstanceId { get; set; }
/// <summary>
/// 账号模式 RELAX | FAST | TURBO
/// </summary>
public List<GenerationSpeedMode>? Modes { get; set; } = new List<GenerationSpeedMode>();
/// <summary>
/// 账号是否 remixMidjourney Remix
/// </summary>
public bool? Remix { get; set; }
/// <summary>
/// 账号是否 remixNijiourney Remix
/// </summary>
public bool? NijiRemix { get; set; }
/// <summary>
/// 账号过滤时,remix 自动提交视为账号的 remix 为 false
/// </summary>
public bool? RemixAutoConsidered { get; set; }
}
/// <summary>
/// 生成速度模式枚举.
/// </summary>
public enum GenerationSpeedMode
{
RELAX,
FAST,
TURBO
}
}
@@ -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
View File
@@ -0,0 +1,18 @@
namespace LMS.Repository.MJPackage
{
// Models/SyncResult.cs
public class SyncResult
{
public int TotalTokenCount { get; set; }
public int ActiveTokenCount { get; set; }
public int RecordsUpdated { get; set; }
}
public class TaskStatistics
{
public int TotalTasks { get; set; } = 0;
public int CompletedTasks { get; set; } = 0;
public int FailedTasks { get; set; } = 0;
public int InProgressTasks { get; set; } = 0;
}
}
@@ -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; } = [];
}
}
@@ -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; }
}
}
@@ -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; } // 历史使用记录
}
}
@@ -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; }
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using static LMS.Common.Enums.MachineEnum; using static LMS.Common.Enums.MachineEnum;
namespace LMS.Repository.Models.Machine namespace LMS.Repository.Machine
{ {
public class MachineModel public class MachineModel
{ {
+26
View File
@@ -0,0 +1,26 @@
using LMS.Common.Enums;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.Options;
public class AddOptionModel
{
[Required]
public required string Key { get; set; } = string.Empty;
/// <summary>
/// Value of the option,这个值是一个json字符串
/// </summary>
public string? Value { get; set; } = string.Empty;
[Required]
public OptionTypeEnum Type { get; set; } = OptionTypeEnum.String;
[Required]
public OptionCategory Category { get; set; } = OptionCategory.System;
[Required]
public List<string> RoleNames { get; set; }
}
@@ -0,0 +1,20 @@
using LMS.Common.Enums;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.Options;
public class ModifyOptionByKeyModel
{
/// <summary>
/// Value of the option,这个值是一个json字符串
/// </summary>
public required string Value { get; set; } = string.Empty;
public required OptionTypeEnum Type { get; set; } = OptionTypeEnum.String;
public required OptionCategory Category { get; set; } = OptionCategory.System;
[Required]
public required List<string> RoleNames { get; set; } = [];
}
@@ -0,0 +1,9 @@
namespace LMS.Repository.Options
{
public class ResetUserFreeCountSettingModel
{
public bool EnableMonthlyReset { get; set; } = false;
public int OnceFreeCount { get; set; } = 5;
}
}
+20
View File
@@ -0,0 +1,20 @@
using LMS.Common.Enums;
using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.Other
{
public class AddDataInfo
{
/// <summary>
/// 数据的类型
/// </summary>
[Required]
public required DataInfoTypeEnum Type { get; set; }
/// <summary>
/// 数据的字符串
/// </summary>
[Required]
public required string DataString { get; set; }
}
}
@@ -0,0 +1,45 @@
using LMS.Common.Enums;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
using static LMS.Common.Enums.SoftwareControlEnum;
namespace LMS.Repository.Other
{
public class AddMachineAuthorization
{
/// <summary>
/// 机器码或者是授权码
/// </summary>
public string? MachineID { get; set; }
/// <summary>
/// 可用时间
/// </summary>
[Required]
public required SoftwareControlValidityEnum ExpiryTime { get; set; }
/// <summary>
/// 过期日期
/// </summary>
public DateTime? ExpiryDate { get; set; }
/// <summary>
/// 授权使用状态
/// </summary>
[Required]
public required MachineAuthorizationUseTypeEnum UseType { get; set; }
/// <summary>
/// 授权码
/// </summary>
[Required]
public required string AuthorizationCode { get; set; }
/// <summary>
/// 授权软件类型
/// </summary>
[Required]
public required MachineAuthorizationEnum Type { get; set; }
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using static LMS.Common.Enums.PermissionEnum; using static LMS.Common.Enums.PermissionEnum;
namespace LMS.Repository.RequestModel.Permission namespace LMS.Repository.Promission
{ {
public class PermissionModel public class PermissionModel
{ {
@@ -1,7 +1,7 @@
using static LMS.Common.Enums.PermissionEnum; using static LMS.Common.Enums.PermissionEnum;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace LMS.Repository.Models.Promission namespace LMS.Repository.Promission
{ {
public class PermissionTypeModel public class PermissionTypeModel
{ {
@@ -1,4 +1,4 @@
using LMS.Common.Enum; using LMS.Common.Enums;
namespace LMS.Repository.Software namespace LMS.Repository.Software
{ {
+4 -1
View File
@@ -6,7 +6,8 @@ namespace LMS.Repository.Models.User
{ {
[Required] [Required]
public required string UserName { get; set; } public required string UserName { get; set; }
public string? Email { get; set; } [Required]
public required string Email { get; set; }
[Required] [Required]
public required string Password { get; set; } public required string Password { get; set; }
[Required] [Required]
@@ -14,5 +15,7 @@ namespace LMS.Repository.Models.User
[Required] [Required]
public required string AffiliateCode { get; set; } public required string AffiliateCode { get; set; }
public string? VerificationCode { get; set; }
} }
} }
@@ -1,15 +0,0 @@
namespace LMS.Tools.Extensions
{
public class BeijingTimeExtension
{
/// <summary>
/// 获取北京时间,将时区转换为北京时间
/// </summary>
/// <returns></returns>
public static DateTime GetBeijingTime()
{
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow,
TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"));
}
}
}
+70
View File
@@ -0,0 +1,70 @@
using LMS.DAO.OptionDAO;
using LMS.Tools.ImageTool;
using static LMS.Repository.DTO.FileUploadDto;
namespace LMS.Tools.FileTool
{
public static class FileService
{
public static byte[]? ConvertBase64ToBytes(string base64String)
{
if (string.IsNullOrEmpty(base64String))
{
return null;
}
// 检查是否以 "data:" 开头并包含 base64 编码,如果是则提取实际的 base64 部分
if (base64String.StartsWith("data:"))
{
// 提取 base64 编码部分
var commaIndex = base64String.IndexOf(',');
if (commaIndex >= 0)
{
base64String = base64String.Substring(commaIndex + 1);
}
}
try
{
// 尝试将 base64 字符串转换为字节数组
return Convert.FromBase64String(base64String);
}
catch (FormatException)
{
// 如果格式不正确,返回 null
return null;
}
}
public static bool IsValidImageFile(byte[] fileBytes)
{
if (fileBytes == null || fileBytes.Length == 0)
{
return false;
}
return ImageTypeDetector.IsValidImage(fileBytes);
}
public static async Task<UploadResult> CheckFileSize(byte[] fileBytes, double MaxFileSize)
{
if (fileBytes == null || fileBytes.Length == 0)
{
return new UploadResult
{
Success = false,
Message = "文件不能为空"
};
}
if (fileBytes.Length > MaxFileSize * 1024 * 1024)
{
return new UploadResult
{
Success = false,
Message = $"文件大小({fileBytes.Length} bytes)超过限制({MaxFileSize * 1024 * 1024} bytes)"
};
}
return new UploadResult { Success = true };
}
}
}
+60
View File
@@ -0,0 +1,60 @@
using LMS.Repository.DB;
using LMS.Repository.FileUpload;
using Qiniu.Http;
using static LMS.Repository.DTO.FileUploadDto;
namespace LMS.Tools.FileTool
{
public interface IQiniuService
{
/// <summary>
/// 检查文件的字节大小是否符合要求
/// </summary>
/// <param name="fileBytes"></param>
/// <returns></returns>
public Task<UploadResult> CheckFileBytesSize(byte[] fileBytes);
/// <summary>
/// 生成七牛云上传的路径 key
/// </summary>
/// <param name="userId"></param>
/// <param name="fileName"></param>
/// <returns></returns>
string GenerateFileKey(long userId, string fileName);
/// <summary>
/// 计算文件的 SHA1 哈希值
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
string ComputeSHA1Hash(byte[] data);
/// <summary>
/// 获取七牛云的配置 用于上传图片
/// </summary>
/// <returns></returns>
Task<QiniuSettings> InitQiniuSetting();
/// <summary>
/// 生成七牛的上传凭证
/// </summary>
/// <param name="qiniuSettings"></param>
/// <returns></returns>
string GeneratePolicy(QiniuSettings qiniuSettings);
/// <summary>
/// 将 byte 数组上传到七牛云
/// </summary>
/// <param name="fileBytes"></param>
/// <returns></returns>
Task<FileUploads> UploadFileToQiNiu(byte[] fileBytes, long userId, string fileName, string fileKey);
/// <summary>
/// 构建文件的访问 URL
/// </summary>
/// <param name="domain"></param>
/// <param name="fileKey"></param>
/// <returns></returns>
string BuildFileUrl(string domain, string fileKey);
}
}
+135
View File
@@ -0,0 +1,135 @@
using LMS.Common.Extensions;
using LMS.DAO.OptionDAO;
using LMS.Repository.DB;
using LMS.Repository.FileUpload;
using Microsoft.Extensions.Logging;
using Qiniu.Http;
using Qiniu.IO;
using Qiniu.IO.Model;
using Qiniu.Util;
using System.Security.Cryptography;
using static LMS.Repository.DTO.FileUploadDto;
namespace LMS.Tools.FileTool
{
public class QiniuService(OptionGlobalDAO optionGlobalDAO, ILogger<QiniuService> logger) : IQiniuService
{
private readonly OptionGlobalDAO _optionGlobalDAO = optionGlobalDAO;
private readonly UploadManager _uploadManager = new UploadManager();
private readonly ILogger<QiniuService> _logger = logger;
/// <summary>
/// 检查文件的字节大小是否符合要求
/// </summary>
/// <param name="fileBytes"></param>
/// <returns></returns>
public async Task<UploadResult> CheckFileBytesSize(byte[] fileBytes)
{
if (fileBytes == null || fileBytes.Length == 0)
{
return new UploadResult
{
Success = false,
Message = "文件字节数据不能为空"
};
}
double MaxFileSize = await _optionGlobalDAO.FindAndReturnOption<double>("SYS_MaxUploadFileSize");
if (fileBytes.Length > MaxFileSize * 1024 * 1024)
{
return new UploadResult
{
Success = false,
Message = $"文件大小不能超过 {MaxFileSize}MB"
};
}
return new UploadResult { Success = true, Message = string.Empty };
}
public string ComputeSHA1Hash(byte[] data)
{
var hash = SHA1.HashData(data);
return Convert.ToHexString(hash).ToLower();
}
public string GenerateFileKey(long userId, string fileName)
{
var date = DateTime.Now.ToString("yyyyMMdd");
//var extension = Path.GetExtension(fileName);
return $"user/{userId}/{date}/{fileName}";
}
public async Task<QiniuSettings> InitQiniuSetting()
{
QiniuSettings? qiniuSettings = await _optionGlobalDAO.FindAndReturnOption<QiniuSettings>("SYS_QiniuSetting");
if (qiniuSettings == null || string.IsNullOrEmpty(qiniuSettings.AccessKey) || string.IsNullOrEmpty(qiniuSettings.SecretKey) || string.IsNullOrEmpty(qiniuSettings.BucketName) || string.IsNullOrEmpty(qiniuSettings.Domain))
{
throw new Exception("七牛云配置不完整,请检查配置,请联系管理员");
}
return qiniuSettings;
}
public string GeneratePolicy(QiniuSettings qiniuSettings)
{
Mac mac = new(qiniuSettings.AccessKey, qiniuSettings.SecretKey);
var putPolicy = new PutPolicy
{
Scope = qiniuSettings.BucketName
};
if (qiniuSettings.DeleteDay != null)
{
putPolicy.DeleteAfterDays = qiniuSettings.DeleteDay.Value; // 设置过期时间
}
putPolicy.SetExpires(3600);
string token = Auth.CreateUploadToken(mac, putPolicy.ToJsonString());
return token;
}
public string BuildFileUrl(string domain, string fileKey)
{
return $"{domain}/{fileKey}";
}
public async Task<FileUploads> UploadFileToQiNiu(byte[] fileBytes, long userId, string fileName, string fileKey)
{
QiniuSettings qiniuSettings = await InitQiniuSetting();
string token = GeneratePolicy(qiniuSettings);
string hash = ComputeSHA1Hash(fileBytes);
HttpResult uploadResult;
_logger.LogInformation("开始上传文件, 用户ID: {userId}, 文件名: {fileName}, 文件大小: {fileLength} 字节, 文件Key: {fileKey}", userId, fileName, fileBytes.Length, fileKey);
using (var stream = new MemoryStream(fileBytes))
{
uploadResult = await _uploadManager.UploadStreamAsync(stream, fileKey, token);
}
// 8. 检查上传结果
if (uploadResult.Code != 200)
{
_logger.LogError("文件上传失败, 上传用户ID: {userId}, 错误信息: {error}", userId, uploadResult.Text);
throw new Exception(uploadResult.Text);
}
var qiniuUrl = BuildFileUrl(qiniuSettings.Domain, fileKey);
_logger.LogInformation("文件上传成功, 上传用户ID: {userId}, 文件Key: {fileKey},文件链接: {url}", userId, fileKey, qiniuUrl);
return new FileUploads
{
UserId = userId,
FileName = fileName,
FileKey = fileKey,
FileSize = fileBytes.Length,
ContentType = "application/octet-stream",
Hash = hash,
QiniuUrl = qiniuUrl,
UploadTime = DateTime.Now,
Status = "active",
CreatedAt = DateTime.Now,
DeleteTime = qiniuSettings.DeleteDay != null ? BeijingTimeExtension.GetBeijingTime().AddDays((double)qiniuSettings.DeleteDay) : new DateTime(2099, 12, 31, 23, 59, 59) // 默认未删除
};
}
}
}
+173
View File
@@ -0,0 +1,173 @@
using LMS.Tools.FileTool;
using Microsoft.Extensions.Logging;
using System.Text;
namespace LMS.Tools.HttpTool
{
/// <summary>
/// HTTP网络请求服务
/// </summary>
public class HttpService : IHttpService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<HttpService> _logger;
public HttpService(
IHttpClientFactory httpClientFactory,
ILogger<HttpService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
/// <summary>
/// 下载文件并返回字节数组
/// </summary>
/// <param name="url">文件URL</param>
/// <returns>文件字节数组</returns>
public async Task<byte[]?> DownloadFileAsync(string url, double maxFileSize)
{
try
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("URL不能为空", nameof(url));
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
throw new ArgumentException("无效的URL格式", nameof(url));
using var httpClient = _httpClientFactory.CreateClient("HttpService");
using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"HTTP请求失败,状态码: {response.StatusCode}");
}
// 检查文件大小
if (response.Content.Headers.ContentLength.HasValue)
{
if (response.Content.Headers.ContentLength.Value > maxFileSize * 1024 * 1024)
{
throw new InvalidOperationException($"文件大小({response.Content.Headers.ContentLength.Value} bytes)超过限制({maxFileSize * 1024 * 1024} bytes)");
}
}
var fileBytes = await response.Content.ReadAsByteArrayAsync();
if (fileBytes.Length > maxFileSize * 1024 * 1024)
{
throw new InvalidOperationException($"下载的文件大小({fileBytes.Length} bytes)超过限制({maxFileSize * 1024 * 1024} bytes)");
}
return fileBytes;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP请求异常: {Url}", url);
throw;
}
catch (TaskCanceledException ex)
{
_logger.LogError(ex, "请求超时: {Url}", url);
throw new TimeoutException("请求超时", ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "下载文件失败: {Url}", url);
throw;
}
}
/// <summary>
/// 发送GET请求
/// </summary>
/// <param name="url">请求URL</param>
/// <returns>响应内容</returns>
public async Task<string> GetAsync(string url)
{
try
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("URL不能为空", nameof(url));
using var httpClient = _httpClientFactory.CreateClient("HttpService");
var response = await httpClient.GetStringAsync(url);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "GET请求失败: {Url}", url);
throw;
}
}
/// <summary>
/// 发送POST请求
/// </summary>
/// <param name="url">请求URL</param>
/// <param name="content">请求内容</param>
/// <returns>响应内容</returns>
public async Task<string> PostAsync(string url, string content)
{
try
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("URL不能为空", nameof(url));
using var httpClient = _httpClientFactory.CreateClient("HttpService");
var httpContent = new StringContent(content, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(url, httpContent);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "POST请求失败: {Url}", url);
throw;
}
}
/// <summary>
/// 检查URL是否可访问
/// </summary>
/// <param name="url">要检查的URL</param>
/// <returns>是否可访问</returns>
public async Task<bool> IsUrlAccessibleAsync(string url)
{
try
{
using var httpClient = _httpClientFactory.CreateClient("HttpService");
using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
return response.IsSuccessStatusCode;
}
catch
{
return false;
}
}
/// <summary>
/// 获取URL的Content-Type
/// </summary>
/// <param name="url">要检查的URL</param>
/// <returns>Content-Type</returns>
public async Task<string?> GetContentTypeAsync(string url)
{
try
{
using var httpClient = _httpClientFactory.CreateClient("HttpService");
using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
return response.Content.Headers.ContentType?.MediaType;
}
return null;
}
catch
{
return null;
}
}
}
}
+45
View File
@@ -0,0 +1,45 @@
namespace LMS.Tools.HttpTool
{
/// <summary>
/// HTTP服务接口
/// </summary>
public interface IHttpService
{
/// <summary>
/// 下载文件并返回字节数组
/// </summary>
/// <param name="url">文件URL</param>
/// <param name="maxFileSize">最大文件大小限制(字节)</param>
/// <returns>文件字节数组</returns>
Task<byte[]?> DownloadFileAsync(string url, double maxFileSize);
/// <summary>
/// 发送GET请求
/// </summary>
/// <param name="url">请求URL</param>
/// <returns>响应内容</returns>
Task<string> GetAsync(string url);
/// <summary>
/// 发送POST请求
/// </summary>
/// <param name="url">请求URL</param>
/// <param name="content">请求内容</param>
/// <returns>响应内容</returns>
Task<string> PostAsync(string url, string content);
/// <summary>
/// 检查URL是否可访问
/// </summary>
/// <param name="url">要检查的URL</param>
/// <returns>是否可访问</returns>
Task<bool> IsUrlAccessibleAsync(string url);
/// <summary>
/// 获取URL的Content-Type
/// </summary>
/// <param name="url">要检查的URL</param>
/// <returns>Content-Type</returns>
Task<string?> GetContentTypeAsync(string url);
}
}
+66
View File
@@ -0,0 +1,66 @@
namespace LMS.Tools.ImageTool
{
public static class ImageTypeDetector
{
private static readonly Dictionary<string, byte[]> ImageSignatures = new()
{
{ "image/jpeg", new byte[] { 0xFF, 0xD8, 0xFF } },
{ "image/png", new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } },
{ "image/gif", new byte[] { 0x47, 0x49, 0x46, 0x38 } }, // GIF8
{ "image/bmp", new byte[] { 0x42, 0x4D } }, // BM
{ "image/webp", new byte[] { 0x52, 0x49, 0x46, 0x46 } } // RIFF (需要额外检查)
};
/// <summary>
/// 检查是否为支持的图片格式
/// </summary>
/// <param name="fileBytes">文件字节数组</param>
/// <returns>是否为图片</returns>
public static bool IsValidImage(byte[] fileBytes)
{
if (fileBytes == null || fileBytes.Length < 8)
return false;
// 检查 JPEG
if (StartsWithBytes(fileBytes, ImageSignatures["image/jpeg"]))
return true;
// 检查 PNG
if (StartsWithBytes(fileBytes, ImageSignatures["image/png"]))
return true;
// 检查 GIF
if (StartsWithBytes(fileBytes, ImageSignatures["image/gif"]))
return true;
// 检查 BMP
if (StartsWithBytes(fileBytes, ImageSignatures["image/bmp"]))
return true;
// 检查 WEBP (RIFF + WEBP标识)
if (StartsWithBytes(fileBytes, ImageSignatures["image/webp"]) &&
fileBytes.Length >= 12 &&
fileBytes[8] == 0x57 && fileBytes[9] == 0x45 &&
fileBytes[10] == 0x42 && fileBytes[11] == 0x50) // "WEBP"
{
return true;
}
return false;
}
private static bool StartsWithBytes(byte[] fileBytes, byte[] signature)
{
if (fileBytes.Length < signature.Length)
return false;
for (int i = 0; i < signature.Length; i++)
{
if (fileBytes[i] != signature[i])
return false;
}
return true;
}
}
}
+10 -1
View File
@@ -7,7 +7,16 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LMS.Common\LMS.Common.csproj" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Qiniu.Shared" Version="7.2.15" />
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LMS.DAO\LMS.DAO.csproj" />
<ProjectReference Include="..\LMS.Repository\LMS.Repository.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -0,0 +1,19 @@
using LMS.Repository.DB;
using LMS.Repository.MJPackage;
namespace LMS.Tools.MJPackage
{
// Services/ITaskConcurrencyManager.cs
public interface ITaskConcurrencyManager
{
Task CreateTaskAsync(string token, string thirdPartyTaskId, MJSubmitImageModel model);
Task CreateErrorTaskAsync(string token, MJSubmitImageModel model, string description);
Task UpdateTaskInDatabase(MJApiTasks mJApiTasks);
Task BatchUpdateTaskChaheToDatabaseAsync();
Task<MJApiTasks?> GetTaskInfoByThirdPartyIdAsync(string taskId);
}
}
+10
View File
@@ -0,0 +1,10 @@
using LMS.Repository.DB;
using Microsoft.AspNetCore.Mvc;
namespace LMS.Tools.MJPackage
{
public interface ITaskService
{
Task<Dictionary<string, object>?> FetchTaskAsync(MJApiTasks mJApiTasks);
}
}
+26
View File
@@ -0,0 +1,26 @@
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();
Task<string> LoadMJAPIBasicUrlAsync();
Task<string> GetMJAPIBasicUrl();
}
}
@@ -0,0 +1,292 @@
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 Newtonsoft.Json;
using System.Collections.Concurrent;
using System.Text;
namespace LMS.Tools.MJPackage
{
public class TaskConcurrencyManager : ITaskConcurrencyManager
{
private readonly TokenUsageTracker _usageTracker;
private readonly ILogger<TaskConcurrencyManager> _logger;
private readonly ApplicationDbContext _dbContext;
private readonly ITokenService _tokenService;
public TaskConcurrencyManager(
TokenUsageTracker usageTracker,
ILogger<TaskConcurrencyManager> logger,
ApplicationDbContext dbContext,
ITokenService tokenService)
{
_usageTracker = usageTracker;
_logger = logger;
_dbContext = dbContext;
_tokenService = tokenService;
}
/// <summary>
/// 尝试开始新任务(获取并发许可)
/// </summary>
public async Task CreateTaskAsync(
string token,
string thirdPartyTaskId,
MJSubmitImageModel model)
{
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 = JsonConvert.SerializeObject(new
{
id = thirdPartyTaskId,
action = "IMAGINE",
customId = "",
botType = "",
prompt = model.Prompt,
promptEn = "",
description = "提交成功",
state = "",
mode = "",
proxy = "",
submitTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
startTime = 0,
finishTime = 0,
imageUrl = "",
imageUrls = null as string[],
imageHeight = 0,
imageWidth = 0,
videoUrl = "",
status = "",
progress = "0%",
failReason = "",
buttons = null as object[],
maskBase64 = "",
properties = null as object,
}),
};
// 5. 持久化任务信息到数据库
await _dbContext.AddAsync(mJApiTasks);
await _dbContext.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"开始任务时发生错误: Token={token}");
}
}
public async Task CreateErrorTaskAsync(string token, MJSubmitImageModel model, string description)
{
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 thirdPartyTaskId = GenerateId().ToString();
var mJApiTasks = new MJApiTasks
{
TaskId = taskId,
Token = token,
TokenId = tokenConfig.Id,
StartTime = BeijingTimeExtension.GetBeijingTime(),
Status = MJTaskStatus.FAILURE,
ThirdPartyTaskId = thirdPartyTaskId,
Properties = JsonConvert.SerializeObject(new
{
id = thirdPartyTaskId,
action = "IMAGINE",
customId = "",
botType = "",
prompt = model.Prompt,
promptEn = "",
description,
state = "",
mode = "",
proxy = "",
submitTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
startTime = 0,
finishTime = 0,
imageUrl = "",
imageUrls = null as string[],
imageHeight = 0,
imageWidth = 0,
videoUrl = "",
status = MJTaskStatus.FAILURE,
progress = "0%",
failReason = description,
buttons = null as object[],
maskBase64 = "",
properties = null as object,
}),
};
// 5. 持久化任务信息到数据库
await _dbContext.AddAsync(mJApiTasks);
await _dbContext.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"开始任务时发生错误: Token={token}");
}
}
/// <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 = _usageTracker.TryGetTaskCache(thirdPartyId);
// 从数据库获取
mjApiTasks ??= await LoadTaskFromDatabaseByThirdPartyId(thirdPartyId);
if (mjApiTasks == null)
{
_logger.LogWarning($"缓存和数据库中均未找到任务: ThirdPartyTaskId={thirdPartyId}");
return null;
}
return mjApiTasks;
}
/// <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}");
}
}
public async Task BatchUpdateTaskChaheToDatabaseAsync()
{
var startTime = BeijingTimeExtension.GetBeijingTime();
try
{
// 获取所有缓存中的任务
var tasks = _usageTracker.GetAllTaskCaches();
if (tasks == null || tasks.Count == 0)
{
_logger.LogInformation("缓存中没有需要更新的任务");
return;
}
// 批量同步
var taskList = new List<MJApiTasks>();
foreach (var task in tasks)
{
// 从缓存中获取任务
MJApiTasks? mJApiTasks = _usageTracker.TryGetTaskCache(task.ThirdPartyTaskId);
if (mJApiTasks != null)
{
taskList.Add(mJApiTasks);
}
}
if (taskList.Count == 0)
{
_logger.LogInformation("缓存中没有需要更新的任务");
return;
}
// 批量更新到数据库
_dbContext.MJApiTasks.UpdateRange(taskList);
await _dbContext.SaveChangesAsync();
int count = 0;
// 删除缓存中状态为已完成的任务
for (int i = 0; i < taskList.Count; i++)
{
var task = taskList[i];
if (task.Status == MJTaskStatus.SUCCESS || task.Status == MJTaskStatus.FAILURE || task.Status == MJTaskStatus.CANCEL)
{
bool removeResult = _usageTracker.RemoveTaskCache(task.ThirdPartyTaskId);
if (removeResult == true)
{
count++;
}
}
}
var duration = BeijingTimeExtension.GetBeijingTime() - startTime;
_logger.LogInformation($"批量更新了 {taskList.Count} 个缓存中的任务到数据库,耗费时间: {duration.TotalMilliseconds}, 缓存中删除了 {count} 个完成的任务");
}
catch (Exception ex)
{
_logger.LogError(ex, "批量更新任务到数据库失败");
}
}
/// <summary>
/// 从数据库加载任务
/// </summary>
private async Task<MJApiTasks?> LoadTaskFromDatabaseByThirdPartyId(string thirdPartyId)
{
try
{
MJApiTasks? mJApiTasks = await _dbContext.MJApiTasks.FirstOrDefaultAsync(x => x.ThirdPartyTaskId == thirdPartyId);
if (mJApiTasks == null)
{
return null;
}
return mJApiTasks;
}
catch (Exception ex)
{
return null;
}
}
// 示例实现
private static long GenerateId()
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var random = new Random().Next(100, 999);
return long.Parse($"{timestamp}{random}");
}
}
}
+190
View File
@@ -0,0 +1,190 @@
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 originResult = await TryOriginApiAsync(mJApiTasks.ThirdPartyTaskId);
var properties = new Dictionary<string, object>();
if (string.IsNullOrWhiteSpace(originResult))
{
originResult = await TryBackupApiAsync(mJApiTasks.ThirdPartyTaskId, useToken);
}
if (string.IsNullOrWhiteSpace(originResult))
{
// 没有找到数据
_logger.LogInformation($"备用API没有返回数据,TaskId: {mJApiTasks.TaskId}");
return null;
}
try
{
// 不为空 开始解析数据
properties = JsonConvert.DeserializeObject<Dictionary<string, object>>(originResult);
}
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?> TryOriginApiAsync(string id)
{
string originUrl = $"https://mjapi.bzu.cn/mj/task/{id}/fetch";
// 判断 原始token 不存在 直接 返回空
string orginToken = await _tokenService.GetOriginToken();
if (string.IsNullOrWhiteSpace(orginToken))
{
return null;
}
try
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", orginToken);
client.Timeout = TimeSpan.FromSeconds(30);
var response = await client.GetAsync(originUrl);
var content = await response.Content.ReadAsStringAsync();
// 判断是不是返回空
if ((int)response.StatusCode == 204 || string.IsNullOrWhiteSpace(content))
{
return string.Empty;
}
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning($"源API调用返回错误状态码,TaskId: {id}, StatusCode: {response.StatusCode}");
return null;
}
return content;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "原始API调用失败,TaskId: {TaskId},准备尝试备用API", id);
return null;
}
}
private async Task<string?> TryBackupApiAsync(string id, string useToken)
{
string mjAPIBasicUrl = await _tokenService.GetMJAPIBasicUrl();
string backupUrl = $"{mjAPIBasicUrl}/mj/task/{id}/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);
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;
}
}
}
@@ -0,0 +1,326 @@
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, TokenUsageTracker tokenUsageTracker) : 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;
private readonly TokenUsageTracker _tokenUsageTracker = tokenUsageTracker;
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation($"开始检查TASK信息 - 检查间隔: 20 秒,同步加载 原始请求的Token!");
var startTime = BeijingTimeExtension.GetBeijingTime();
try
{
// 强制同步数据库数据,原请求Token
await _tokenService.LoadOriginTokenAsync();
// 强制同步数据 MJ API 的 Basic URL
await _tokenService.LoadMJAPIBasicUrlAsync();
// 检查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()).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);
// 尝试释放 当前缓存中的任务
_tokenUsageTracker.RemoveTaskCache(task.ThirdPartyTaskId);
_logger.LogWarning("任务轮询检查,未请求到对应的MJ数据,释放Token,释放任务" + task.Token);
_tokenUsageTracker.ReleaseConcurrencyPermit(task.Token);
}
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 (properties.TryGetValue("isPartner", out var isPartner))
{
if ((bool)isPartner == true)
{
properties = ProcessPartnerTaskDataAsync(properties);
}
}
else if (properties.TryGetValue("isOfficial", out var isOfficial))
{
if ((bool)isOfficial == true)
{
properties = ProcessOfficialTaskDataAsync(properties);
}
}
else if (properties.TryGetValue("isYouChuan", out var isYouChuan))
{
if ((bool)isYouChuan == true)
{
properties = ProcessYouChuanTaskDataAsync(properties);
}
}
if (status == MJTaskStatus.SUCCESS || status == MJTaskStatus.FAILURE || status == MJTaskStatus.CANCEL)
{
// 当前任务已经被释放过了
// 开始修改数据
task.EndTime = BeijingTimeExtension.GetBeijingTime();
task.Properties = JsonConvert.SerializeObject(properties);
_logger.LogInformation("任务轮询检查,已请求到对应的MJ数据,并且状态为成功,失败,取消,释放Token,释放任务" + task.Token);
_tokenUsageTracker.ReleaseConcurrencyPermit(task.Token);
// 尝试释放 当前缓存中的任务
_tokenUsageTracker.RemoveTaskCache(task.ThirdPartyTaskId);
}
else
{
// 任务还在处理中
task.EndTime = null; // 处理中没有结束时间
task.Properties = JsonConvert.SerializeObject(properties);
}
}
// 开始修改数据
await _taskConcurrencyManager.UpdateTaskInDatabase(task);
}
catch (Exception ex)
{
// 报错
_logger.LogError(ex, $"检查任务 {task.Token} 时发生错误,释放Token,释放任务", task.TaskId);
task.Status = MJTaskStatus.FAILURE;
var newProperties = new
{
failReason = "任务报错"
};
task.EndTime = BeijingTimeExtension.GetBeijingTime();
task.Properties = JsonConvert.SerializeObject(newProperties);
_tokenUsageTracker.ReleaseConcurrencyPermit(task.Token);
// 尝试释放 当前缓存中的任务
_tokenUsageTracker.RemoveTaskCache(task.ThirdPartyTaskId);
// 开始修改数据
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);
}
}
/// <summary>
/// 合作伙伴任务数据处理
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
public Dictionary<string, object> ProcessPartnerTaskDataAsync(Dictionary<string, object> properties)
{
try
{
string jsonStr = JsonConvert.SerializeObject(properties);
dynamic data = JsonConvert.DeserializeObject(jsonStr) ?? new object { };
// 检查是否是合作伙伴任务
// 直接用dynamic访问,不做类型判断
bool isPartner = data?.isPartner ?? false;
string partnerTaskId = data?.partnerTaskId ?? string.Empty;
if (isPartner && !string.IsNullOrEmpty(partnerTaskId))
{
_logger.LogInformation($"处理合作伙伴任务: {partnerTaskId}");
// 直接遍历,让异常处理兜底
var imageUrls = new List<object>();
foreach (var item in data?.partnerTaskInfo?.imgUrls ?? Array.Empty<object>())
{
string url = item?.url ?? string.Empty;
if (!string.IsNullOrEmpty(url))
{
imageUrls.Add(new { url });
}
}
if (imageUrls.Count > 0)
{
properties["imageUrls"] = imageUrls;
_logger.LogInformation($"成功提取了{imageUrls.Count}个图片URL");
}
else
{
_logger.LogInformation("未找到partnerTaskInfo或者是图片信信息");
}
}
else
{
_logger.LogInformation($"任务不是partner任务或缺少必要信息: isPartner={isPartner}, partnerTaskId={partnerTaskId}");
}
return properties;
}
catch (Exception ex)
{
_logger.LogError(ex, "处理任务数据时发生错误");
return properties; // 返回原始数据,避免处理错误导致数据丢失
}
}
/// <summary>
/// 官方任务数据处理
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
public Dictionary<string, object> ProcessOfficialTaskDataAsync(Dictionary<string, object> properties)
{
try
{
string jsonStr = JsonConvert.SerializeObject(properties);
dynamic data = JsonConvert.DeserializeObject(jsonStr) ?? new object { };
// 检查是否是合作伙伴任务
// 直接用dynamic访问,不做类型判断
bool isOfficial = data?.isOfficial ?? false;
string officialTaskId = data?.officialTaskId ?? string.Empty;
if (isOfficial && !string.IsNullOrEmpty(officialTaskId))
{
_logger.LogInformation($"处理官方任务: {officialTaskId}");
// 直接遍历,让异常处理兜底
var imageUrls = new List<object>();
foreach (var item in data?.officialTaskInfo?.imgUrls ?? Array.Empty<object>())
{
string url = item?.url ?? string.Empty;
if (!string.IsNullOrEmpty(url))
{
imageUrls.Add(new { url });
}
}
if (imageUrls.Count > 0)
{
properties["imageUrls"] = imageUrls;
_logger.LogInformation($"成功提取了{imageUrls.Count}个图片URL");
}
else
{
_logger.LogInformation("未找到officialTaskInfo或者是图片信信息");
}
}
else
{
_logger.LogInformation($"任务不是official任务或缺少必要信息: isOfficial={isOfficial}, officialTaskId={officialTaskId}");
}
return properties;
}
catch (Exception ex)
{
_logger.LogError(ex, "处理任务数据时发生错误");
return properties; // 返回原始数据,避免处理错误导致数据丢失
}
}
/// <summary>
/// 悠船任务数据处理
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
public Dictionary<string, object> ProcessYouChuanTaskDataAsync(Dictionary<string, object> properties)
{
try
{
string jsonStr = JsonConvert.SerializeObject(properties);
dynamic data = JsonConvert.DeserializeObject(jsonStr) ?? new object { };
// 检查是否是合作伙伴任务
// 直接用dynamic访问,不做类型判断
bool isYouChuan = data?.isYouChuan ?? false;
string youChuanTaskId = data?.youChuanTaskId ?? string.Empty;
if (isYouChuan && !string.IsNullOrEmpty(youChuanTaskId))
{
_logger.LogInformation($"处理悠船任务: {youChuanTaskId}");
// 直接遍历,让异常处理兜底
var imageUrls = new List<object>();
foreach (var item in data?.youChuanTaskInfo?.imgUrls ?? Array.Empty<object>())
{
string url = item?.url ?? string.Empty;
if (!string.IsNullOrEmpty(url))
{
imageUrls.Add(new { url });
}
}
if (imageUrls.Count > 0)
{
properties["imageUrls"] = imageUrls;
_logger.LogInformation($"成功提取了{imageUrls.Count}个图片URL");
}
else
{
_logger.LogInformation("未找到youChuanTaskInfo或者是图片信信息");
}
}
else
{
_logger.LogInformation($"任务不是YouChuan任务或缺少必要信息: isYouChuan={isYouChuan}, youChuanTaskId={youChuanTaskId}");
}
return properties;
}
catch (Exception ex)
{
_logger.LogError(ex, "处理任务数据时发生错误");
return properties; // 返回原始数据,避免处理错误导致数据丢失
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
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 TaskSyncService(
ILogger<TokenSyncService> logger,
ITaskConcurrencyManager taskConcurrencyManager) : IJob
{
private readonly ILogger<TokenSyncService> _logger = logger;
private readonly ITaskConcurrencyManager _taskConcurrencyManager = taskConcurrencyManager;
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation($"开始 Task 信息 - 同步间隔: 15 秒, (使用EF Core)");
await _taskConcurrencyManager.BatchUpdateTaskChaheToDatabaseAsync();
}
}
}
+24
View File
@@ -0,0 +1,24 @@
using LMS.Common.Extensions;
using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LMS.Tools.MJPackage
{
[DisallowConcurrentExecution]
public class TokenResetService(ITokenService tokenService, ILogger<TokenResetService> logger) : IJob
{
private readonly ITokenService _tokenService = tokenService;
private readonly ILogger<TokenResetService> _logger = logger;
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("开始每天重置 Token 日使用 统计数据, 执行时间 " + BeijingTimeExtension.GetBeijingTime());
await _tokenService.ResetDailyUsage();
}
}
}
+406
View File
@@ -0,0 +1,406 @@
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.Collections.Concurrent;
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;
// 在TokenService类内部
private static readonly ConcurrentDictionary<string, Lazy<Task<TokenCacheItem>>> _tokenLoadTasks = new();
/// <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"></param>
/// <returns></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. 使用Lazy确保任务只创建一次
var lazyTask = _tokenLoadTasks.GetOrAdd(token, _ => new Lazy<Task<TokenCacheItem>>(
() => LoadTokenFromDatabaseAsync(token),
LazyThreadSafetyMode.ExecutionAndPublication));
try
{
return await lazyTask.Value;
}
catch (Exception ex)
{
_logger.LogError(ex, $"加载Token时发生错误: {token}");
throw;
}
finally
{
// 查询完成后移除任务
_tokenLoadTasks.TryRemove(token, out _);
}
}
private async Task<TokenCacheItem> LoadTokenFromDatabaseAsync(string token)
{
_logger.LogInformation($"Token不在缓存中,从数据库加载: {token}");
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.DailyUsage}, 并发限制: {tokenItem.ConcurrencyLimit}");
return tokenItem;
}
/// <summary>
/// 增加Token使用量
/// </summary>
/// <param name="token">Token字符串</param>
public void IncrementUsage(string token)
{
_logger.LogInformation($"递增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();
}
public async Task<string> LoadMJAPIBasicUrlAsync()
{
// 没找到 从数据库中获取
Options? oprions = await _dbContext.Options.Where(x => x.Key == "MJAPIBasicUrl").FirstOrDefaultAsync();
if (oprions == null)
{
_logger.LogWarning("未找到配置的MJAPI Basic URL, 使用默认的!");
_usageTracker.MJAPIBasicUrl = string.Empty;
return _usageTracker.MJAPIBasicUrl;
}
else
{
// 处理数据
string mjBasicUrl = oprions.GetValueObject<string>() ?? string.Empty;
if (string.IsNullOrWhiteSpace(mjBasicUrl))
{
_logger.LogWarning("未找到配置的MJAPI Basic URL 数据, 使用默认的!");
_usageTracker.MJAPIBasicUrl = string.Empty;
return _usageTracker.MJAPIBasicUrl;
}
_usageTracker.MJAPIBasicUrl = mjBasicUrl;
return mjBasicUrl;
}
}
public async Task<string> GetMJAPIBasicUrl()
{
// 缓存中就有 直接返回
if (!string.IsNullOrWhiteSpace(_usageTracker.MJAPIBasicUrl))
{
return _usageTracker.MJAPIBasicUrl;
}
// 缓存中没有 从数据库中获取
return await LoadMJAPIBasicUrlAsync();
}
/// <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
View File
@@ -0,0 +1,198 @@
using LMS.Common.Extensions;
using LMS.DAO;
using LMS.Repository.MJPackage;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Quartz;
using System.Data;
using System.Text;
namespace LMS.Tools.MJPackage
{
[DisallowConcurrentExecution]
public class TokenSyncService : IJob
{
private readonly TokenUsageTracker _usageTracker;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<TokenSyncService> _logger;
// 活动阈值:5分钟内有活动的Token才同步
private readonly TimeSpan _activityThreshold = TimeSpan.FromMinutes(5);
public TokenSyncService(
TokenUsageTracker usageTracker,
IServiceProvider serviceProvider,
ILogger<TokenSyncService> logger)
{
_usageTracker = usageTracker;
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation($"开始同步Token信息 - 同步间隔: 30 秒, 活动阈值: {_activityThreshold.TotalMinutes}分钟 (使用EF Core)");
var startTime = BeijingTimeExtension.GetBeijingTime();
try
{
var syncResult = await SyncActiveTokensToDatabase();
var duration = BeijingTimeExtension.GetBeijingTime() - startTime;
if (syncResult.ActiveTokenCount > 0)
{
_logger.LogInformation(
"Token同步完成: {ActiveTokens}/{TotalTokens} 个活跃Token已同步, 耗时: {Duration}ms, 更新记录: {RecordsUpdated}",
syncResult.ActiveTokenCount,
syncResult.TotalTokenCount,
duration.TotalMilliseconds,
syncResult.RecordsUpdated);
}
else
{
_logger.LogDebug(
"Token同步跳过: 无活跃Token (总计: {TotalTokens}, 耗时: {Duration}ms)",
syncResult.TotalTokenCount,
duration.TotalMilliseconds);
}
}
catch (Exception ex)
{
var duration = BeijingTimeExtension.GetBeijingTime() - startTime;
_logger.LogError(ex, "Token同步失败,耗时: {Duration}ms", duration.TotalMilliseconds);
}
}
/// <summary>
/// 同步活跃Token数据到数据库
/// </summary>
/// <returns>同步结果</returns>
private async Task<SyncResult> SyncActiveTokensToDatabase()
{
// 先 删除10分钟内不活跃得Token
var (act, nact) = _usageTracker.RemoveNotActiveTokens(TimeSpan.FromMinutes(10));
_logger.LogInformation($"删除不活跃的 Token 数 {nact},删除后活跃 Token 数:{act},判断不活跃时间:{10} 分钟");
// 1. 获取活跃Token(最近5分钟内有活动的Token)
var activeTokens = _usageTracker.GetActiveTokens(_activityThreshold).ToList();
var totalTokens = _usageTracker.GetAllTokens().Count();
if (!activeTokens.Any())
{
_logger.LogInformation("0 条活跃Token,跳过同步!");
return new SyncResult
{
TotalTokenCount = totalTokens,
ActiveTokenCount = 0,
RecordsUpdated = 0
};
}
// 2. 创建数据库上下文
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var today = BeijingTimeExtension.GetBeijingTime().Date;
var recordsUpdated = 0;
// 3. 构造批量数据
var batchData = activeTokens.Select(token => new TokenUsageData
{
TokenId = token.Id,
Date = today,
DailyUsage = token.DailyUsage,
TotalUsage = token.TotalUsage,
LastActivityTime = token.LastActivityTime
}).ToList();
// 4. 使用EF Core事务批量更新数据库
using var transaction = await dbContext.Database.BeginTransactionAsync();
try
{
recordsUpdated = await BatchUpdateTokenUsageWithEfCore(dbContext, batchData);
await transaction.CommitAsync();
_logger.LogDebug(
"批量更新完成: {RecordsUpdated} 条活跃Token记录已更新,跳过 {SkippedCount} 条非活跃Token",
recordsUpdated,
totalTokens - activeTokens.Count);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "数据库事务失败,已回滚");
throw;
}
return new SyncResult
{
TotalTokenCount = totalTokens,
ActiveTokenCount = activeTokens.Count,
RecordsUpdated = recordsUpdated
};
}
/// <summary>
/// 使用EF Core批量更新Token使用数据
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="batchData">批量数据</param>
/// <returns>更新的记录数</returns>
private async Task<int> BatchUpdateTokenUsageWithEfCore(ApplicationDbContext dbContext, List<TokenUsageData> batchData)
{
int batchSize = 500;
if (!batchData.Any()) return 0;
var recordsUpdated = 0;
// 分批处理
for (int i = 0; i < batchData.Count; i += batchSize)
{
var batch = batchData.Skip(i).Take(batchSize).ToList();
// 构建真正的批量 SQL
var sqlBuilder = new StringBuilder();
var parameters = new List<object>();
sqlBuilder.AppendLine("INSERT INTO MJApiTokenUsage (TokenId, Date, DailyUsage, TotalUsage, LastActivityAt) VALUES ");
// 为每条记录构建 VALUES 子句
for (int j = 0; j < batch.Count; j++)
{
if (j > 0) sqlBuilder.Append(", ");
var paramIndex = j * 5;
sqlBuilder.Append($"({{{paramIndex}}}, {{{paramIndex + 1}}}, {{{paramIndex + 2}}}, {{{paramIndex + 3}}}, {{{paramIndex + 4}}})");
parameters.AddRange(new object[]
{
batch[j].TokenId,
batch[j].Date,
batch[j].DailyUsage,
batch[j].TotalUsage,
batch[j].LastActivityTime
});
}
sqlBuilder.AppendLine(@"
ON DUPLICATE KEY UPDATE
Date = VALUES(Date),
DailyUsage = VALUES(DailyUsage),
TotalUsage = VALUES(TotalUsage),
LastActivityAt = VALUES(LastActivityAt)");
// 一次性执行整个批次
var affectedRows = await dbContext.Database.ExecuteSqlRawAsync(
sqlBuilder.ToString(),
parameters.ToArray());
recordsUpdated += affectedRows;
_logger.LogInformation($"批量更新完成: 批次 {i / batchSize + 1}, 记录数: {batch.Count}, 影响行数: {affectedRows}");
}
return recordsUpdated;
}
}
}
+682
View File
@@ -0,0 +1,682 @@
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, MJApiTasks> _taskCache = new();
private readonly ConcurrentDictionary<string, Lazy<ConcurrencyController>> _concurrencyControllers = new();
private readonly ReaderWriterLockSlim _cacheLock = new(LockRecursionPolicy.SupportsRecursion);
private string _originToken = string.Empty;
private string _mjAPIBasicUrl = 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.LogInformation($"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.EnterWriteLock();
try
{
if (_tokenCache.TryGetValue(token, out var cacheItem))
{
int beforeDaily = cacheItem.DailyUsage;
int beforeTotal = cacheItem.TotalUsage;
cacheItem.DailyUsage++;
cacheItem.TotalUsage++;
cacheItem.LastActivityTime = BeijingTimeExtension.GetBeijingTime();
_logger.LogInformation($"Token使用量已更新: {token}, 今日使用: {beforeDaily} → {cacheItem.DailyUsage}, 总使用: {beforeTotal} → {cacheItem.TotalUsage}");
}
else
{
_logger.LogWarning($"尝试更新未缓存的Token使用量: {token}");
}
}
finally
{
_cacheLock.ExitWriteLock();
}
}
/// <summary>
/// 减少使用量 请求失败的时候 或者成功但是没有添加成功的时候
/// </summary>
/// <param name="token"></param>
public void DecrementUsage(string token)
{
_cacheLock.EnterWriteLock();
try
{
if (_tokenCache.TryGetValue(token, out var cacheItem))
{
int beforeDaily = cacheItem.DailyUsage;
int beforeTotal = cacheItem.TotalUsage;
// 确保不会减少到负数
if (cacheItem.DailyUsage > 0)
cacheItem.DailyUsage--;
if (cacheItem.TotalUsage > 0)
cacheItem.TotalUsage--;
cacheItem.LastActivityTime = BeijingTimeExtension.GetBeijingTime();
_logger.LogInformation($"Token使用量已减少: {token}, 今日使用: {beforeDaily} → {cacheItem.DailyUsage}, 总使用: {beforeTotal} → {cacheItem.TotalUsage}");
}
else
{
_logger.LogWarning($"尝试减少未缓存的Token使用量: {token}");
}
}
finally
{
_cacheLock.ExitWriteLock();
}
}
/// <summary>
/// 原子性检查并增加Token使用量(仅当未超出限制时增加)
/// </summary>
public bool CheckAndIncrementUsage(string token, int dailyLimit)
{
_cacheLock.EnterWriteLock(); // 直接使用写锁确保整个操作的原子性
try
{
if (_tokenCache.TryGetValue(token, out var cacheItem))
{
// 在同一个锁内检查和增加(原子操作)
if (dailyLimit > 0 && cacheItem.DailyUsage >= dailyLimit)
{
_logger.LogWarning($"Token日限制已达上限,拒绝请求: {token}, 当前: {cacheItem.DailyUsage}, 限制: {dailyLimit}");
return false; // 已达上限,拒绝增加
}
int beforeDaily = cacheItem.DailyUsage;
int beforeTotal = cacheItem.TotalUsage;
// 未达上限,增加计数
cacheItem.DailyUsage++;
cacheItem.TotalUsage++;
cacheItem.LastActivityTime = BeijingTimeExtension.GetBeijingTime();
_logger.LogInformation($"Token使用量已原子性更新: {token}, 今日使用: {beforeDaily} → {cacheItem.DailyUsage}, 总使用: {beforeTotal} → {cacheItem.TotalUsage}");
return true; // 计数成功增加
}
else
{
_logger.LogWarning($"尝试更新未缓存的Token使用量: {token}");
return false; // Token不在缓存中
}
}
finally
{
_cacheLock.ExitWriteLock();
}
}
/// <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为空值,可能请求原始的请求不可用!!");
}
}
}
public string MJAPIBasicUrl
{
get
{
if (_mjAPIBasicUrl.EndsWith('/'))
{
// 删除最后一个 /
return _mjAPIBasicUrl = _mjAPIBasicUrl.TrimEnd('/');
}
else
{
return _mjAPIBasicUrl.Trim();
}
}
set
{
if (!string.IsNullOrWhiteSpace(value))
{
_mjAPIBasicUrl = value;
}
else
{
// 设置初始值
_mjAPIBasicUrl = "https://laitool.net";
// 如果尝试设置为空值,记录警告日志,可能请求原始的请求不可用
_logger.LogWarning("尝试设置OriginToken为空值,可能请求原始的请求不可用!!");
}
}
}
public MJApiTasks? TryGetTaskCache(string thirdPartyId)
{
_cacheLock.EnterReadLock();
try
{
if (_taskCache.TryGetValue(thirdPartyId, out var task))
{
return task;
}
else
{
_logger.LogWarning($"未找到任务缓存: {thirdPartyId}");
return null;
}
}
finally
{
_cacheLock.ExitReadLock();
}
}
public List<MJApiTasks> GetAllTaskCaches()
{
_cacheLock.EnterReadLock();
try
{
return _taskCache.Values.ToList() ?? [];
}
finally
{
_cacheLock.ExitReadLock();
}
}
public bool AddOrUpdateTaskCache(MJApiTasks task)
{
_cacheLock.EnterWriteLock();
try
{
if (task == null || string.IsNullOrWhiteSpace(task.TaskId) || string.IsNullOrWhiteSpace(task.ThirdPartyTaskId))
{
_logger.LogWarning("尝试添加或更新任务缓存时,任务、任务ID或者第三方任务ID为空");
return false;
}
_taskCache[task.ThirdPartyTaskId] = task;
_logger.LogDebug($"任务缓存已增加或更新: {task.TaskId}, 状态: {task.Status}, 第三方任务ID: {task.ThirdPartyTaskId}");
return true;
}
finally
{
_cacheLock.ExitWriteLock();
}
}
public bool RemoveTaskCache(string thirdPartyId)
{
_cacheLock.EnterWriteLock();
try
{
if (_taskCache.TryRemove(thirdPartyId, out var removedTask))
{
_logger.LogInformation($"任务缓存已移除: {removedTask.TaskId}, 状态: {removedTask.Status}, 第三方任务ID: {removedTask.ThirdPartyTaskId}");
return true;
}
else
{
_logger.LogWarning($"尝试移除但未找到的第三方任务的缓存: {removedTask}");
return false;
}
}
finally
{
_cacheLock.ExitWriteLock();
}
}
}
}
@@ -0,0 +1,67 @@
using Quartz;
using Microsoft.AspNetCore.Identity;
using LMS.Repository.Models.DB;
using Microsoft.Extensions.Logging;
using LMS.Repository.DB;
using LMS.DAO;
using LMS.Common.Enums;
using Microsoft.EntityFrameworkCore;
using LMS.Repository.Options;
namespace LMS.Tools.TaskScheduler;
[DisallowConcurrentExecution]
public class ResetUserFreeCount(ILogger<ResetUserFreeCount> logger, UserManager<User> userManager, ApplicationDbContext context) : IJob
{
private readonly ILogger<ResetUserFreeCount> _logger = logger;
private readonly UserManager<User> _userManager = userManager;
private readonly ApplicationDbContext _context = context;
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation($"开始执行月度任务 - 当前时间: {DateTime.Now}");
await ProcessResetUserFreeCount();
_logger.LogInformation($"月度任务执行完成 - 当前时间: {DateTime.Now}");
}
private async Task ProcessResetUserFreeCount()
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// 这里是您的业务逻辑
_logger.LogInformation("正在重置用户余换绑次数数据...");
// 加载Options
Options? options = await _context.Options.FirstOrDefaultAsync(x => x.Key == OptionKeyName.ResetFreeCountSetting) ?? throw new Exception("未找到重置用户免费次数的设置,请检查!");
ResetUserFreeCountSettingModel resetFreeCountSetting = options.GetValueObject<ResetUserFreeCountSettingModel>() ?? new ResetUserFreeCountSettingModel();
if (resetFreeCountSetting.EnableMonthlyReset == false)
{
// 不允许重置
_logger.LogInformation("未开启用户重置换绑次数,有需要请开启!");
return;
}
// 获取所有用户
List<User> users = await _userManager.Users.ToListAsync();
// 修改所有的用户的免费次数
foreach (User user in users)
{
long machineCount = user.AllDeviceCount;
user.FreeCount = resetFreeCountSetting.OnceFreeCount * machineCount;
await _userManager.UpdateAsync(user);
}
await transaction.CommitAsync();
await _context.SaveChangesAsync();
_logger.LogInformation("重置用户余换绑次数数据成功!");
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "月度任务执行失败");
throw; // 重新抛出异常以便 Quartz 记录作业失败
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
using LMS.Tools; using LMS.Common.Extensions;
using static LMS.Common.Enums.ResponseCodeEnum; using static LMS.Common.Enums.ResponseCodeEnum;
namespace LMS.service namespace LMS.service
@@ -0,0 +1,29 @@
using Serilog;
namespace LMS.service.Configuration
{
public static class AddLoggerConfig
{
public static void AddLoggerService(this IServiceCollection services)
{
// 确保logs目录存在
Directory.CreateDirectory("logs");
// 加载配置
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
// 配置Serilog
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
// 添加Serilog到.NET Core的日志系统
services.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddSerilog(dispose: true);
});
}
}
}
@@ -2,7 +2,7 @@
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.Text; using System.Text;
namespace Lai_server.Configuration namespace LMS.service.Configuration
{ {
public static class AuthenticationExtensions public static class AuthenticationExtensions
{ {
@@ -1,17 +1,17 @@
using AutoMapper; using AutoMapper;
using LMS.Repository.DB; using LMS.Repository.DB;
using LMS.Repository.DTO; using LMS.Repository.DTO;
using LMS.Repository.DTO.OptionDto;
using LMS.Repository.DTO.PromptDto; using LMS.Repository.DTO.PromptDto;
using LMS.Repository.DTO.PromptTypeDto; using LMS.Repository.DTO.PromptTypeDto;
using LMS.Repository.DTO.UserDto; using LMS.Repository.DTO.UserDto;
using LMS.Repository.Machine;
using LMS.Repository.Models.DB; using LMS.Repository.Models.DB;
using LMS.Repository.Models.Machine; using LMS.Repository.Promission;
using LMS.Repository.Models.Promission;
using LMS.Repository.PromptModel; using LMS.Repository.PromptModel;
using LMS.Repository.RequestModel.Permission; using static LMS.Repository.DTO.MachineDto;
using static LMS.Repository.DTO.MachineResponse.MachineDto;
namespace Lai_server.Configuration namespace LMS.service.Configuration
{ {
public class AutoMapperConfig : Profile public class AutoMapperConfig : Profile
{ {
@@ -1,12 +1,10 @@
using LMS.DAO; using LMS.Common.Enums;
using LMS.Common.Extensions;
using LMS.DAO;
using LMS.Repository.DB; using LMS.Repository.DB;
using LMS.Repository.Models.DB; using LMS.Repository.Models.DB;
using LMS.Tools.Extensions;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using LMS.Common.Enum;
using System.Threading;
using static LMS.Common.Enums.ResponseCodeEnum;
namespace LMS.service.Configuration.InitConfiguration; namespace LMS.service.Configuration.InitConfiguration;
@@ -102,7 +100,11 @@ public class DatabaseConfiguration(IServiceProvider serviceProvider) : IHostedSe
new Options { Key = "LaitoolUpdateContent", Value = string.Empty, Type = OptionTypeEnum.String }, new Options { Key = "LaitoolUpdateContent", Value = string.Empty, Type = OptionTypeEnum.String },
new Options { Key = "LaitoolNotice", Value = string.Empty, Type = OptionTypeEnum.String }, new Options { Key = "LaitoolNotice", Value = string.Empty, Type = OptionTypeEnum.String },
new Options { Key = "LaitoolVersion", Value = string.Empty, Type = OptionTypeEnum.String }, new Options { Key = "LaitoolVersion", Value = string.Empty, Type = OptionTypeEnum.String },
new Options { Key = "LaiToolTrialDays", Value = "2" , Type = OptionTypeEnum.Number} new Options { Key = "LaiToolTrialDays", Value = "2" , Type = OptionTypeEnum.Number},
new Options { Key = OptionKeyName.LaitoolFluxApiModelList, Value = "{}" , Type = OptionTypeEnum.JSON },
new Options {Key = OptionKeyName.EnableMailService, Value = false.ToString(), Type = OptionTypeEnum.Boolean},
new Options {Key = OptionKeyName.SMTPMailSetting, Value ="{}" , Type = OptionTypeEnum.JSON },
new Options { Key = OptionKeyName.ResetFreeCountSetting, Value = "{}", Type = OptionTypeEnum.JSON},
]; ];
// 遍历所有的配置项,如果没有则添加 // 遍历所有的配置项,如果没有则添加
@@ -1,7 +1,7 @@
using LMS.Common.RSAKey; using LMS.Common.Extensions;
using LMS.Common.RSAKey;
using LMS.DAO; using LMS.DAO;
using LMS.Repository.DB; using LMS.Repository.DB;
using LMS.Tools.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace LMS.service.Configuration.InitConfiguration namespace LMS.service.Configuration.InitConfiguration
@@ -0,0 +1,113 @@
using LMS.Tools.MJPackage;
using LMS.Tools.TaskScheduler;
using Quartz;
public static class QuartzTaskSchedulerConfig
{
public static void AddQuartzTaskSchedulerService(this IServiceCollection services)
{
services.AddQuartz(q =>
{
// 时区配置
var chinaTimeZone = GetChinaTimeZone();
// 每月任务配置
ConfigureMonthlyTask(q, chinaTimeZone);
// 每日任务配置
ConfigureDailyTask(q, chinaTimeZone);
// 每30秒任务配置
ConfigureThirtySecondTask(q, chinaTimeZone);
ConfigureFiftySecondTask(q, chinaTimeZone);
ConfigureFiveMinuteTask(q, chinaTimeZone);
});
services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});
// 注册作业类
services.AddTransient<ResetUserFreeCount>();
services.AddTransient<TokenResetService>();
services.AddTransient<TokenSyncService>();
services.AddTransient<TaskStatusCheckService>();
services.AddTransient<TaskSyncService>();
}
private static TimeZoneInfo GetChinaTimeZone()
{
try
{
return TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");
}
catch
{
try
{
return TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai");
}
catch
{
return TimeZoneInfo.CreateCustomTimeZone(
"China_Custom",
new TimeSpan(8, 0, 0),
"China Custom Time",
"China Standard Time");
}
}
}
private static void ConfigureMonthlyTask(IServiceCollectionQuartzConfigurator q, TimeZoneInfo timeZone)
{
var jobKey = new JobKey("MonthlyTask", "DefaultGroup");
q.AddJob<ResetUserFreeCount>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("MonthlyTaskTrigger", "DefaultGroup")
.WithCronSchedule("0 0 0 1 * ?", x => x.InTimeZone(timeZone)));
}
private static void ConfigureDailyTask(IServiceCollectionQuartzConfigurator q, TimeZoneInfo timeZone)
{
var jobKey = new JobKey("DailyTask", "DefaultGroup");
q.AddJob<TokenResetService>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("DailyTaskTrigger", "DefaultGroup")
.WithCronSchedule("0 3 0 * * ?", x => x.InTimeZone(timeZone))); // 每天凌晨0点3分执行
}
private static void ConfigureThirtySecondTask(IServiceCollectionQuartzConfigurator q, TimeZoneInfo timeZone)
{
var jobKey = new JobKey("ThirtySecondTask", "DefaultGroup");
q.AddJob<TokenSyncService>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("ThirtySecondTaskTrigger", "DefaultGroup")
.WithCronSchedule("*/30 * * * * ?", x => x.InTimeZone(timeZone))); // 每30秒执行一次
}
private static void ConfigureFiftySecondTask(IServiceCollectionQuartzConfigurator q, TimeZoneInfo timeZone)
{
var jobKey = new JobKey("FiftySecondTask", "DefaultGroup");
q.AddJob<TaskSyncService>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("FiftySecondTaskTrigger", "DefaultGroup")
.WithCronSchedule("*/15 * * * * ?", 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("*/20 * * * * ?", x => x.InTimeZone(timeZone))); // 每5分钟执行一次
}
}
@@ -1,14 +1,22 @@
using LMS.DAO.MachineDAO; using LMS.DAO.MachineDAO;
using LMS.DAO.OptionDAO;
using LMS.DAO.PermissionDAO; using LMS.DAO.PermissionDAO;
using LMS.DAO.RoleDAO; using LMS.DAO.RoleDAO;
using LMS.DAO.UserDAO; using LMS.DAO.UserDAO;
using LMS.service.Configuration.InitConfiguration; using LMS.service.Configuration.InitConfiguration;
using LMS.service.Extensions.Mail;
using LMS.service.Service; using LMS.service.Service;
using LMS.service.Service.FileUploadService;
using LMS.service.Service.MJPackage;
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.FileTool;
using LMS.Tools.HttpTool;
using LMS.Tools.MJPackage;
namespace Lai_server.Configuration namespace Lai_server.Configuration
{ {
@@ -34,8 +42,10 @@ namespace Lai_server.Configuration
services.AddScoped<ForwardWordService>(); services.AddScoped<ForwardWordService>();
services.AddScoped<SoftwareControlService>(); services.AddScoped<SoftwareControlService>();
services.AddScoped<SoftwareService>(); services.AddScoped<SoftwareService>();
services.AddScoped<MachineAuthorizationService>();
services.AddScoped<DataInfoService>();
services.AddScoped<ITokenManagementService, TokenManagementService>();
services.AddScoped<IMJPackageService, MJPackageService>();
// 注入 DAO // 注入 DAO
services.AddScoped<UserBasicDao>(); services.AddScoped<UserBasicDao>();
@@ -43,7 +53,39 @@ namespace Lai_server.Configuration
services.AddScoped<MachineBasicDao>(); services.AddScoped<MachineBasicDao>();
services.AddScoped<PermissionBasicDao>(); services.AddScoped<PermissionBasicDao>();
services.AddScoped<PermissionTypeDao>(); services.AddScoped<PermissionTypeDao>();
services.AddScoped<OptionGlobalDAO>();
// 注入 Extensions
services.AddScoped<EmailService>();
services.AddScoped<EmailVerificationService>();
// 添加分布式缓存(用于存储验证码)
services.AddDistributedMemoryCache();
// 注册自定义服务
services.AddSingleton<TokenUsageTracker>();
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<ITaskConcurrencyManager, TaskConcurrencyManager>();
services.AddScoped<ITaskService, TaskService>();
services.AddScoped<IQiniuUploadService, QiniuUploadService>();
services.AddScoped<IQiniuService, QiniuService>();
// 注册HTTP服务(注意:由于HttpService是单例,这里使用工厂模式)
services.AddSingleton<IHttpService, HttpService>(); // 注册为单例
services.AddHttpClient("HttpService", client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
});
// 注册后台服务
services.AddHostedService<RsaConfigurattions>();
services.AddHostedService<DatabaseConfiguration>();
//services.AddHostedService<TokenSyncService>();
//services.AddHostedService<DailyResetService>();
} }
} }
} }
@@ -0,0 +1,70 @@
using LMS.Common.Extensions;
using LMS.Repository.DTO;
using LMS.service.Service.FileUploadService;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using static LMS.Repository.DTO.FileUploadDto;
using static LMS.Repository.FileUpload.FileRequestReturn;
namespace LMS.service.Controllers
{
[ApiController]
[Route("lms/[controller]/[action]")]
public class FileUploadController(IQiniuUploadService qiniuUploadService) : ControllerBase
{
private readonly IQiniuUploadService _qiniuUploadService = qiniuUploadService;
/// <summary>
/// 通过字节数组上传文件
/// </summary>
/// <param name="request">字节上传请求</param>
/// <returns></returns>
[HttpPost("{machineId}")]
public async Task<ActionResult<APIResponseModel<UploadResult>>> FileUpload(string machineId, [FromBody] ByteUploadRequest request)
{
return await _qiniuUploadService.UploadBase64Async(request, machineId);
}
/// <summary>
/// 获取用户文件列表,通过MachineId
/// </summary>
/// <param name="page">页码</param>
/// <param name="pageSize">每页数量</param>
/// <returns></returns>
[HttpGet("{machineId}")]
public async Task<ActionResult<APIResponseModel<CollectionResponse<FileMachineRequestReturn>>>> GetFilesByMachineId(string machineId, int page = 1, int pageSize = 10)
{
return await _qiniuUploadService.GetFilesByMachineId(machineId, page, pageSize);
}
/// <summary>
/// 获取指定ID的用户的文件列表
/// </summary>
/// <param name="userId"></param>
/// <param name="page"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
[HttpGet("{userId}")]
[Authorize]
public async Task<ActionResult<APIResponseModel<CollectionResponse<FileUserRequestReturn>>>> GetFilesByUser(long userId, [FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
long requestUserId = ConvertExtension.ObjectToLong(HttpContext.Items["UserId"] ?? 0);
return await _qiniuUploadService.GetFilesByUser(requestUserId, userId, page, pageSize);
}
/// <summary>
/// URL转存文件
/// </summary>
/// <param name="machineId"></param>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost("{machineId}")]
public async Task<ActionResult<APIResponseModel<UploadResult>>> UrlUpload(string machineId, [FromBody] UrlUploadRequest request)
{
return await _qiniuUploadService.UrlUpload(machineId, request);
}
}
}
+141 -1
View File
@@ -4,15 +4,21 @@ using LMS.service.Service;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using System.Text.Json;
using static LMS.Common.Enums.ResponseCodeEnum; using static LMS.Common.Enums.ResponseCodeEnum;
namespace LMS.service.Controllers; namespace LMS.service.Controllers;
[Route("lms/[controller]/[action]")] [Route("lms/[controller]/[action]")]
[ApiController] [ApiController]
public class ForwardController(ForwardWordService forwardWordService) : ControllerBase
public class ForwardController(ForwardWordService forwardWordService, ILogger<ForwardController> logger) : ControllerBase
{ {
private readonly ForwardWordService _forwardWordService = forwardWordService; private readonly ForwardWordService _forwardWordService = forwardWordService;
private readonly ILogger<ForwardController> _logger = logger;
#region
/// <summary> /// <summary>
/// 转发OpenAi格式的请求 /// 转发OpenAi格式的请求
@@ -29,6 +35,10 @@ public class ForwardController(ForwardWordService forwardWordService) : Controll
return await _forwardWordService.ForwardWord(request); return await _forwardWordService.ForwardWord(request);
} }
#endregion
#region ()
/// <summary> /// <summary>
/// 流式转发 /// 流式转发
/// </summary> /// </summary>
@@ -70,4 +80,134 @@ public class ForwardController(ForwardWordService forwardWordService) : Controll
return propertyName.ToLower(); return propertyName.ToLower();
} }
} }
#endregion
#region OpenAI
[HttpPost]
[Route("/lms/Forward/forward-stream-struct")]
public async Task<IActionResult> ForwardStreamStruct([FromBody] ForwardModelOpenAI req)
{
HttpResponseMessage? upstreamResponse = null;
try
{
// 1. Service 层请求 (保持不变,这里已经是 HeadersRead 模式了)
upstreamResponse = await _forwardWordService.ForwardWordStreamRaw(req);
}
catch (Exception e)
{
return BadRequest(e.Message);
}
try
{
// 2. 处理错误情况
if (!upstreamResponse.IsSuccessStatusCode)
{
Response.StatusCode = (int)upstreamResponse.StatusCode;
// 复制 Content-Type,防止前端解析乱码
if (upstreamResponse.Content.Headers.ContentType != null)
Response.ContentType = upstreamResponse.Content.Headers.ContentType.ToString();
var errorContent = await upstreamResponse.Content.ReadAsStringAsync();
await Response.WriteAsync(errorContent);
return new EmptyResult();
}
// 3. 成功连接,设置 SSE 响应头
Response.ContentType = "text/event-stream";
Response.Headers.Add("Cache-Control", "no-cache");
Response.Headers.Add("Connection", "keep-alive");
// 禁用缓冲 (对于某些服务器环境很重要)
// var responseFeature = HttpContext.Features.Get<IHttpResponseBodyFeature>();
// responseFeature?.DisableBuffering();
// 4. 【核心修改】手动流式转发循环
await using var upstreamStream = await upstreamResponse.Content.ReadAsStreamAsync();
// 定义一个较小的缓冲区,比如 1024 甚至更小,其实 buffer 大小不影响实时性,因为 ReadAsync 会在收到任何数据时立即返回
var buffer = new byte[4096];
int bytesRead;
// 使用 HttpContext.RequestAborted,这样前端断开时后端也会停止读取
while ((bytesRead = await upstreamStream.ReadAsync(buffer, HttpContext.RequestAborted)) != 0)
{
// 收到多少发多少
await Response.Body.WriteAsync(buffer.AsMemory(0, bytesRead), HttpContext.RequestAborted);
// 【关键】立刻刷新缓冲区,将数据强制推送到网络
await Response.Body.FlushAsync(HttpContext.RequestAborted);
}
}
catch (OperationCanceledException)
{
// 客户端(前端)主动断开连接,这是正常现象,不做处理
}
catch (Exception)
{
// 网络异常处理
if (!Response.HasStarted) return StatusCode(502);
}
finally
{
upstreamResponse?.Dispose();
}
return new EmptyResult();
}
#endregion
#region Post
/// <summary>
/// 转发所有的OpenAI格式的AI请求
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost]
public async Task<ActionResult<APIResponseModel<object>>> SimpleTransfer([FromBody] SimpleTransferModel request)
{
if (!ModelState.IsValid)
{
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.ParameterError);
}
return await _forwardWordService.SimpleTransfer(request);
}
#endregion
#region Get直接转发接口
[HttpPost]
public async Task<ActionResult<APIResponseModel<object>>> GetTransfer([FromBody] GetTransferModel getTransferModel)
{
if (!ModelState.IsValid)
{
return APIResponseModel<object>.CreateErrorResponseModel(ResponseCode.ParameterError);
}
return await _forwardWordService.GetTransfer(getTransferModel);
}
#endregion
[HttpPost]
[Route("/lms/v1/chat/completions")]
public async Task<ActionResult<APIResponseModel<object>>> Chat(JsonElement json)
{
_logger.LogInformation("Received chat request: {Json}", json.GetRawText());
return APIResponseModel<object>.CreateSuccessResponseModel("");
}
[HttpPost]
[Route("/lms/mj-relax/mj/submit/video")]
[Route("/lms/mj-fast/mj/submit/video")]
[Route("/lms/mj/submit/video")]
public async Task<ActionResult<APIResponseModel<object>>> Imagine(JsonElement json)
{
_logger.LogInformation("Received chat request: {Json}", json.GetRawText());
return APIResponseModel<object>.CreateSuccessResponseModel("");
}
} }

Some files were not shown because too many files have changed in this diff Show More