diff --git a/src/.config/dotnet-tools.json b/src/.config/dotnet-tools.json new file mode 100644 index 0000000..05b4dd6 --- /dev/null +++ b/src/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.6", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/src/Common/Enums/ResponseCode.cs b/src/Common/Enums/ResponseCode.cs new file mode 100644 index 0000000..22f428a --- /dev/null +++ b/src/Common/Enums/ResponseCode.cs @@ -0,0 +1,40 @@ +using lai_transfer.Tool.Attributes; + +namespace lai_transfer.Common.Enums; + +public enum ResponseCode +{ + + #region 成功 + + [Result("请求成功")] + [Description("请求成功")] + Success = 1, + + #endregion + + + #region 系统报错 + + [Result("系统错误")] + [Description("系统错误")] + SystemError = 5000, + + [Result("参数错误")] + [Description("参数错误")] + ParameterError = 5001, + + [Result("无效的操作")] + [Description("无效的操作")] + InvalidOptions = 5002, + + [Result("数据不存在")] + [Description("数据不存在")] + IdDateNotFound = 5003, + + [Result("操作错误")] + [Description("操作错误")] + OptionsError = 5004, + #endregion + +} \ No newline at end of file diff --git a/src/Common/Extensions/ClaimsPrincipalExtensions.cs b/src/Common/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000..5c5ed70 --- /dev/null +++ b/src/Common/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,16 @@ +using System.Security.Claims; + +namespace lai_transfer.Common.Extensions; + +public static class ClaimsPrincipalExtensions +{ + public static long GetUserId(this ClaimsPrincipal claimsPrincipal) + { + if (!int.TryParse(claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier), out var id)) + { + throw new InvalidOperationException("Invalid UserId"); + } + + return id; + } +} diff --git a/src/Common/Extensions/EndpointExtensions.cs b/src/Common/Extensions/EndpointExtensions.cs new file mode 100644 index 0000000..de883f8 --- /dev/null +++ b/src/Common/Extensions/EndpointExtensions.cs @@ -0,0 +1,62 @@ +using lai_transfer.Endpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.OpenApi.Models; + +namespace lai_transfer.Common.Extensions +{ + public static class EndpointExtensions + { + private static readonly OpenApiSecurityScheme SecurityScheme = new() + { + Type = SecuritySchemeType.Http, + Name = JwtBearerDefaults.AuthenticationScheme, + Scheme = JwtBearerDefaults.AuthenticationScheme, + Reference = new() + { + Type = ReferenceType.SecurityScheme, + Id = JwtBearerDefaults.AuthenticationScheme + } + }; + + /// + /// 无需授权的分组 + /// + /// + /// + /// + public static RouteGroupBuilder MapPublicGroup(this IEndpointRouteBuilder app, string? prefix = null) + { + return app.MapGroup(prefix ?? string.Empty) + .AllowAnonymous(); + } + + /// + /// 需要权限校验的分组 + /// + /// + /// + /// + public static RouteGroupBuilder MapAuthorizedGroup(this IEndpointRouteBuilder app, string? prefix = null) + { + return app.MapGroup(prefix ?? string.Empty) + .RequireAuthorization() + .WithOpenApi(x => new(x) + { + Security = [new() { [SecurityScheme] = [] }], + }); + } + + /// + /// 绑定一个 IEndpoint 实现到路由中 + /// + /// + /// + /// + public static IEndpointRouteBuilder MapEndpoint(this IEndpointRouteBuilder app) + where TEndpoint : IEndpoint + { + TEndpoint.Map(app); + return app; + } + } +} diff --git a/src/Common/Extensions/HttpContextExtensions.cs b/src/Common/Extensions/HttpContextExtensions.cs new file mode 100644 index 0000000..d22f83f --- /dev/null +++ b/src/Common/Extensions/HttpContextExtensions.cs @@ -0,0 +1,59 @@ +using lai_transfer.Common.Results; + +namespace lai_transfer.Common.Extensions +{ + + public static class HttpContextExtensions + { + /// + /// 从HttpContext中获取存储的值 + /// + /// Http上下文 + /// 要获取的键名(如"AuthToken"或"BaseUrl") + /// 存储的值,如果不存在则返回null + public static string? GetContextItem(this HttpContext httpContext, string key) + { + return httpContext.Items.TryGetValue(key, out var value) ? value?.ToString() : null; + } + + + /// + /// 获取 转发接口中的 httpContext 中的 Authorization 相关的 Token 和 BaseUrl数据 + /// + /// + /// + /// + public static TransferAuthorizationResult GetAuthorizationItemsAndValidation(this HttpContext httpContext) + { + string? token = httpContext.GetContextItem("AuthToken"); + string? baseUrl = httpContext.GetContextItem("BaseUrl"); + if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(baseUrl)) + { + throw new InvalidOperationException("Authentication token or base URL is not set in the context."); + } + return new TransferAuthorizationResult(token, baseUrl); + } + + /// + /// 获取当前请求的完整路径(不包括主机和端口) + /// + /// Http上下文 + /// 是否包含查询字符串 + /// 完整的请求路径 + public static string GetFullRequestPath(this HttpContext httpContext, bool includeQueryString = true) + { + string pathBase = httpContext.Request.PathBase.Value ?? string.Empty; + string path = httpContext.Request.Path.Value ?? string.Empty; + string fullPath = $"{pathBase}{path}"; + + if (includeQueryString) + { + string queryString = httpContext.Request.QueryString.Value ?? string.Empty; + fullPath += queryString; + } + + return fullPath; + } + + } +} diff --git a/src/Common/Extensions/RouteHandlerBuilderAuthorizationExtensions.cs b/src/Common/Extensions/RouteHandlerBuilderAuthorizationExtensions.cs new file mode 100644 index 0000000..465bcfd --- /dev/null +++ b/src/Common/Extensions/RouteHandlerBuilderAuthorizationExtensions.cs @@ -0,0 +1,20 @@ +using lai_transfer.Common.Filters; +using lai_transfer.Configuration; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace lai_transfer.Common.Extensions +{ + + public static class RouteHandlerBuilderAuthorizationExtensions + { + /// + /// 添加授权头处理过滤器 + /// + public static RouteHandlerBuilder WithMJAuthorizationHeader(this RouteHandlerBuilder builder) + { + return builder + .AddEndpointFilter() + .Produces(StatusCodes.Status401Unauthorized); + } + } +} diff --git a/src/Common/Extensions/RouteHandlerBuilderValidationExtensions.cs b/src/Common/Extensions/RouteHandlerBuilderValidationExtensions.cs new file mode 100644 index 0000000..ed1c9a5 --- /dev/null +++ b/src/Common/Extensions/RouteHandlerBuilderValidationExtensions.cs @@ -0,0 +1,65 @@ +using lai_transfer.Common.Filters; +using lai_transfer.Common.Types; +using lai_transfer.Configuration; +using System; +using System.Security.Principal; + +namespace lai_transfer.Common.Extensions +{ + public static class RouteHandlerBuilderValidationExtensions + { + /// + /// Adds a request validation filter to the route handler. + /// + /// + /// + /// A that can be used to futher customize the endpoint. + public static RouteHandlerBuilder WithRequestValidation(this RouteHandlerBuilder builder) + { + return builder + .AddEndpointFilter>() + .ProducesValidationProblem(); + } + + /// + /// Adds a request validation filter to the route handler to ensure a exists with the id returned by . + /// + /// + /// + /// + /// A function which selects the Id property from the + /// A that can be used to futher customize the endpoint. + public static RouteHandlerBuilder WithEnsureEntityExists(this RouteHandlerBuilder builder, Func idSelector) where TEntity : class, IEntity + { + return builder + .AddEndpointFilterFactory((endpointFilterFactoryContext, next) => async context => + { + var db = context.HttpContext.RequestServices.GetRequiredService(); + var filter = new EnsureEntityExistsFilter(db, idSelector); + return await filter.InvokeAsync(context, next); + }) + .ProducesProblem(StatusCodes.Status404NotFound); + } + + /// + /// Adds a request validation filter to the route handler to ensure the current owns the with the id returned by . + /// + /// + /// + /// + /// A function which selects the Id property from the + /// A that can be used to futher customize the endpoint. + public static RouteHandlerBuilder WithEnsureUserOwnsEntity(this RouteHandlerBuilder builder, Func idSelector) where TEntity : class, IEntity, IOwnedEntity + { + return builder + .AddEndpointFilterFactory((endpointFilterFactoryContext, next) => async context => + { + var db = context.HttpContext.RequestServices.GetRequiredService(); + var filter = new EnsureUserOwnsEntityFilter(db, idSelector); + return await filter.InvokeAsync(context, next); + }) + .ProducesProblem(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status403Forbidden); + } + } +} diff --git a/src/Common/Extensions/ValidationRulesExtension.cs b/src/Common/Extensions/ValidationRulesExtension.cs new file mode 100644 index 0000000..9f128ec --- /dev/null +++ b/src/Common/Extensions/ValidationRulesExtension.cs @@ -0,0 +1,24 @@ +using FluentValidation; + +namespace lai_transfer.Common.Extensions; + +public static class ValidationRulesExtension +{ + public static IRuleBuilderOptions AuthUsernameRule( + this IRuleBuilderInitial rule) + { + return rule.NotEmpty().WithMessage("用户名不能为空"); + } + + public static IRuleBuilderOptions AuthPasswordRule( + this IRuleBuilderInitial rule) + { + return rule.NotEmpty().MinimumLength(8); + } + + public static IRuleBuilderOptions AuthNameRule( + this IRuleBuilderInitial rule) + { + return rule.NotEmpty().MaximumLength(50); + } +} diff --git a/src/Common/Filters/EnsureEntityExistsFilter.cs b/src/Common/Filters/EnsureEntityExistsFilter.cs new file mode 100644 index 0000000..bce0bce --- /dev/null +++ b/src/Common/Filters/EnsureEntityExistsFilter.cs @@ -0,0 +1,30 @@ +using lai_transfer.Common.Results; +using lai_transfer.Common.Types; +using lai_transfer.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace lai_transfer.Common.Filters; + +public class EnsureEntityExistsFilter(ApplicationDbContext database, Func idSelector) : IEndpointFilter + where TEntity : class, IEntity +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var request = context.Arguments.OfType().Single(); + var cancellationToken = context.HttpContext.RequestAborted; + var id = idSelector(request); + + if (!id.HasValue) + { + return await next(context); + } + + var exists = await database + .Set() + .AnyAsync(x => x.Id == id, cancellationToken); + + return exists + ? await next(context) + : new NotFoundProblem($"{typeof(TEntity).Name} with id {id} was not found."); + } +} \ No newline at end of file diff --git a/src/Common/Filters/EnsureUserOwnsEntityFilter.cs b/src/Common/Filters/EnsureUserOwnsEntityFilter.cs new file mode 100644 index 0000000..0281898 --- /dev/null +++ b/src/Common/Filters/EnsureUserOwnsEntityFilter.cs @@ -0,0 +1,41 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Results; +using lai_transfer.Common.Types; +using lai_transfer.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace lai_transfer.Common.Filters; + +/// +/// 判断 对应 userId 的 Entity 数据是不是存在 +/// +/// +/// +/// +/// +public class EnsureUserOwnsEntityFilter(ApplicationDbContext database, Func idSelector) : IEndpointFilter + where TEntity : class, IEntity, IOwnedEntity +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var request = context.Arguments.OfType().Single(); + var cancellationToken = context.HttpContext.RequestAborted; + var userId = context.HttpContext.User.GetUserId(); + var id = idSelector(request); + + var entity = await database + .Set() + .Where(x => x.Id == id) + .Select(x => new Entity(x.Id, x.UserId)) + .SingleOrDefaultAsync(cancellationToken); + + return entity switch + { + null => new NotFoundProblem($"{typeof(TEntity).Name} with id {id} was not found."), + _ when entity.UserId != userId => TypedResults.Forbid(), + _ => await next(context) + }; + } + + private record Entity(int Id, int UserId); +} \ No newline at end of file diff --git a/src/Common/Filters/RequestLoggingFilter.cs b/src/Common/Filters/RequestLoggingFilter.cs new file mode 100644 index 0000000..94e39f5 --- /dev/null +++ b/src/Common/Filters/RequestLoggingFilter.cs @@ -0,0 +1,11 @@ +namespace lai_transfer.Common.Filters +{ + public class RequestLoggingFilter(ILogger logger) : IEndpointFilter + { + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + logger.LogInformation("HTTP {Method} {Path} received", context.HttpContext.Request.Method, context.HttpContext.Request.Path); + return await next(context); + } + } +} diff --git a/src/Common/Filters/RequestValidationFilter.cs b/src/Common/Filters/RequestValidationFilter.cs new file mode 100644 index 0000000..f06ab6d --- /dev/null +++ b/src/Common/Filters/RequestValidationFilter.cs @@ -0,0 +1,29 @@ +using FluentValidation; + +namespace lai_transfer.Common.Filters; + +public class RequestValidationFilter(ILogger> logger, IValidator? validator = null) : IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var requestName = typeof(TRequest).FullName; + + if (validator is null) + { + logger.LogInformation("{Request}: No validator configured.", requestName); + return await next(context); + } + + logger.LogInformation("{Request}: Validating...", requestName); + var request = context.Arguments.OfType().First(); + var validationResult = await validator.ValidateAsync(request, context.HttpContext.RequestAborted); + if (!validationResult.IsValid) + { + logger.LogWarning("{Request}: Validation failed.", requestName); + return TypedResults.ValidationProblem(validationResult.ToDictionary()); + } + + logger.LogInformation("{Request}: Validation succeeded.", requestName); + return await next(context); + } +} \ No newline at end of file diff --git a/src/Common/Filters/SplitMJAuthorizationFilter.cs b/src/Common/Filters/SplitMJAuthorizationFilter.cs new file mode 100644 index 0000000..ceef808 --- /dev/null +++ b/src/Common/Filters/SplitMJAuthorizationFilter.cs @@ -0,0 +1,69 @@ +namespace lai_transfer.Common.Filters +{ + public class SplitMJAuthorizationFilter(ILogger logger) : IEndpointFilter + { + private readonly ILogger _logger = logger; + public async ValueTask InvokeAsync( + EndpointFilterInvocationContext context, + EndpointFilterDelegate next) + { + // 1. 从请求头获取 Authorization + var httpContext = context.HttpContext; + + string authorization = string.Empty; + if (httpContext.Request.Headers.TryGetValue("mj-api-secret", out var mjSecret)) + { + authorization = mjSecret.ToString(); + } + else if (httpContext.Request.Headers.TryGetValue("Authorization", out var authHeader)) + { + authorization = authHeader.ToString(); + } + + if (string.IsNullOrWhiteSpace(authorization)) + { + _logger.LogWarning("Authorization header is missing"); + return TypedResults.Unauthorized(); + } + else + { + // 2. 处理令牌,判断是不是有前缀 删除前缀 + if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + authorization = authorization["Bearer ".Length..]; + } + if (authorization.Contains("?url=")) + { + // 使用Split方法拆分字符串 + string[] parts = authorization.Split("?url=", 2); + if (parts.Length == 2) + { + string token = parts[0].Trim(); + string baseUrl = parts[1].TrimEnd('/'); + if (string.IsNullOrWhiteSpace(authorization) || string.IsNullOrWhiteSpace(baseUrl)) + { + _logger.LogWarning("令牌或URL为空"); + return TypedResults.Unauthorized(); + } + httpContext.Items["AuthToken"] = token; + // 将baseUrl也存入HttpContext以便后续使用 + httpContext.Items["BaseUrl"] = baseUrl; + return await next(context); + } + else + { + _logger.LogWarning("令牌解析错误"); + return TypedResults.Unauthorized(); + } + } + else + { + _logger.LogWarning("令牌解析错误,没有包含 url"); + return TypedResults.Unauthorized(); + } + } + + + } + } +} \ No newline at end of file diff --git a/src/Common/Helper/ConfigHelper.cs b/src/Common/Helper/ConfigHelper.cs new file mode 100644 index 0000000..936d316 --- /dev/null +++ b/src/Common/Helper/ConfigHelper.cs @@ -0,0 +1,39 @@ +using lai_transfer.Tool.Extensions; + +namespace lai_transfer.Common.Helper +{ + public class ConfigHelper + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + + // 存储Origin配置 + public static class Origin + { + // 将private set改为internal set,允许同一程序集中的代码设置属性值 + public static string BaseUrl { get; internal set; } + public static string Token { get; internal set; } + } + + // 初始化配置 + public static void Initialize() + { + try + { + _logger.LogInformation("正在加载应用程序配置..."); + + // 读取配置文件 + var reader = new JsonConfigReader("Configuration/config/transfer.json"); + + // 加载Origin配置 + Origin.BaseUrl = reader.GetString("Origin.BaseUrl") ?? string.Empty; + Origin.Token = reader.GetString("Origin.Token") ?? string.Empty; + + _logger.LogInformation("配置加载完成"); + } + catch (Exception ex) + { + _logger.LogError(ex, "加载配置文件失败"); + } + } + } +} \ No newline at end of file diff --git a/src/Common/Helper/JSONHelper.cs b/src/Common/Helper/JSONHelper.cs new file mode 100644 index 0000000..7a24dbf --- /dev/null +++ b/src/Common/Helper/JSONHelper.cs @@ -0,0 +1,38 @@ +using System.Text; +using System.Text.Json; + +namespace lai_transfer.Common.Helper +{ + public static class JSONHelper + { + + /// + /// 高效移除JSON属性的方法,避免完全序列化和反序列化 + /// + public static string RemoveJsonProperties(JsonElement element, params string[] propertiesToRemove) + { + // 检查元素是否为对象 + if (element.ValueKind != JsonValueKind.Object) + return element.GetRawText(); + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + + writer.WriteStartObject(); + + // 遍历所有属性,跳过需要移除的 + foreach (var property in element.EnumerateObject()) + { + if (!propertiesToRemove.Contains(property.Name)) + { + property.WriteTo(writer); + } + } + + writer.WriteEndObject(); + writer.Flush(); + + return Encoding.UTF8.GetString(stream.ToArray()); + } + } +} diff --git a/src/Common/Helper/LogHelper.cs b/src/Common/Helper/LogHelper.cs new file mode 100644 index 0000000..d7de7f1 --- /dev/null +++ b/src/Common/Helper/LogHelper.cs @@ -0,0 +1,46 @@ +namespace lai_transfer.Common.Helper +{ + /// + /// 提供静态日志访问的帮助类 + /// + public static class LogHelper + { + private static ILoggerFactory? _factory; + + // 在应用启动时初始化 + public static void Initialize(ILoggerFactory factory) + { + _factory = factory; + } + + // 获取指定类型的日志记录器 + public static ILogger GetLogger() + { + if (_factory == null) + throw new InvalidOperationException("LogHelper未初始化。请在应用启动时调用Initialize方法。"); + + return _factory.CreateLogger(); + } + + // 获取指定类别名称的日志记录器 + public static ILogger GetLogger(string categoryName) + { + if (_factory == null) + throw new InvalidOperationException("LogHelper未初始化。请在应用启动时调用Initialize方法。"); + + return _factory.CreateLogger(categoryName); + } + + // 记录错误日志的快捷方法 + public static void LogError(Exception ex, string message, params object[] args) + { + GetLogger("GlobalErrorLogger").LogError(ex, message, args); + } + + // 记录信息日志的快捷方法 + public static void LogInfo(string message, params object[] args) + { + GetLogger("GlobalInfoLogger").LogInformation(message, args); + } + } +} \ No newline at end of file diff --git a/src/Common/Results/APIResponseModel.cs b/src/Common/Results/APIResponseModel.cs new file mode 100644 index 0000000..96ad505 --- /dev/null +++ b/src/Common/Results/APIResponseModel.cs @@ -0,0 +1,164 @@ +using lai_transfer.Common.Enums; +using lai_transfer.Tool.Extensions; + +namespace lai_transfer.Common.Results +{ + public class APIResponseModel + { + public APIResponseModel() + { + Code = (int)ResponseCode.Success; + Message = string.Empty; + Data = default(T); + } + + + /// + /// 返回的码 0,1 + /// + public int Code { get; set; } + + /// + /// 返回的信息,成功或者失败的信息 + /// + public string? Message { get; set; } + + /// + /// 返回的数据,可以是任何类型 + /// + public object? Data { get; set; } + + /// + /// 创建返回消息 + /// + /// 返回码 + /// 返回数据 + /// 返回消息 + /// + public static APIResponseModel CreateResponseModel(ResponseCode code, T data) + { + return new APIResponseModel + { + Code = (int)code, + Message = code.GetResult(), + Data = data + }; + } + + /// + /// 创建正常的返回数据 + /// + /// 返回的数据 + /// 返回成功的消息 + /// + public static APIResponseModel CreateSuccessResponseModel(T data, string? message = null) + { + return new APIResponseModel + { + Code = (int)ResponseCode.Success, + Message = message ?? "Requset SuccessFul", + Data = data + }; + } + + /// + /// 创建一个返回成功的消息 + /// + /// + /// + /// + public static APIResponseModel CreateSuccessResponseModel(string message) + { + // 判断 T 的类型是不是 MessageResult,是的话,构建Data + if (typeof(T) == typeof(IOperationResult)) + { + return new APIResponseModel + { + Code = (int)ResponseCode.Success, + Message = message, + Data = new MessageResult(message) + }; + } + return new APIResponseModel + { + Code = (int)ResponseCode.Success, + Message = message, + Data = null + }; + } + + /// + /// 创建正常的返回数据 + /// + /// 返回的数据 + /// + public static APIResponseModel CreateSuccessResponseModel(ResponseCode code) + { + return new APIResponseModel + { + Code = (int)code, + Message = code.GetResult(), + Data = null + }; + } + + /// + /// 返回错误消息 + /// + /// 错误的码 + /// 返回的数据 + /// + public static APIResponseModel CreateErrorResponseModel(ResponseCode code, T data) + { + return new APIResponseModel + { + Code = (int)code, + Message = code.GetResult(), + Data = data + }; + } + + /// + /// 返回错误消息 + /// + /// 错误的码 + /// 返回的数据 + /// 返回的错误消息 + /// + public static APIResponseModel CreateErrorResponseModel(ResponseCode code, T data, string message) + { + return new APIResponseModel + { + Code = (int)code, + Message = message, + Data = data + }; + } + + /// + /// 创建一个错误的返回数据,只有错误消息 + /// + /// 错误的码 + /// 错误消息提示 + /// + public static APIResponseModel CreateErrorResponseModel(ResponseCode code, string message) + { + // 判断 T 的类型是不是 MessageResult,是的话,构建Data + if (typeof(T) == typeof(IOperationResult)) + { + return new APIResponseModel + { + Code = (int)code, + Message = message, + Data = new ErrorResult(message) + }; + } + return new APIResponseModel + { + Code = (int)code, + Message = message, + Data = null + }; + } + } +} diff --git a/src/Common/Results/ErrorResult.cs b/src/Common/Results/ErrorResult.cs new file mode 100644 index 0000000..c504f33 --- /dev/null +++ b/src/Common/Results/ErrorResult.cs @@ -0,0 +1,4 @@ +namespace lai_transfer.Common.Results +{ + public record ErrorResult(string Error) : IOperationResult; +} diff --git a/src/Common/Results/IOperationResult.cs b/src/Common/Results/IOperationResult.cs new file mode 100644 index 0000000..94d40f0 --- /dev/null +++ b/src/Common/Results/IOperationResult.cs @@ -0,0 +1,6 @@ +namespace lai_transfer.Common.Results +{ + public interface IOperationResult + { + } +} diff --git a/src/Common/Results/MessageResult.cs b/src/Common/Results/MessageResult.cs new file mode 100644 index 0000000..7159ed0 --- /dev/null +++ b/src/Common/Results/MessageResult.cs @@ -0,0 +1,4 @@ +namespace lai_transfer.Common.Results +{ + public record MessageResult(string Message) : IOperationResult; +} diff --git a/src/Common/Results/NotFoundProblem.cs b/src/Common/Results/NotFoundProblem.cs new file mode 100644 index 0000000..e47b327 --- /dev/null +++ b/src/Common/Results/NotFoundProblem.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; +using System.Reflection; + +namespace lai_transfer.Common.Results; + +public sealed class NotFoundProblem : IResult, IEndpointMetadataProvider, IStatusCodeHttpResult, IContentTypeHttpResult, IValueHttpResult, IValueHttpResult +{ + private readonly ProblemHttpResult problem; + + public NotFoundProblem(string errorMessage) + { + problem = TypedResults.Problem + ( + statusCode: StatusCode, + title: "Not Found", + detail: errorMessage + ); + } + + public int? StatusCode => StatusCodes.Status404NotFound; + public string? ContentType => problem.ContentType; + public object? Value => problem.ProblemDetails; + ProblemDetails? IValueHttpResult.Value => problem.ProblemDetails; + + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(ProblemDetails), ["application/problem+json"])); + } + + public async Task ExecuteAsync(HttpContext httpContext) + { + await problem.ExecuteAsync(httpContext); + } +} \ No newline at end of file diff --git a/src/Common/Results/TransferAuthorizationResult.cs b/src/Common/Results/TransferAuthorizationResult.cs new file mode 100644 index 0000000..c23c770 --- /dev/null +++ b/src/Common/Results/TransferAuthorizationResult.cs @@ -0,0 +1,4 @@ +namespace lai_transfer.Common.Results +{ + public record TransferAuthorizationResult(string Token, string BaseUrl); +} diff --git a/src/Common/Results/TransferResult.cs b/src/Common/Results/TransferResult.cs new file mode 100644 index 0000000..cc000ff --- /dev/null +++ b/src/Common/Results/TransferResult.cs @@ -0,0 +1,10 @@ +namespace lai_transfer.Common.Results +{ + /// + /// 转发的请求返回 + /// + /// + /// + /// + public record TransferResult(string Content, string ContentType, int StatusCode); +} diff --git a/src/Common/Types/IEntity.cs b/src/Common/Types/IEntity.cs new file mode 100644 index 0000000..d32ba75 --- /dev/null +++ b/src/Common/Types/IEntity.cs @@ -0,0 +1,12 @@ +namespace lai_transfer.Common.Types; + +public interface IEntity +{ + int Id { get; } + Guid ReferenceId { get; } +} + +public interface IOwnedEntity +{ + int UserId { get; } +} diff --git a/src/Configuration/ApplicationDbContext.cs b/src/Configuration/ApplicationDbContext.cs new file mode 100644 index 0000000..ca8cb2b --- /dev/null +++ b/src/Configuration/ApplicationDbContext.cs @@ -0,0 +1,17 @@ +using lai_transfer.Model.Entity; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace lai_transfer.Configuration +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + } + } +} diff --git a/src/Configuration/MyDbcontextDesignFactory.cs b/src/Configuration/MyDbcontextDesignFactory.cs new file mode 100644 index 0000000..01761e6 --- /dev/null +++ b/src/Configuration/MyDbcontextDesignFactory.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace lai_transfer.Configuration +{ + public class MyDbcontextDesignFactory : IDesignTimeDbContextFactory + { + //public MyDbcontextDesignFactory CreateDbContext(string[] args) + //{ + // DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder(); + // string str = Environment.GetEnvironmentVariable("CONNECTION_STRING"); + // optionsBuilder.UseMySql(str, ServerVersion.Parse("8.0.18-mysql")); + // ApplicationDbContext db = new ApplicationDbContext(optionsBuilder.Options); + // return db; + //} + + public ApplicationDbContext CreateDbContext(string[] args) + { + DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder(); + string str = "server=123.129.219.240;port=14080;user=luo;password=Luoqiang1405;database=LMS_TEST_1"; + optionsBuilder.UseMySql(str, ServerVersion.Parse("8.0.18-mysql")); + ApplicationDbContext db = new ApplicationDbContext(optionsBuilder.Options); + return db; + } + } +} diff --git a/src/Configuration/SerilogConfig.cs b/src/Configuration/SerilogConfig.cs new file mode 100644 index 0000000..c5ef7bd --- /dev/null +++ b/src/Configuration/SerilogConfig.cs @@ -0,0 +1,31 @@ +using Serilog; + +namespace lai_transfer.Configuration +{ + public static class SerilogConfig + { + public static void AddLoggerService(this IServiceCollection services) + { + // 确保logs目录存在 + Directory.CreateDirectory("logs"); + + // 加载独立的serilog.json配置 + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("Configuration/config/serilog.json", optional: false, reloadOnChange: true) + .Build(); + + // 配置Serilog + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); + + // 添加Serilog到.NET Core的日志系统 + services.AddLogging(builder => + { + builder.ClearProviders(); + builder.AddSerilog(dispose: true); + }); + } + } +} diff --git a/src/Configuration/SwaggerConfig.cs b/src/Configuration/SwaggerConfig.cs new file mode 100644 index 0000000..d2c3637 --- /dev/null +++ b/src/Configuration/SwaggerConfig.cs @@ -0,0 +1,34 @@ +using Microsoft.OpenApi.Models; + +namespace lai_transfer.Configuration +{ + public static class SwaggerConfig + { + public static void AddSwaggerService(this IServiceCollection services) + { + services.AddSwaggerGen(c => + { + var scheme = new OpenApiSecurityScheme() + { + Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'", + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Authorization" + }, + Scheme = "oauth2", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + }; + c.AddSecurityDefinition("Authorization", scheme); + c.CustomSchemaIds(type => type.FullName?.Replace("+", ".")); + var requirement = new OpenApiSecurityRequirement + { + [scheme] = [] + }; + c.AddSecurityRequirement(requirement); + }); + } + } +} diff --git a/src/Configuration/config/serilog.json b/src/Configuration/config/serilog.json new file mode 100644 index 0000000..d8e29ec --- /dev/null +++ b/src/Configuration/config/serilog.json @@ -0,0 +1,23 @@ +{ + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "logs/app-.log", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}", + "retainedFileCountLimit": 31 + } + } + ], + "Enrich": [ "FromLogContext" ] + } +} \ No newline at end of file diff --git a/src/Configuration/config/transfer.json b/src/Configuration/config/transfer.json new file mode 100644 index 0000000..aa83fe3 --- /dev/null +++ b/src/Configuration/config/transfer.json @@ -0,0 +1,6 @@ +{ + "Origin": { + "BaseUrl": "https://mjapi.bzu.cn", + "Token": "23830faf80e8e69988b3fcd9aa08e9ad123" + } +} diff --git a/src/ConfigureApp.cs b/src/ConfigureApp.cs new file mode 100644 index 0000000..98cc86a --- /dev/null +++ b/src/ConfigureApp.cs @@ -0,0 +1,40 @@ +using lai_transfer.Endpoints; +using Serilog; + +namespace lai_transfer +{ + public static class ConfigureApp + { + /// + /// 添加APP配置 + /// + /// + public static void Configure(this WebApplication app) + { + // 配置 Swagger + app.SwaggerService(); + + app.UseHttpsRedirection(); + app.UseCors("AllowAll"); + app.MapEndpoints(); + //app.UseAuthentication(); + //app.UseAuthorization(); + } + + private static void SwaggerService(this WebApplication app) + { + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseWhen(context => context.Request.Path.Value == "/", builder => + builder.Run(context => + { + context.Response.Redirect("/swagger"); + return Task.CompletedTask; + })); + } + } + } +} diff --git a/src/ConfigureServices.cs b/src/ConfigureServices.cs new file mode 100644 index 0000000..d8bc178 --- /dev/null +++ b/src/ConfigureServices.cs @@ -0,0 +1,138 @@ +using FluentValidation; +using lai_transfer.Configuration; +using lai_transfer.Model.Entity; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Serilog; +using System.Text; + +namespace lai_transfer +{ + public static class ConfigureServices + { + /// + /// 添加 builder.Services 的配置 + /// + /// + public static void AddServices(this WebApplicationBuilder builder) + { + // 添加 Swagger 配置 + builder.Services.AddSwaggerService(); + + // 配置跨域 + builder.Services.AddCorsServices(); + + + // 添加日志配置 + builder.Services.AddLoggerService(); + builder.Host.UseSerilog(); + // 关键步骤:注册 Serilog.ILogger 到 DI 容器 + builder.Services.AddSingleton(Log.Logger); + + // 添加数据库连接 + builder.Services.AddDbConnection(); + + // 添加Ideneity配置 + builder.Services.AddIdeneity(); + + // 添加JWT认证配置 + builder.Services.AddJWTAuthentication(); + + // 注册校验 + builder.Services.AddValidatorsFromAssembly(typeof(ConfigureServices).Assembly); + + } + + /// + /// JWT 配置 + /// + /// + private static void AddJWTAuthentication(this IServiceCollection services) + { + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(x => + { + // TODO: 这里的密钥需要从配置文件中读取,或者从环境变量中读取 + string secKeyEnv = "secKeyEnv"; + //string secKeyEnv = Environment.GetEnvironmentVariable("SecKey"); + byte[] keyBytes = Encoding.UTF8.GetBytes(secKeyEnv); + var secKey = new SymmetricSecurityKey(keyBytes); + x.TokenValidationParameters = new() + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = secKey + }; + }); + } + + /// + /// 配置Identity服务配置 + /// + /// + private static void AddIdeneity(this IServiceCollection services) + { + services.AddIdentityCore(options => + { + options.SignIn.RequireConfirmedAccount = true; //已有账号才能登录 + options.SignIn.RequireConfirmedEmail = true; // + options.Password.RequireDigit = true; // 数据库中至少有一个数字 + options.Password.RequireLowercase = true; // 数据库中至少有一个小写字母 + options.Password.RequireUppercase = true; // 数据库中至少有一个大写字母 + options.Password.RequireNonAlphanumeric = true; // 数据库中至少有一个特殊字符 + options.Password.RequiredLength = 8; // 密码长度最少8位 + + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); // 锁定时间 + options.Lockout.MaxFailedAccessAttempts = 10; // 尝试次数 + options.Lockout.AllowedForNewUsers = true; // 新用户是否可以锁定 + + //options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; // 用户名允许的字符 + options.User.RequireUniqueEmail = true; // 允许重复邮箱 + }); + + var idBuilder = new IdentityBuilder(typeof(User), typeof(Role), services); + idBuilder.AddEntityFrameworkStores() + .AddDefaultTokenProviders() + .AddUserManager>() + .AddRoleManager>(); + + } + + /// + /// 配置数据库连接 + /// + /// + private static void AddDbConnection(this IServiceCollection services) + { + services.AddDbContext(options => + { + string connectionString = $"server=yisurds-66dc0b453c05d4.rds.ysydb1.com;port=14080;user=luo;password=Luoqiang1405;database=LMS_TEST_1;ConvertZeroDateTime=True;SslMode=None;"; + options.UseMySql(connectionString, ServerVersion.Parse("8.0.18-mysql")); + }); + } + + /// + /// 配置跨域 + /// + /// + + private static void AddCorsServices(this IServiceCollection services) + { + services.AddCors(options => + { + options.AddPolicy("AllowAll", + builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); + }); + } + + } +} diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..a5f9514 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,30 @@ +# 请参阅 https://aka.ms/customizecontainer 以了解如何自定义调试容器,以及 Visual Studio 如何使用此 Dockerfile 生成映像以更快地进行调试。 + +# 此阶段用于在快速模式(默认为调试配置)下从 VS 运行时 +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + + +# 此阶段用于生成服务项目 +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["lai_transfer.csproj", "."] +RUN dotnet restore "./lai_transfer.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "./lai_transfer.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# 此阶段用于发布要复制到最终阶段的服务项目 +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./lai_transfer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# 此阶段在生产中使用,或在常规模式下从 VS 运行时使用(在不使用调试配置时为默认值) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "lai_transfer.dll"] \ No newline at end of file diff --git a/src/EndpointServices/MJTransferEndpoint/MJGetFetchIdService.cs b/src/EndpointServices/MJTransferEndpoint/MJGetFetchIdService.cs new file mode 100644 index 0000000..c2e8374 --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJGetFetchIdService.cs @@ -0,0 +1,375 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using lai_transfer.Tool.Extensions; +using Newtonsoft.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJGetFetchIdService : IEndpoint + { + // 使用静态字段保存日志记录器 + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapGet("task/{id}/fetch", Handle) + .WithSummary("Midjourney 指定ID获取任务") + .WithMJAuthorizationHeader(); + + public static async Task Handle(string id, HttpContext httpContext) + { + // 先尝试调用原始API + TransferResult? originTransferResult = await TryOriginApiAsync(id); + if (originTransferResult != null) + { + return Results.Text(originTransferResult.Content, originTransferResult.ContentType, statusCode: originTransferResult.StatusCode); + } + // 原始请求失败,请求到指定的API + TransferResult transferResult = await SendUrlTaskFetchById(id, httpContext); + // 返回结果 + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + /// + /// 获取传入的 url 对应的任务 + /// + /// + /// + /// + public static async Task SendUrlTaskFetchById(string id, HttpContext httpContext) + { + try + { + //string baseUrl = + TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{transferAuthorizationResult.BaseUrl}/mj/task/{id}/fetch"; + + // 设置HttpClient + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + + // 发送请求 + var response = await client.GetAsync(url); + string content = await response.Content.ReadAsStringAsync(); + + // 返回结果 + return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, $"转发 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + } + + /// + /// 获取原始数据信息 bzu + /// + /// + /// + private static async Task TryOriginApiAsync(string id) + { + + string? baseUrl = ConfigHelper.Origin.BaseUrl; + string? token = ConfigHelper.Origin.Token; + + if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(token)) + { + _logger.LogWarning("配置文件中未找到 Origin.BaseUrl 或 Origin.Token"); + return null; + } + + string originUrl = $"{baseUrl.TrimEnd('/')}/mj/task/{id}/fetch"; + + try + { + using var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", token); + 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 null; + } + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning($"源API调用返回错误状态码,TaskId: {id}, StatusCode: {response.StatusCode}"); + return null; + } + + // 有返回结果 这这边开始处理返回的数据信息 + Dictionary? properties = ProcessTaskData(content); + if (properties != null) + { + return new TransferResult(JsonConvert.SerializeObject(properties), "application/json", (int)response.StatusCode); + } + + // 这边是原始请求失败 + return null; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "原始API调用失败,TaskId: {TaskId},准备尝试备用API", id); + return null; + } + } + + /// + /// 处理返回数据 + /// + /// + /// + private static Dictionary? ProcessTaskData(string content) + { + try + { + // 解析 JSON 数据 + var properties = JsonConvert.DeserializeObject>(content); + if (properties == null) + { + _logger.LogWarning("解析任务数据失败,返回内容为空或格式不正确"); + return null; + } + + 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); + } + } + return properties; + } + catch (Exception ex) + { + _logger.LogError(ex, "处理任务数据时发生错误"); + return null; + } + } + + public static Dictionary ProcessTaskObjectData(Dictionary properties) + { + try + { + 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); + } + } + return properties; + } + catch (Exception ex) + { + _logger.LogError(ex, "处理任务数据时发生错误"); + return null; + } + } + + + /// + /// 合作伙伴任务数据处理 + /// + /// + /// + public static Dictionary ProcessPartnerTaskDataAsync(Dictionary 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(); + + foreach (var item in data?.partnerTaskInfo?.imgUrls ?? Array.Empty()) + { + 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; // 返回原始数据,避免处理错误导致数据丢失 + } + } + + /// + /// 官方任务数据处理 + /// + /// + /// + public static Dictionary ProcessOfficialTaskDataAsync(Dictionary 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(); + + foreach (var item in data?.officialTaskInfo?.imgUrls ?? Array.Empty()) + { + 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; // 返回原始数据,避免处理错误导致数据丢失 + } + } + + /// + /// 悠船任务数据处理 + /// + /// + /// + public static Dictionary ProcessYouChuanTaskDataAsync(Dictionary 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(); + + foreach (var item in data?.youChuanTaskInfo?.imgUrls ?? Array.Empty()) + { + 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; // 返回原始数据,避免处理错误导致数据丢失 + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJGetImageSeedService.cs b/src/EndpointServices/MJTransferEndpoint/MJGetImageSeedService.cs new file mode 100644 index 0000000..13c4813 --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJGetImageSeedService.cs @@ -0,0 +1,51 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJGetImageSeedService : IEndpoint + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapGet("/task/{id}/image-seed", Handle) + .WithSummary("Midjourney 提交 Action 任务") + .WithMJAuthorizationHeader(); + + private static async Task Handle(string id, HttpContext httpContext) + { + TransferResult transferResult = await SendOriginalImageSeed(id, httpContext); + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + private static async Task SendOriginalImageSeed(string id, HttpContext httpContext) + { + try + { + //string baseUrl = + TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{transferAuthorizationResult.BaseUrl}/mj/task/{id}/image-seed"; + + // 设置HttpClient + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + + // 发送请求 + var response = await client.GetAsync(url); + string content = await response.Content.ReadAsStringAsync(); + + // 返回结果 + return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, $"转发 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostActionService.cs b/src/EndpointServices/MJTransferEndpoint/MJPostActionService.cs new file mode 100644 index 0000000..c4c175c --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostActionService.cs @@ -0,0 +1,50 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Configuration; +using lai_transfer.Endpoints; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostActionService : IEndpoint + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/submit/action", Handle) + .WithSummary("Midjourney 提交 Action 任务") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext) + { + (string content, string contentType, int statusCode) = await SendOriginalAction(model, httpContext); + return Results.Text(content, contentType, statusCode: statusCode); + } + + private static async Task SendOriginalAction(JsonElement model, HttpContext httpContext) + { + try + { + TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{authorizationResult.BaseUrl}/mj/submit/action"; + // 设置HttpClient + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + + string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); + // 发送请求 + var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); + string content = await response.Content.ReadAsStringAsync(); + return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, $"请求 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostBlendService.cs b/src/EndpointServices/MJTransferEndpoint/MJPostBlendService.cs new file mode 100644 index 0000000..102d491 --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostBlendService.cs @@ -0,0 +1,49 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostBlendService : IEndpoint + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/submit/blend", Handle) + .WithSummary("Midjourney 提交 Blend 任务") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext) + { + TransferResult transferResult = await SendOriginalBlend(model, httpContext); + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + private static async Task SendOriginalBlend(JsonElement model, HttpContext httpContext) + { + try + { + TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{authorizationResult.BaseUrl}/mj/submit/blend"; + // 设置HttpClient + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + // 删除回调参数 notifyHook + string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); + // 发送请求 + var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); + string content = await response.Content.ReadAsStringAsync(); + + return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, $"转发 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostDescribeService.cs b/src/EndpointServices/MJTransferEndpoint/MJPostDescribeService.cs new file mode 100644 index 0000000..35e937f --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostDescribeService.cs @@ -0,0 +1,50 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostDescribeService : IEndpoint + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/submit/describe", Handle) + .WithSummary("Midjourney 提交 Describe 任务") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext) + { + TransferResult transferResult = await SendOriginalDescribe(model, httpContext); + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + private static async Task SendOriginalDescribe(JsonElement model, HttpContext httpContext) + { + try + { + TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{authorizationResult.BaseUrl}/mj/submit/describe"; + // 设置HttpClient + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + + // 删除回调参数 notifyHook + string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); + // 发送请求 + var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); + string content = await response.Content.ReadAsStringAsync(); + + return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, $"转发 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostFetchListByConditionService.cs b/src/EndpointServices/MJTransferEndpoint/MJPostFetchListByConditionService.cs new file mode 100644 index 0000000..56aaf61 --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostFetchListByConditionService.cs @@ -0,0 +1,149 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using lai_transfer.Tool.Extensions; +using Microsoft.AspNetCore.Authorization; +using Newtonsoft.Json; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostFetchListByConditionService : IEndpoint + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/task/list-by-condition", Handle) + .WithSummary("Midjourney 根据ID列表查询任务") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext) + { + + // 先尝试调用原始API + TransferResult? originTransferResult = await TryOriginApiAsync(model); + if (originTransferResult != null) + { + return Results.Text(originTransferResult.Content, originTransferResult.ContentType, statusCode: originTransferResult.StatusCode); + } + + TransferResult transferResult = await SendOriginalFetchListByCondition(model, httpContext); + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + private static async Task SendOriginalFetchListByCondition(JsonElement model, HttpContext httpContext) + { + try + { + TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{transferAuthorizationResult.BaseUrl}/mj/task/list-by-condition"; + // 设置HttpClient + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + + string body = model.GetRawText(); + // 发送请求 + var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); + string content = await response.Content.ReadAsStringAsync(); + return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, $"转发 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + } + + /// + /// 获取原始数据信息 bzu + /// + /// + /// + private static async Task TryOriginApiAsync(JsonElement model) + { + // 如果要访问任意JSON节点,可以使用JsonConfigReader + //var reader = new JsonConfigReader("Configuration/config/transfer.json"); + string? baseUrl = ConfigHelper.Origin.BaseUrl; + string? token = ConfigHelper.Origin.Token; + + if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(token)) + { + _logger.LogWarning("配置文件中未找到 Origin.BaseUrl 或 Origin.Token"); + return null; + } + + string originUrl = $"{baseUrl.TrimEnd('/')}/mj/task/list-by-condition"; + + try + { + using var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", token); + client.Timeout = TimeSpan.FromSeconds(30); + + string body = model.GetRawText(); + var response = await client.PostAsync(originUrl, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); + var content = await response.Content.ReadAsStringAsync(); + + // 判断是不是返回空 + if ((int)response.StatusCode == 204 || string.IsNullOrWhiteSpace(content)) + { + return null; + } + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning($"源API调用 /mj/task/list-by-condition 返回错误状态码, StatusCode: {response.StatusCode}"); + return null; + } + + // 有返回结果 这这边开始处理返回的数据信息 + List>? properties = ProcessTaskArrayData(content); + if (properties != null && properties.Count > 0) + { + return new TransferResult( + JsonConvert.SerializeObject(properties), + "application/json", + StatusCodes.Status200OK); + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "原始API /mj/task/list-by-condition 调用失败"); + return null; + } + } + + private static List>? ProcessTaskArrayData(string content) + { + try + { + List> result = []; + // 解析 JSON 数据 + var jsonObject = JsonConvert.DeserializeObject>>(content); + if (jsonObject == null || jsonObject.Count == 0) + { + return []; + } + // 处理每个任务数据 + for (int i = 0; i < jsonObject.Count; i++) + { + var properties = MJGetFetchIdService.ProcessTaskObjectData(jsonObject[i]); + result.Add(properties); + } + + // 返回数据 + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "处理任务数据失败"); + return null; + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostFetchListByIdsService.cs b/src/EndpointServices/MJTransferEndpoint/MJPostFetchListByIdsService.cs new file mode 100644 index 0000000..88d3100 --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostFetchListByIdsService.cs @@ -0,0 +1,56 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using Microsoft.AspNetCore.Authorization; +using Newtonsoft.Json; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostFetchListByIdsService : IEndpoint + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/task/list-by-ids", Handle) + .WithSummary("Midjourney 根据ID列表查询任务-字段displays") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext) + { + TransferResult transferResult = await SendOriginalFetchListByIds(model, httpContext); + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + private static async Task SendOriginalFetchListByIds(JsonElement model, HttpContext httpContext) + { + try + { + //TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + //string url = $"{transferAuthorizationResult.BaseUrl}/mj/task/list-by-ids"; + //// 设置HttpClient + //using HttpClient client = new HttpClient(); + //client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}"); + //client.Timeout = Timeout.InfiniteTimeSpan; + + //string body = model.GetRawText(); + //// 发送请求 + //var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); + //string content = await response.Content.ReadAsStringAsync(); + //return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + string content = JsonConvert.SerializeObject(new + { + code = 6, + description = "该方法已停用!!" + }); + return await Task.FromResult(new TransferResult(content, "application/json", StatusCodes.Status200OK)); + } + catch (Exception ex) + { + _logger.LogError(ex, $"转发 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return await Task.FromResult(new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError)); + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostImagineService.cs b/src/EndpointServices/MJTransferEndpoint/MJPostImagineService.cs new file mode 100644 index 0000000..766c30f --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostImagineService.cs @@ -0,0 +1,58 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Configuration; +using lai_transfer.Endpoints; +using System.Text; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostImagineService : IEndpoint + { + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/submit/imagine", Handle) + .WithSummary("Midjourney 提交 Imagine 任务") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext, ILogger logger) + { + (string content, string contentType, int statusCode) = await SendOriginalImagine(model, httpContext, logger); + return Results.Text(content, contentType, statusCode: statusCode); + } + + /// + /// 实际转发请求的方法 + /// + /// + /// + /// + private static async Task<(string content, string contentType, int statusCode)> SendOriginalImagine(JsonElement model, HttpContext httpContext, ILogger logger) + { + try + { + TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{authorizationResult.BaseUrl}/mj/submit/imagine"; + + // 设置HttpClient + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + + // 删除回调参数 notifyHook + + string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); + // 发送请求 + var response = await client.PostAsync(url, new StringContent(body, Encoding.UTF8, "application/json")); + string content = await response.Content.ReadAsStringAsync(); + return (content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + logger.LogError(ex, $"请求 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return (ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostModalService.cs b/src/EndpointServices/MJTransferEndpoint/MJPostModalService.cs new file mode 100644 index 0000000..5bbdc5f --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostModalService.cs @@ -0,0 +1,48 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostModalService : IEndpoint + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/submit/modal", Handle) + .WithSummary("Midjourney 提交 Modal 任务") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext) + { + TransferResult transferResult = await SendOriginalModal(model, httpContext); + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + private static async Task SendOriginalModal(JsonElement model, HttpContext httpContext) + { + try + { + TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{transferAuthorizationResult.BaseUrl}/mj/submit/modal"; + // 设置HttpClient + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + // 删除回调参数 notifyHook + string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); + // 发送请求 + var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); + string content = await response.Content.ReadAsStringAsync(); + return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, $"转发 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostShortenService.cs b/src/EndpointServices/MJTransferEndpoint/MJPostShortenService.cs new file mode 100644 index 0000000..1d08e93 --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostShortenService.cs @@ -0,0 +1,49 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostShortenService : IEndpoint + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/submit/shorten", Handle) + .WithSummary("Midjourney 提交 Shorten 任务") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext) + { + TransferResult transferResult = await SendOriginalShorten(model, httpContext); + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + private static async Task SendOriginalShorten(JsonElement model, HttpContext httpContext) + { + try + { + TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{transferAuthorizationResult.BaseUrl}/mj/submit/shorten"; + + // 设置HttpClient + using HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + // 删除回调参数 notifyHook + string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); + // 发送请求 + var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); + string content = await response.Content.ReadAsStringAsync(); + return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, $"请求 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostSwapFaceService.cs b/src/EndpointServices/MJTransferEndpoint/MJPostSwapFaceService.cs new file mode 100644 index 0000000..9d82bb6 --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostSwapFaceService.cs @@ -0,0 +1,48 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostSwapFaceService : IEndpoint + { + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/insight-face/swap", Handle) + .WithSummary("Midjourney 提交 swap_face 任务") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext) + { + TransferResult transferResult = await SendOriginalSwapFace(model, httpContext); + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + private static async Task SendOriginalSwapFace(JsonElement model, HttpContext httpContext) + { + try + { + TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); + string url = $"{transferAuthorizationResult.BaseUrl}/mj/insight-face/swap"; + + // 设置HttpClient + using HttpClient client = new(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}"); + client.Timeout = Timeout.InfiniteTimeSpan; + // 删除回调参数 notifyHook + string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); + // 发送请求 + var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); + string content = await response.Content.ReadAsStringAsync(); + return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, $"转发 {httpContext.GetFullRequestPath()} 失败"); + return new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError); + } + } + } +} diff --git a/src/EndpointServices/MJTransferEndpoint/MJPostUploadDiscordImages.cs b/src/EndpointServices/MJTransferEndpoint/MJPostUploadDiscordImages.cs new file mode 100644 index 0000000..25af673 --- /dev/null +++ b/src/EndpointServices/MJTransferEndpoint/MJPostUploadDiscordImages.cs @@ -0,0 +1,44 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Helper; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; +using Newtonsoft.Json; +using System.Text.Json; + +namespace lai_transfer.EndpointServices.MJTransferEndpoint +{ + public class MJPostUploadDiscordImages : IEndpoint + { + + private static readonly ILogger _logger = LogHelper.GetLogger(); + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/submit/upload-discord-images", Handle) + .WithSummary("Midjourney 提交 上传文件到discord 任务(禁用)") + .WithMJAuthorizationHeader(); + + private static async Task Handle(JsonElement model, HttpContext httpContext) + { + TransferResult transferResult = await SendOriginalUploadDiscordImages(model, httpContext); + return Results.Text(transferResult.Content, transferResult.ContentType, statusCode: transferResult.StatusCode); + } + + private static async Task SendOriginalUploadDiscordImages(JsonElement model, HttpContext httpContext) + { + try + { + string content = JsonConvert.SerializeObject(new + { + code = 4, + description = "图片上传已禁用!!" + }); + return await Task.FromResult(new TransferResult(content, "application/json", StatusCodes.Status200OK)); + } + catch (Exception ex) + { + _logger.LogError(ex, $"转发 {httpContext.GetFullRequestPath()} 失败"); + // 处理异常,返回错误信息 + return await Task.FromResult(new TransferResult(ex.Message, "application/json", StatusCodes.Status500InternalServerError)); + } + } + } +} diff --git a/src/EndpointServices/UserEndpoint/UserRegisterService.cs b/src/EndpointServices/UserEndpoint/UserRegisterService.cs new file mode 100644 index 0000000..9cbd13e --- /dev/null +++ b/src/EndpointServices/UserEndpoint/UserRegisterService.cs @@ -0,0 +1,90 @@ +using FluentValidation; +using lai_transfer.Common.Enums; +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Results; +using lai_transfer.Configuration; +using lai_transfer.Endpoints; +using lai_transfer.Model.Entity; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace lai_transfer.EndpointServices.UserEndpoint +{ + public class UserRegisterService : IEndpoint + { + /// + /// 请求的参数 + /// + /// 用户名 必填 + /// 加密后的密码 必填 + /// 邮箱 必填 + /// 获取密钥的Key + /// 验证码 + public record Request(string UserName, string Password, string Mail, string TokenId, string AffiliateCode); + + public class RequestValidator : AbstractValidator + { + public RequestValidator() + { + RuleFor(x => x.UserName).NotEmpty().Length(3, 20); + RuleFor(x => x.Password).NotEmpty(); + RuleFor(x => x.Mail).NotEmpty().EmailAddress(); + RuleFor(x => x.TokenId).NotEmpty(); + RuleFor(x => x.AffiliateCode).NotEmpty(); + } + } + + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/register", Handle) + .WithSummary("用户注册") + .WithRequestValidation(); // 添加验证; + + private static async Task> Handle(Request request, ApplicationDbContext dbContext, ILogger logger, UserManager userManager) + { + using var transaction = await dbContext.Database.BeginTransactionAsync(); + try + { + // 1. 解密Password + + // 2. 判断邮箱是不是已使用 + + // 3. 判断用户名是不是已使用 + + // 4. 判断验证码是不是有效或被使用 + + // 5. 创建用户 + var user = new User { UserName = request.UserName, Email = request.Mail, NickName = request.UserName }; + + string decryptedPassword = request.Password; // 这里需要替换成实际的解密逻辑 + + var result = await userManager.CreateAsync(user, decryptedPassword); + if (!result.Succeeded) + { + foreach (var s in result.Errors) + { + return APIResponseModel.CreateErrorResponseModel(ResponseCode.OptionsError, s.Description); + } + } + try + { + await userManager.AddToRoleAsync(user, "Simple User"); + } + catch (Exception e) + { + // 如果添加角色失败,删除用户 + await userManager.DeleteAsync(user); + throw; + } + await dbContext.SaveChangesAsync(); + await transaction.CommitAsync(); + return APIResponseModel.CreateSuccessResponseModel("User Register Success"); + } + catch (Exception ex) + { + logger.LogError(ex, $"Registration failed for user {request.UserName}, mail {request.Mail}"); + await transaction.RollbackAsync(); + return APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, "Registration failed"); + } + } + } +} diff --git a/src/EndpointServices/WeatherForecast.cs b/src/EndpointServices/WeatherForecast.cs new file mode 100644 index 0000000..36522e2 --- /dev/null +++ b/src/EndpointServices/WeatherForecast.cs @@ -0,0 +1,44 @@ +using FluentValidation; +using lai_transfer.Common.Enums; +using lai_transfer.Common.Extensions; +using lai_transfer.Common.Results; +using lai_transfer.Endpoints; + +namespace lai_transfer.EndpointServices +{ + public class WeatherForecast : IEndpoint + { + public record Request(string Username, string Password, string Name); + public record Response(string Token) : IOperationResult; + + public class RequestValidator : AbstractValidator + { + public RequestValidator() + { + RuleFor(x => x.Username).NotEmpty(); + RuleFor(x => x.Password).NotEmpty(); + RuleFor(x => x.Name).NotEmpty(); + } + } + + public static void Map(IEndpointRouteBuilder app) => app + .MapPost("/signup", Handle) + .WithSummary("Creates a new user account") + .WithRequestValidation(); // 添加验证; + + private static Task> Handle(Request request) + { + try + { + // 模拟生成 JWT Token + var token = $"jwt_token_for_{request.Username}_{DateTime.UtcNow.Ticks}"; + var response = new Response(token); + return Task.FromResult(APIResponseModel.CreateSuccessResponseModel(response)); + } + catch (Exception) + { + return Task.FromResult(APIResponseModel.CreateErrorResponseModel(ResponseCode.SystemError, "Registration failed")); + } + } + } +} \ No newline at end of file diff --git a/src/Endpoints/IEndpoint.cs b/src/Endpoints/IEndpoint.cs new file mode 100644 index 0000000..56ed773 --- /dev/null +++ b/src/Endpoints/IEndpoint.cs @@ -0,0 +1,7 @@ +namespace lai_transfer.Endpoints +{ + public interface IEndpoint + { + static abstract void Map(IEndpointRouteBuilder app); + } +} diff --git a/src/Endpoints/MJEndpoint.cs b/src/Endpoints/MJEndpoint.cs new file mode 100644 index 0000000..bf374a5 --- /dev/null +++ b/src/Endpoints/MJEndpoint.cs @@ -0,0 +1,52 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.EndpointServices.MJTransferEndpoint; + +namespace lai_transfer.Endpoints +{ + public static class MJEndpoint + { + public static void MapMJEndpoint(this IEndpointRouteBuilder app) + { + // 创建基础路由组 + var baseGroup = app.MapGroup(""); + + var defaultGroup = baseGroup.MapGroup("/mj") + .WithTags("MidjourneyAPI-Default"); + + // 创建三个独立的路由组 + var relaxGroup = baseGroup.MapGroup("/mj-relax/mj") + .WithTags("MidjourneyAPI-Relax"); + + var fastGroup = baseGroup.MapGroup("/mj-fast/mj") + .WithTags("MidjourneyAPI-Fast"); + + var turboGroup = baseGroup.MapGroup("/mj-turbo/mj") + .WithTags("MidjourneyAPI-Turbo"); + + // 为每个路由组配置端点 + MapEndpointsForGroup(defaultGroup); + MapEndpointsForGroup(relaxGroup); + MapEndpointsForGroup(fastGroup); + MapEndpointsForGroup(turboGroup); + } + + private static void MapEndpointsForGroup(RouteGroupBuilder group) + { + group.MapPublicGroup() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint(); + + // 可以添加更多特定于此组的端点 + } + } +} \ No newline at end of file diff --git a/src/Endpoints/MainEndpoints.cs b/src/Endpoints/MainEndpoints.cs new file mode 100644 index 0000000..1b39a63 --- /dev/null +++ b/src/Endpoints/MainEndpoints.cs @@ -0,0 +1,23 @@ +using lai_transfer.Common.Filters; + +namespace lai_transfer.Endpoints +{ + public static class MainEndpoints + { + public static void MapEndpoints(this WebApplication app) + { + var endpoints = app.MapGroup("/api") + .AddEndpointFilter() + .WithOpenApi(); + + endpoints.MapWeatherForecastEndpoint(); + endpoints.MapUserEndpoint(); + + var emptyEndpoints = app.MapGroup("") + .AddEndpointFilter() + .WithOpenApi(); + emptyEndpoints.MapMJEndpoint(); + } + + } +} diff --git a/src/Endpoints/UserEndpoint.cs b/src/Endpoints/UserEndpoint.cs new file mode 100644 index 0000000..124cb22 --- /dev/null +++ b/src/Endpoints/UserEndpoint.cs @@ -0,0 +1,18 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.EndpointServices.UserEndpoint; + +namespace lai_transfer.Endpoints +{ + public static class UserEndpoint + { + public static void MapUserEndpoint(this IEndpointRouteBuilder app) + { + var endpoints = app.MapGroup("/user") + .WithTags("UserAPI"); + + endpoints.MapPublicGroup() + .MapEndpoint(); + } + + } +} diff --git a/src/Endpoints/WeatherForecastEndpoint.cs b/src/Endpoints/WeatherForecastEndpoint.cs new file mode 100644 index 0000000..d7cfdf4 --- /dev/null +++ b/src/Endpoints/WeatherForecastEndpoint.cs @@ -0,0 +1,18 @@ +using lai_transfer.Common.Extensions; +using lai_transfer.EndpointServices; + +namespace lai_transfer.Endpoints +{ + public static class WeatherForecastEndpoint + { + public static void MapWeatherForecastEndpoint(this IEndpointRouteBuilder app) + { + var endpoints = app.MapGroup("/auth") + .WithTags("Authentication"); + + endpoints.MapPublicGroup() + .MapEndpoint(); + } + + } +} diff --git a/src/Model/Entity/Role.cs b/src/Model/Entity/Role.cs new file mode 100644 index 0000000..fb6cc29 --- /dev/null +++ b/src/Model/Entity/Role.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Identity; + +namespace lai_transfer.Model.Entity +{ + public class Role : IdentityRole + { + public long CreatedUserId { get; set; } + + public long UpdatedUserId { get; set; } + + public DateTime CreatedTime { get; set; } + + public DateTime UpdatedTime { get; set; } + + public string? Remark { get; set; } = string.Empty; + } +} diff --git a/src/Model/Entity/User.cs b/src/Model/Entity/User.cs new file mode 100644 index 0000000..11df613 --- /dev/null +++ b/src/Model/Entity/User.cs @@ -0,0 +1,40 @@ +using lai_transfer.Tool.Extensions; +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace lai_transfer.Model.Entity +{ + public class User : IdentityUser + { + [MaxLength(50)] + public required string NickName { get; set; } + + [Column(TypeName = "datetime")] + public DateTime CreatedDate { get; set; } = BeijingTimeExtension.GetBeijingTime(); + + [Column(TypeName = "datetime")] + public DateTime UpdatedDate { get; set; } = BeijingTimeExtension.GetBeijingTime(); + + [Column(TypeName = "datetime")] + public DateTime LastLoginDate { get; set; } = BeijingTimeExtension.GetBeijingTime(); + + public string? LastLoginIp { get; set; } = ""; + + public string? LastLoginDevice { get; set; } = ""; + + + + /// + /// 用户微信号 + /// + public string? WXNumber { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } + + + } +} diff --git a/src/Model/Entity/UserRoles.cs b/src/Model/Entity/UserRoles.cs new file mode 100644 index 0000000..9ee8924 --- /dev/null +++ b/src/Model/Entity/UserRoles.cs @@ -0,0 +1,11 @@ +namespace lai_transfer.Model.Entity +{ + public class UserRoles + { + + public required string RoleId { get; set; } + + public required string UserId { get; set; } + + } +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..3f1281a --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,26 @@ +using lai_transfer; +using lai_transfer.Common.Helper; + +var builder = WebApplication.CreateBuilder(args); + + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); +builder.AddServices(); + +var app = builder.Build(); +app.Configure(); + +// ʼ־ +LogHelper.Initialize(app.Services.GetRequiredService()); +ConfigHelper.Initialize(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); +app.Run(); diff --git a/src/Properties/launchSettings.json b/src/Properties/launchSettings.json new file mode 100644 index 0000000..e248239 --- /dev/null +++ b/src/Properties/launchSettings.json @@ -0,0 +1,46 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5141" + }, + "https": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7199;http://localhost:5141" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14921/", + "sslPort": 44349 + } + } +} \ No newline at end of file diff --git a/src/Tool/Attributes/DescriptionAttribute.cs b/src/Tool/Attributes/DescriptionAttribute.cs new file mode 100644 index 0000000..da52cae --- /dev/null +++ b/src/Tool/Attributes/DescriptionAttribute.cs @@ -0,0 +1,13 @@ +namespace lai_transfer.Tool.Attributes +{ + [AttributeUsage(AttributeTargets.Field)] + public class DescriptionAttribute : Attribute + { + public string Description { get; set; } + + public DescriptionAttribute(string description) + { + Description = description; + } + } +} \ No newline at end of file diff --git a/src/Tool/Attributes/NotEmptyAttribute.cs b/src/Tool/Attributes/NotEmptyAttribute.cs new file mode 100644 index 0000000..7b4583f --- /dev/null +++ b/src/Tool/Attributes/NotEmptyAttribute.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace lai_transfer.Tool.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public class NotEmptyAttribute : ValidationAttribute + { + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + if (value is string str && string.IsNullOrWhiteSpace(str)) + { + return new ValidationResult(ErrorMessage ?? "字段不能为空"); + } + return ValidationResult.Success; + } + } +} diff --git a/src/Tool/Attributes/ResultAttribute.cs b/src/Tool/Attributes/ResultAttribute.cs new file mode 100644 index 0000000..62a04f8 --- /dev/null +++ b/src/Tool/Attributes/ResultAttribute.cs @@ -0,0 +1,13 @@ +namespace lai_transfer.Tool.Attributes +{ + [AttributeUsage(AttributeTargets.Field)] + public class ResultAttribute : Attribute + { + public string Result { get; set; } + + public ResultAttribute(string result) + { + Result = result; + } + } +} diff --git a/src/Tool/Extensions/BeijingTimeExtension.cs b/src/Tool/Extensions/BeijingTimeExtension.cs new file mode 100644 index 0000000..92bb986 --- /dev/null +++ b/src/Tool/Extensions/BeijingTimeExtension.cs @@ -0,0 +1,52 @@ +namespace lai_transfer.Tool.Extensions +{ + public class BeijingTimeExtension + { + /// + /// 获取北京时间,将时区转换为北京时间 + /// + /// + public static DateTime GetBeijingTime() + { + return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, + TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")); + } + + /// + /// 智能转换时间为北京时间 + /// 如果是UTC时间则转换,否则直接返回 + /// + /// 输入的时间 + /// 北京时间 + 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; + } + } +} diff --git a/src/Tool/Extensions/ConvertExtension.cs b/src/Tool/Extensions/ConvertExtension.cs new file mode 100644 index 0000000..08a717d --- /dev/null +++ b/src/Tool/Extensions/ConvertExtension.cs @@ -0,0 +1,61 @@ +namespace lai_transfer.Tool.Extensions +{ + public class ConvertExtension + { + /// + /// 将字符串转换为long,默认或者是转换错误返回0 + /// + /// + /// + public static long ObjectToLong(object obj) + { + if (obj == null) + return 0; + + if (obj is long longValue) + return longValue; + + if (obj is int intValue) + return intValue; + + if (obj is string strValue) + { + if (long.TryParse(strValue, out long result)) + return result; + } + + // 处理其他数值类型 + if (obj is IConvertible convertible) + { + try + { + return convertible.ToInt64(System.Globalization.CultureInfo.InvariantCulture); + } + catch + { + // 转换失败,返回0 + return 0; + } + } + + return 0; // 默认返回0 + } + + /// + /// 将字符串转换为int,默认或者是转换错误返回0 + /// + /// + /// + public static int ConvertStringToIntOrDefault(string input) + { + if (int.TryParse(input, out int result)) + { + return result; + } + else + { + return 0; + } + } + } +} diff --git a/src/Tool/Extensions/EnumExtensions.cs b/src/Tool/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..6595b25 --- /dev/null +++ b/src/Tool/Extensions/EnumExtensions.cs @@ -0,0 +1,73 @@ +using lai_transfer.Tool.Attributes; + +namespace lai_transfer.Tool.Extensions +{ + public static class EnumExtensions + { + /// + /// 判断是否为有效的权限类型 + /// + /// + /// + public static bool IsValidPermissionType(Type enumType, object value) + { + if (enumType == null || !enumType.IsEnum) + { + return false; + } + + if (value == null) + { + return false; + } + + // 处理整数类型 + if (value is int intValue) + { + return Enum.IsDefined(enumType, intValue); + } + + // 处理字符串类型 + if (value is string stringValue) + { + return Enum.TryParse(enumType, stringValue, true, out _); + } + + // 如果不是整数或字符串,尝试转换为枚举底层类型 + try + { + var underlyingType = Enum.GetUnderlyingType(enumType); + var convertedValue = Convert.ChangeType(value, underlyingType); + return Enum.IsDefined(enumType, convertedValue); + } + catch + { + return false; + } + } + + /// + /// 获取对应的枚举的描述 + /// + /// + /// + public static string GetDescription(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + var attribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute; + return attribute == null ? value.ToString() : attribute.Description; + } + + /// + /// 获取对应的枚举的结果 + /// + /// + /// + public static string GetResult(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + var attribute = Attribute.GetCustomAttribute(field, typeof(ResultAttribute)) as ResultAttribute; + return attribute == null ? value.ToString() : attribute.Result; + } + } +} diff --git a/src/Tool/Extensions/JsonConfigReader.cs b/src/Tool/Extensions/JsonConfigReader.cs new file mode 100644 index 0000000..0861714 --- /dev/null +++ b/src/Tool/Extensions/JsonConfigReader.cs @@ -0,0 +1,107 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace lai_transfer.Tool.Extensions +{ + /// + /// 通用JSON配置文件读取器 + /// + public class JsonConfigReader(string filePath) + { + private readonly string _filePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + private JsonNode? _rootNode; + private DateTime _lastReadTime = DateTime.MinValue; + private readonly TimeSpan _cacheTimeout = TimeSpan.FromMinutes(5); + + /// + /// 读取整个JSON文件并返回根节点 + /// + public JsonNode? GetRootNode(bool forceReload = false) + { + // 如果需要强制重新加载,或者缓存过期,或者尚未加载 + if (forceReload || _rootNode == null || DateTime.Now - _lastReadTime > _cacheTimeout) + { + string fullPath = Path.Combine(Directory.GetCurrentDirectory(), _filePath); + if (File.Exists(fullPath)) + { + string jsonContent = File.ReadAllText(_filePath); + _rootNode = JsonNode.Parse(jsonContent); + _lastReadTime = DateTime.Now; + } + else + { + throw new FileNotFoundException($"配置文件未找到: {_filePath}"); + } + } + + return _rootNode; + } + + /// + /// 获取指定路径的配置节点 + /// + /// 使用点分隔的路径,例如"Origin.BaseUrl" + public JsonNode? GetSection(string path, bool forceReload = false) + { + if (string.IsNullOrEmpty(path)) + return GetRootNode(forceReload); + + var root = GetRootNode(forceReload); + if (root == null) + return null; + + var pathSegments = path.Split('.'); + JsonNode? current = root; + + foreach (var segment in pathSegments) + { + current = current?[segment]; + if (current == null) + return null; + } + + return current; + } + + /// + /// 将指定路径的节点转换为指定类型 + /// + /// 目标类型 + /// 使用点分隔的路径,例如"Origin" + public T? GetValue(string path, bool forceReload = false) + { + var section = GetSection(path, forceReload); + if (section == null) + return default; + + return section.Deserialize(); + } + + /// + /// 获取指定路径的字符串值 + /// + public string? GetString(string path, bool forceReload = false) + { + var section = GetSection(path, forceReload); + return section?.GetValue(); + } + + /// + /// 获取指定路径的整数值 + /// + public int? GetInt(string path, bool forceReload = false) + { + var section = GetSection(path, forceReload); + return section?.GetValue(); + } + + /// + /// 获取指定路径的布尔值 + /// + public bool? GetBool(string path, bool forceReload = false) + { + var section = GetSection(path, forceReload); + return section?.GetValue(); + } + } +} \ No newline at end of file diff --git a/src/appsettings.Development.json b/src/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/appsettings.json b/src/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/docker_release.sh b/src/docker_release.sh new file mode 100644 index 0000000..12142bb --- /dev/null +++ b/src/docker_release.sh @@ -0,0 +1,14 @@ +#!/bin/bash +VERSION=$1 # 如 1.0.0 + +# 构建镜像 +docker build -t yuzhile/lai_transfer:latest . + +# 标记多个版本 +docker tag yuzhile/lai_transfer:latest yuzhile/lai_transfer:$VERSION + +# 推送所有标记 +docker push yuzhile/lai_transfer:latest +docker push yuzhile/lai_transfer:$VERSION + +echo "成功发布 yuzhile/lai_transfer 版本 $VERSION" \ No newline at end of file diff --git a/src/lai_transfer.csproj b/src/lai_transfer.csproj new file mode 100644 index 0000000..30be99c --- /dev/null +++ b/src/lai_transfer.csproj @@ -0,0 +1,39 @@ + + + + net9.0 + enable + enable + f320e299-f265-4200-aa03-9de3060d270c + docker.io + yuzhile/lai_transfer + latest + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/src/lai_transfer.http b/src/lai_transfer.http new file mode 100644 index 0000000..5452118 --- /dev/null +++ b/src/lai_transfer.http @@ -0,0 +1,6 @@ +@lai_transfer_HostAddress = http://localhost:5141 + +GET {{lai_transfer_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/lai_transfer.sln b/src/lai_transfer.sln new file mode 100644 index 0000000..0bf1602 --- /dev/null +++ b/src/lai_transfer.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36212.18 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "lai_transfer", "lai_transfer.csproj", "{3CC32194-3C1F-4EE6-BBE0-743958C0F3AB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3CC32194-3C1F-4EE6-BBE0-743958C0F3AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CC32194-3C1F-4EE6-BBE0-743958C0F3AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CC32194-3C1F-4EE6-BBE0-743958C0F3AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CC32194-3C1F-4EE6-BBE0-743958C0F3AB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {29435341-C82F-4586-ABFE-5CA094DAC509} + EndGlobalSection +EndGlobal