Compare commits
33
Commits
eb8a48c642
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0c94f3eac | ||
|
|
9edbe83376 | ||
|
|
8019b24b61 | ||
|
|
5875ffe671 | ||
|
|
6d41f52de5 | ||
|
|
25d481d7d6 | ||
|
|
aa0e7c27e5 | ||
|
|
145a47fa8d | ||
|
|
8f82da94b7 | ||
|
|
8b0db6a4e6 | ||
|
|
758735fefc | ||
|
|
ef46c30ed3 | ||
|
|
4340dd25a1 | ||
|
|
3470ce9229 | ||
|
|
6f580ff8eb | ||
|
|
b5ed6a3b1c | ||
|
|
7f269c8b04 | ||
|
|
c12e1b5d65 | ||
|
|
bc90b17961 | ||
|
|
3514cf53f8 | ||
|
|
c07369c297 | ||
|
|
aaebbb9104 | ||
|
|
647f2b75c9 | ||
|
|
0a2ba8bb1a | ||
|
|
61c4e0f977 | ||
|
|
a37c40a2ef | ||
|
|
a23984c9f5 | ||
|
|
57402e0dda | ||
|
|
1c5b9ed3c8 | ||
|
|
76b57c5717 | ||
|
|
01055762ca | ||
|
|
3934088e43 | ||
|
|
3f567cb481 |
@@ -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,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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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] }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace LMS.Common.Enum;
|
|
||||||
|
|
||||||
public enum OptionTypeEnum
|
|
||||||
{
|
|
||||||
String = 1,
|
|
||||||
JSON = 2,
|
|
||||||
Number = 3,
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
-1
@@ -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,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,4 +1,4 @@
|
|||||||
using LMS.Tools.Extensions;
|
using LMS.Common.Extensions;
|
||||||
|
|
||||||
namespace LMS.Repository.Models.DB
|
namespace LMS.Repository.Models.DB
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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,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,4 +1,4 @@
|
|||||||
using LMS.Tools.Extensions;
|
using LMS.Common.Extensions;
|
||||||
|
|
||||||
namespace LMS.Repository.DB
|
namespace LMS.Repository.DB
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
/// 账号是否 remix(Midjourney Remix)
|
||||||
|
/// </summary>
|
||||||
|
public bool? Remix { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 账号是否 remix(Nijiourney Remix)
|
||||||
|
/// </summary>
|
||||||
|
public bool? NijiRemix { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 账号过滤时,remix 自动提交视为账号的 remix 为 false
|
||||||
|
/// </summary>
|
||||||
|
public bool? RemixAutoConsidered { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成速度模式枚举.
|
||||||
|
/// </summary>
|
||||||
|
public enum GenerationSpeedMode
|
||||||
|
{
|
||||||
|
RELAX,
|
||||||
|
FAST,
|
||||||
|
TURBO
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) // 默认未删除
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; // 返回原始数据,避免处理错误导致数据丢失
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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} 的历史记录时发生错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user