添加项目文件。

This commit is contained in:
lq1405 2025-06-27 11:16:03 +08:00
parent 2515f555f8
commit 76efbd96f9
71 changed files with 3073 additions and 0 deletions

View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.6",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

30
src/.dockerignore Normal file
View File

@ -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/**

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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
}
};
/// <summary>
/// 无需授权的分组
/// </summary>
/// <param name="app"></param>
/// <param name="prefix"></param>
/// <returns></returns>
public static RouteGroupBuilder MapPublicGroup(this IEndpointRouteBuilder app, string? prefix = null)
{
return app.MapGroup(prefix ?? string.Empty)
.AllowAnonymous();
}
/// <summary>
/// 需要权限校验的分组
/// </summary>
/// <param name="app"></param>
/// <param name="prefix"></param>
/// <returns></returns>
public static RouteGroupBuilder MapAuthorizedGroup(this IEndpointRouteBuilder app, string? prefix = null)
{
return app.MapGroup(prefix ?? string.Empty)
.RequireAuthorization()
.WithOpenApi(x => new(x)
{
Security = [new() { [SecurityScheme] = [] }],
});
}
/// <summary>
/// 绑定一个 IEndpoint 实现到路由中
/// </summary>
/// <typeparam name="TEndpoint"></typeparam>
/// <param name="app"></param>
/// <returns></returns>
public static IEndpointRouteBuilder MapEndpoint<TEndpoint>(this IEndpointRouteBuilder app)
where TEndpoint : IEndpoint
{
TEndpoint.Map(app);
return app;
}
}
}

View File

@ -0,0 +1,59 @@
using lai_transfer.Common.Results;
namespace lai_transfer.Common.Extensions
{
public static class HttpContextExtensions
{
/// <summary>
/// 从HttpContext中获取存储的值
/// </summary>
/// <param name="httpContext">Http上下文</param>
/// <param name="key">要获取的键名(如"AuthToken"或"BaseUrl"</param>
/// <returns>存储的值如果不存在则返回null</returns>
public static string? GetContextItem(this HttpContext httpContext, string key)
{
return httpContext.Items.TryGetValue(key, out var value) ? value?.ToString() : null;
}
/// <summary>
/// 获取 转发接口中的 httpContext 中的 Authorization 相关的 Token 和 BaseUrl数据
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
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);
}
/// <summary>
/// 获取当前请求的完整路径(不包括主机和端口)
/// </summary>
/// <param name="httpContext">Http上下文</param>
/// <param name="includeQueryString">是否包含查询字符串</param>
/// <returns>完整的请求路径</returns>
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;
}
}
}

View File

@ -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
{
/// <summary>
/// 添加授权头处理过滤器
/// </summary>
public static RouteHandlerBuilder WithMJAuthorizationHeader(this RouteHandlerBuilder builder)
{
return builder
.AddEndpointFilter<SplitMJAuthorizationFilter>()
.Produces(StatusCodes.Status401Unauthorized);
}
}
}

View File

@ -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
{
/// <summary>
/// Adds a request validation filter to the route handler.
/// </summary>
/// <typeparam name="TRequest"></typeparam>
/// <param name="builder"></param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to futher customize the endpoint.</returns>
public static RouteHandlerBuilder WithRequestValidation<TRequest>(this RouteHandlerBuilder builder)
{
return builder
.AddEndpointFilter<RequestValidationFilter<TRequest>>()
.ProducesValidationProblem();
}
/// <summary>
/// Adds a request validation filter to the route handler to ensure a <typeparamref name="TEntity"/> exists with the id returned by <paramref name="idSelector"/>.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TRequest"></typeparam>
/// <param name="builder"></param>
/// <param name="idSelector">A function which selects the <c>Id</c> property from the <typeparamref name="TRequest"/></param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to futher customize the endpoint.</returns>
public static RouteHandlerBuilder WithEnsureEntityExists<TEntity, TRequest>(this RouteHandlerBuilder builder, Func<TRequest, int?> idSelector) where TEntity : class, IEntity
{
return builder
.AddEndpointFilterFactory((endpointFilterFactoryContext, next) => async context =>
{
var db = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>();
var filter = new EnsureEntityExistsFilter<TRequest, TEntity>(db, idSelector);
return await filter.InvokeAsync(context, next);
})
.ProducesProblem(StatusCodes.Status404NotFound);
}
/// <summary>
/// Adds a request validation filter to the route handler to ensure the current <seealso cref="ClaimsPrincipal"/> owns the <typeparamref name="TEntity"/> with the id returned by <paramref name="idSelector"/>.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TRequest"></typeparam>
/// <param name="builder"></param>
/// <param name="idSelector">A function which selects the <c>Id</c> property from the <typeparamref name="TRequest"/></param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to futher customize the endpoint.</returns>
public static RouteHandlerBuilder WithEnsureUserOwnsEntity<TEntity, TRequest>(this RouteHandlerBuilder builder, Func<TRequest, int> idSelector) where TEntity : class, IEntity, IOwnedEntity
{
return builder
.AddEndpointFilterFactory((endpointFilterFactoryContext, next) => async context =>
{
var db = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>();
var filter = new EnsureUserOwnsEntityFilter<TRequest, TEntity>(db, idSelector);
return await filter.InvokeAsync(context, next);
})
.ProducesProblem(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status403Forbidden);
}
}
}

View File

@ -0,0 +1,24 @@
using FluentValidation;
namespace lai_transfer.Common.Extensions;
public static class ValidationRulesExtension
{
public static IRuleBuilderOptions<T, string> AuthUsernameRule<T>(
this IRuleBuilderInitial<T, string> rule)
{
return rule.NotEmpty().WithMessage("用户名不能为空");
}
public static IRuleBuilderOptions<T, string> AuthPasswordRule<T>(
this IRuleBuilderInitial<T, string> rule)
{
return rule.NotEmpty().MinimumLength(8);
}
public static IRuleBuilderOptions<T, string> AuthNameRule<T>(
this IRuleBuilderInitial<T, string> rule)
{
return rule.NotEmpty().MaximumLength(50);
}
}

View File

@ -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<TRequest, TEntity>(ApplicationDbContext database, Func<TRequest, int?> idSelector) : IEndpointFilter
where TEntity : class, IEntity
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var request = context.Arguments.OfType<TRequest>().Single();
var cancellationToken = context.HttpContext.RequestAborted;
var id = idSelector(request);
if (!id.HasValue)
{
return await next(context);
}
var exists = await database
.Set<TEntity>()
.AnyAsync(x => x.Id == id, cancellationToken);
return exists
? await next(context)
: new NotFoundProblem($"{typeof(TEntity).Name} with id {id} was not found.");
}
}

View File

@ -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;
/// <summary>
/// 判断 对应 userId 的 Entity 数据是不是存在
/// </summary>
/// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TEntity"></typeparam>
/// <param name="database"></param>
/// <param name="idSelector"></param>
public class EnsureUserOwnsEntityFilter<TRequest, TEntity>(ApplicationDbContext database, Func<TRequest, int> idSelector) : IEndpointFilter
where TEntity : class, IEntity, IOwnedEntity
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var request = context.Arguments.OfType<TRequest>().Single();
var cancellationToken = context.HttpContext.RequestAborted;
var userId = context.HttpContext.User.GetUserId();
var id = idSelector(request);
var entity = await database
.Set<TEntity>()
.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);
}

View File

@ -0,0 +1,11 @@
namespace lai_transfer.Common.Filters
{
public class RequestLoggingFilter(ILogger<RequestLoggingFilter> logger) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
logger.LogInformation("HTTP {Method} {Path} received", context.HttpContext.Request.Method, context.HttpContext.Request.Path);
return await next(context);
}
}
}

View File

@ -0,0 +1,29 @@
using FluentValidation;
namespace lai_transfer.Common.Filters;
public class RequestValidationFilter<TRequest>(ILogger<RequestValidationFilter<TRequest>> logger, IValidator<TRequest>? validator = null) : IEndpointFilter
{
public async ValueTask<object?> 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<TRequest>().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);
}
}

View File

@ -0,0 +1,69 @@
namespace lai_transfer.Common.Filters
{
public class SplitMJAuthorizationFilter(ILogger<SplitMJAuthorizationFilter> logger) : IEndpointFilter
{
private readonly ILogger<SplitMJAuthorizationFilter> _logger = logger;
public async ValueTask<object?> 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();
}
}
}
}
}

View File

@ -0,0 +1,39 @@
using lai_transfer.Tool.Extensions;
namespace lai_transfer.Common.Helper
{
public class ConfigHelper
{
private static readonly ILogger _logger = LogHelper.GetLogger<ConfigHelper>();
// 存储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, "加载配置文件失败");
}
}
}
}

View File

@ -0,0 +1,38 @@
using System.Text;
using System.Text.Json;
namespace lai_transfer.Common.Helper
{
public static class JSONHelper
{
/// <summary>
/// 高效移除JSON属性的方法避免完全序列化和反序列化
/// </summary>
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());
}
}
}

View File

@ -0,0 +1,46 @@
namespace lai_transfer.Common.Helper
{
/// <summary>
/// 提供静态日志访问的帮助类
/// </summary>
public static class LogHelper
{
private static ILoggerFactory? _factory;
// 在应用启动时初始化
public static void Initialize(ILoggerFactory factory)
{
_factory = factory;
}
// 获取指定类型的日志记录器
public static ILogger<T> GetLogger<T>()
{
if (_factory == null)
throw new InvalidOperationException("LogHelper未初始化。请在应用启动时调用Initialize方法。");
return _factory.CreateLogger<T>();
}
// 获取指定类别名称的日志记录器
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);
}
}
}

View File

@ -0,0 +1,164 @@
using lai_transfer.Common.Enums;
using lai_transfer.Tool.Extensions;
namespace lai_transfer.Common.Results
{
public class APIResponseModel<T>
{
public APIResponseModel()
{
Code = (int)ResponseCode.Success;
Message = string.Empty;
Data = default(T);
}
/// <summary>
/// 返回的码 01
/// </summary>
public int Code { get; set; }
/// <summary>
/// 返回的信息,成功或者失败的信息
/// </summary>
public string? Message { get; set; }
/// <summary>
/// 返回的数据,可以是任何类型
/// </summary>
public object? Data { get; set; }
/// <summary>
/// 创建返回消息
/// </summary>
/// <param name="code">返回码</param>
/// <param name="data">返回数据</param>
/// <param name="message">返回消息</param>
/// <returns></returns>
public static APIResponseModel<T> CreateResponseModel(ResponseCode code, T data)
{
return new APIResponseModel<T>
{
Code = (int)code,
Message = code.GetResult(),
Data = data
};
}
/// <summary>
/// 创建正常的返回数据
/// </summary>
/// <param name="data">返回的数据</param>
/// <param name="message">返回成功的消息</param>
/// <returns></returns>
public static APIResponseModel<T> CreateSuccessResponseModel(T data, string? message = null)
{
return new APIResponseModel<T>
{
Code = (int)ResponseCode.Success,
Message = message ?? "Requset SuccessFul",
Data = data
};
}
/// <summary>
/// 创建一个返回成功的消息
/// </summary>
/// <param name="data"></param>
/// <param name="message"></param>
/// <returns></returns>
public static APIResponseModel<T> CreateSuccessResponseModel(string message)
{
// 判断 T 的类型是不是 MessageResult是的话构建Data
if (typeof(T) == typeof(IOperationResult))
{
return new APIResponseModel<T>
{
Code = (int)ResponseCode.Success,
Message = message,
Data = new MessageResult(message)
};
}
return new APIResponseModel<T>
{
Code = (int)ResponseCode.Success,
Message = message,
Data = null
};
}
/// <summary>
/// 创建正常的返回数据
/// </summary>
/// <param name="data">返回的数据</param>
/// <returns></returns>
public static APIResponseModel<T> CreateSuccessResponseModel(ResponseCode code)
{
return new APIResponseModel<T>
{
Code = (int)code,
Message = code.GetResult(),
Data = null
};
}
/// <summary>
/// 返回错误消息
/// </summary>
/// <param name="code">错误的码</param>
/// <param name="data">返回的数据</param>
/// <returns></returns>
public static APIResponseModel<T> CreateErrorResponseModel(ResponseCode code, T data)
{
return new APIResponseModel<T>
{
Code = (int)code,
Message = code.GetResult(),
Data = data
};
}
/// <summary>
/// 返回错误消息
/// </summary>
/// <param name="code">错误的码</param>
/// <param name="data">返回的数据</param>
/// <param name="message">返回的错误消息</param>
/// <returns></returns>
public static APIResponseModel<T> CreateErrorResponseModel(ResponseCode code, T data, string message)
{
return new APIResponseModel<T>
{
Code = (int)code,
Message = message,
Data = data
};
}
/// <summary>
/// 创建一个错误的返回数据,只有错误消息
/// </summary>
/// <param name="code">错误的码</param>
/// <param name="message">错误消息提示</param>
/// <returns></returns>
public static APIResponseModel<T> CreateErrorResponseModel(ResponseCode code, string message)
{
// 判断 T 的类型是不是 MessageResult是的话构建Data
if (typeof(T) == typeof(IOperationResult))
{
return new APIResponseModel<T>
{
Code = (int)code,
Message = message,
Data = new ErrorResult(message)
};
}
return new APIResponseModel<T>
{
Code = (int)code,
Message = message,
Data = null
};
}
}
}

View File

@ -0,0 +1,4 @@
namespace lai_transfer.Common.Results
{
public record ErrorResult(string Error) : IOperationResult;
}

View File

@ -0,0 +1,6 @@
namespace lai_transfer.Common.Results
{
public interface IOperationResult
{
}
}

View File

@ -0,0 +1,4 @@
namespace lai_transfer.Common.Results
{
public record MessageResult(string Message) : IOperationResult;
}

View File

@ -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<ProblemDetails>
{
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<ProblemDetails>.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);
}
}

View File

@ -0,0 +1,4 @@
namespace lai_transfer.Common.Results
{
public record TransferAuthorizationResult(string Token, string BaseUrl);
}

View File

@ -0,0 +1,10 @@
namespace lai_transfer.Common.Results
{
/// <summary>
/// 转发的请求返回
/// </summary>
/// <param name="Content"></param>
/// <param name="ContentType"></param>
/// <param name="StatusCode"></param>
public record TransferResult(string Content, string ContentType, int StatusCode);
}

View File

@ -0,0 +1,12 @@
namespace lai_transfer.Common.Types;
public interface IEntity
{
int Id { get; }
Guid ReferenceId { get; }
}
public interface IOwnedEntity
{
int UserId { get; }
}

View File

@ -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<User, Role, long>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}
}

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace lai_transfer.Configuration
{
public class MyDbcontextDesignFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
//public MyDbcontextDesignFactory CreateDbContext(string[] args)
//{
// DbContextOptionsBuilder<ApplicationDbContext> optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
// 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<ApplicationDbContext> optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
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;
}
}
}

View File

@ -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);
});
}
}
}

View File

@ -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);
});
}
}
}

View File

@ -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" ]
}
}

View File

@ -0,0 +1,6 @@
{
"Origin": {
"BaseUrl": "https://mjapi.bzu.cn",
"Token": "23830faf80e8e69988b3fcd9aa08e9ad123"
}
}

40
src/ConfigureApp.cs Normal file
View File

@ -0,0 +1,40 @@
using lai_transfer.Endpoints;
using Serilog;
namespace lai_transfer
{
public static class ConfigureApp
{
/// <summary>
/// 添加APP配置
/// </summary>
/// <param name="app"></param>
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;
}));
}
}
}
}

138
src/ConfigureServices.cs Normal file
View File

@ -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
{
/// <summary>
/// 添加 builder.Services 的配置
/// </summary>
/// <param name="builder"></param>
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);
}
/// <summary>
/// JWT 配置
/// </summary>
/// <param name="services"></param>
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
};
});
}
/// <summary>
/// 配置Identity服务配置
/// </summary>
/// <param name="services"></param>
private static void AddIdeneity(this IServiceCollection services)
{
services.AddIdentityCore<User>(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<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddUserManager<UserManager<User>>()
.AddRoleManager<RoleManager<Role>>();
}
/// <summary>
/// 配置数据库连接
/// </summary>
/// <param name="services"></param>
private static void AddDbConnection(this IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(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"));
});
}
/// <summary>
/// 配置跨域
/// </summary>
/// <param name="services"></param>
private static void AddCorsServices(this IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("AllowAll",
builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
}
}
}

30
src/Dockerfile Normal file
View File

@ -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"]

View File

@ -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<MJGetFetchIdService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapGet("task/{id}/fetch", Handle)
.WithSummary("Midjourney 指定ID获取任务")
.WithMJAuthorizationHeader();
public static async Task<IResult> 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);
}
/// <summary>
/// 获取传入的 url 对应的任务
/// </summary>
/// <param name="id"></param>
/// <param name="httpContext"></param>
/// <returns></returns>
public static async Task<TransferResult> 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);
}
}
/// <summary>
/// 获取原始数据信息 bzu
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private static async Task<TransferResult?> 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<string, object>? 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;
}
}
/// <summary>
/// 处理返回数据
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
private static Dictionary<string, object>? ProcessTaskData(string content)
{
try
{
// 解析 JSON 数据
var properties = JsonConvert.DeserializeObject<Dictionary<string, object>>(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<string, object> ProcessTaskObjectData(Dictionary<string, object> 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;
}
}
/// <summary>
/// 合作伙伴任务数据处理
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
public static Dictionary<string, object> ProcessPartnerTaskDataAsync(Dictionary<string, object> properties)
{
try
{
string jsonStr = JsonConvert.SerializeObject(properties);
dynamic data = JsonConvert.DeserializeObject(jsonStr) ?? new object { };
// 检查是否是合作伙伴任务
// 直接用dynamic访问不做类型判断
bool isPartner = data?.isPartner ?? false;
string partnerTaskId = data?.partnerTaskId ?? string.Empty;
if (isPartner && !string.IsNullOrEmpty(partnerTaskId))
{
_logger.LogInformation($"处理合作伙伴任务: {partnerTaskId}");
// 直接遍历,让异常处理兜底
var imageUrls = new List<object>();
foreach (var item in data?.partnerTaskInfo?.imgUrls ?? Array.Empty<object>())
{
string url = item?.url ?? string.Empty;
if (!string.IsNullOrEmpty(url))
{
imageUrls.Add(new { url });
}
}
if (imageUrls.Count > 0)
{
properties["imageUrls"] = imageUrls;
_logger.LogInformation($"成功提取了{imageUrls.Count}个图片URL");
}
else
{
_logger.LogInformation("未找到partnerTaskInfo或者是图片信信息");
}
}
else
{
_logger.LogInformation($"任务不是partner任务或缺少必要信息: isPartner={isPartner}, partnerTaskId={partnerTaskId}");
}
return properties;
}
catch (Exception ex)
{
_logger.LogError(ex, "处理任务数据时发生错误");
return properties; // 返回原始数据,避免处理错误导致数据丢失
}
}
/// <summary>
/// 官方任务数据处理
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
public static Dictionary<string, object> ProcessOfficialTaskDataAsync(Dictionary<string, object> properties)
{
try
{
string jsonStr = JsonConvert.SerializeObject(properties);
dynamic data = JsonConvert.DeserializeObject(jsonStr) ?? new object { };
// 检查是否是合作伙伴任务
// 直接用dynamic访问不做类型判断
bool isOfficial = data?.isOfficial ?? false;
string officialTaskId = data?.officialTaskId ?? string.Empty;
if (isOfficial && !string.IsNullOrEmpty(officialTaskId))
{
_logger.LogInformation($"处理官方任务: {officialTaskId}");
// 直接遍历,让异常处理兜底
var imageUrls = new List<object>();
foreach (var item in data?.officialTaskInfo?.imgUrls ?? Array.Empty<object>())
{
string url = item?.url ?? string.Empty;
if (!string.IsNullOrEmpty(url))
{
imageUrls.Add(new { url });
}
}
if (imageUrls.Count > 0)
{
properties["imageUrls"] = imageUrls;
_logger.LogInformation($"成功提取了{imageUrls.Count}个图片URL");
}
else
{
_logger.LogInformation("未找到officialTaskInfo或者是图片信信息");
}
}
else
{
_logger.LogInformation($"任务不是official任务或缺少必要信息: isOfficial={isOfficial}, officialTaskId={officialTaskId}");
}
return properties;
}
catch (Exception ex)
{
_logger.LogError(ex, "处理任务数据时发生错误");
return properties; // 返回原始数据,避免处理错误导致数据丢失
}
}
/// <summary>
/// 悠船任务数据处理
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
public static Dictionary<string, object> ProcessYouChuanTaskDataAsync(Dictionary<string, object> properties)
{
try
{
string jsonStr = JsonConvert.SerializeObject(properties);
dynamic data = JsonConvert.DeserializeObject(jsonStr) ?? new object { };
// 检查是否是合作伙伴任务
// 直接用dynamic访问不做类型判断
bool isYouChuan = data?.isYouChuan ?? false;
string youChuanTaskId = data?.youChuanTaskId ?? string.Empty;
if (isYouChuan && !string.IsNullOrEmpty(youChuanTaskId))
{
_logger.LogInformation($"处理悠船任务: {youChuanTaskId}");
// 直接遍历,让异常处理兜底
var imageUrls = new List<object>();
foreach (var item in data?.youChuanTaskInfo?.imgUrls ?? Array.Empty<object>())
{
string url = item?.url ?? string.Empty;
if (!string.IsNullOrEmpty(url))
{
imageUrls.Add(new { url });
}
}
if (imageUrls.Count > 0)
{
properties["imageUrls"] = imageUrls;
_logger.LogInformation($"成功提取了{imageUrls.Count}个图片URL");
}
else
{
_logger.LogInformation("未找到youChuanTaskInfo或者是图片信信息");
}
}
else
{
_logger.LogInformation($"任务不是YouChuan任务或缺少必要信息: isYouChuan={isYouChuan}, youChuanTaskId={youChuanTaskId}");
}
return properties;
}
catch (Exception ex)
{
_logger.LogError(ex, "处理任务数据时发生错误");
return properties; // 返回原始数据,避免处理错误导致数据丢失
}
}
}
}

View File

@ -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<MJGetImageSeedService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapGet("/task/{id}/image-seed", Handle)
.WithSummary("Midjourney 提交 Action 任务")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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);
}
}
}
}

View File

@ -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<MJPostActionService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapPost("/submit/action", Handle)
.WithSummary("Midjourney 提交 Action 任务")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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);
}
}
}
}

View File

@ -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<MJPostBlendService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapPost("/submit/blend", Handle)
.WithSummary("Midjourney 提交 Blend 任务")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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);
}
}
}
}

View File

@ -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<MJPostDescribeService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapPost("/submit/describe", Handle)
.WithSummary("Midjourney 提交 Describe 任务")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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);
}
}
}
}

View File

@ -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<MJPostFetchListByConditionService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapPost("/task/list-by-condition", Handle)
.WithSummary("Midjourney 根据ID列表查询任务")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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);
}
}
/// <summary>
/// 获取原始数据信息 bzu
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private static async Task<TransferResult?> 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<Dictionary<string, object>>? 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<Dictionary<string, object>>? ProcessTaskArrayData(string content)
{
try
{
List<Dictionary<string, object>> result = [];
// 解析 JSON 数据
var jsonObject = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(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;
}
}
}
}

View File

@ -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<MJPostFetchListByIdsService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapPost("/task/list-by-ids", Handle)
.WithSummary("Midjourney 根据ID列表查询任务-字段displays")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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));
}
}
}
}

View File

@ -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<IResult> Handle(JsonElement model, HttpContext httpContext, ILogger<MJPostImagineService> logger)
{
(string content, string contentType, int statusCode) = await SendOriginalImagine(model, httpContext, logger);
return Results.Text(content, contentType, statusCode: statusCode);
}
/// <summary>
/// 实际转发请求的方法
/// </summary>
/// <param name="model"></param>
/// <param name="httpContext"></param>
/// <returns></returns>
private static async Task<(string content, string contentType, int statusCode)> SendOriginalImagine(JsonElement model, HttpContext httpContext, ILogger<MJPostImagineService> 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);
}
}
}
}

View File

@ -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<MJPostModalService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapPost("/submit/modal", Handle)
.WithSummary("Midjourney 提交 Modal 任务")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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);
}
}
}
}

View File

@ -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<MJPostShortenService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapPost("/submit/shorten", Handle)
.WithSummary("Midjourney 提交 Shorten 任务")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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);
}
}
}
}

View File

@ -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<MJPostSwapFaceService>();
public static void Map(IEndpointRouteBuilder app) => app
.MapPost("/insight-face/swap", Handle)
.WithSummary("Midjourney 提交 swap_face 任务")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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);
}
}
}
}

View File

@ -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<MJPostUploadDiscordImages>();
public static void Map(IEndpointRouteBuilder app) => app
.MapPost("/submit/upload-discord-images", Handle)
.WithSummary("Midjourney 提交 上传文件到discord 任务(禁用)")
.WithMJAuthorizationHeader();
private static async Task<IResult> 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<TransferResult> 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));
}
}
}
}

View File

@ -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
{
/// <summary>
/// 请求的参数
/// </summary>
/// <param name="Username">用户名 必填</param>
/// <param name="Password">加密后的密码 必填</param>
/// <param name="Mail">邮箱 必填</param>
/// <param name="TokenId">获取密钥的Key</param>
/// <param name="AffiliateCode">验证码</param>
public record Request(string UserName, string Password, string Mail, string TokenId, string AffiliateCode);
public class RequestValidator : AbstractValidator<Request>
{
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<Request>(); // 添加验证;
private static async Task<APIResponseModel<IOperationResult>> Handle(Request request, ApplicationDbContext dbContext, ILogger<RequestValidator> logger, UserManager<User> 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<IOperationResult>.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<IOperationResult>.CreateSuccessResponseModel("User Register Success");
}
catch (Exception ex)
{
logger.LogError(ex, $"Registration failed for user {request.UserName}, mail {request.Mail}");
await transaction.RollbackAsync();
return APIResponseModel<IOperationResult>.CreateErrorResponseModel(ResponseCode.SystemError, "Registration failed");
}
}
}
}

View File

@ -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<Request>
{
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<Request>(); // 添加验证;
private static Task<APIResponseModel<IOperationResult>> 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<IOperationResult>.CreateSuccessResponseModel(response));
}
catch (Exception)
{
return Task.FromResult(APIResponseModel<IOperationResult>.CreateErrorResponseModel(ResponseCode.SystemError, "Registration failed"));
}
}
}
}

View File

@ -0,0 +1,7 @@
namespace lai_transfer.Endpoints
{
public interface IEndpoint
{
static abstract void Map(IEndpointRouteBuilder app);
}
}

View File

@ -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<MJPostImagineService>()
.MapEndpoint<MJPostActionService>()
.MapEndpoint<MJPostBlendService>()
.MapEndpoint<MJPostModalService>()
.MapEndpoint<MJPostDescribeService>()
.MapEndpoint<MJPostUploadDiscordImages>()
.MapEndpoint<MJPostShortenService>()
.MapEndpoint<MJPostSwapFaceService>()
.MapEndpoint<MJGetFetchIdService>()
.MapEndpoint<MJPostFetchListByConditionService>()
.MapEndpoint<MJPostFetchListByIdsService>()
.MapEndpoint<MJGetImageSeedService>();
// 可以添加更多特定于此组的端点
}
}
}

View File

@ -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<RequestLoggingFilter>()
.WithOpenApi();
endpoints.MapWeatherForecastEndpoint();
endpoints.MapUserEndpoint();
var emptyEndpoints = app.MapGroup("")
.AddEndpointFilter<RequestLoggingFilter>()
.WithOpenApi();
emptyEndpoints.MapMJEndpoint();
}
}
}

View File

@ -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<UserRegisterService>();
}
}
}

View File

@ -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<WeatherForecast>();
}
}
}

17
src/Model/Entity/Role.cs Normal file
View File

@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Identity;
namespace lai_transfer.Model.Entity
{
public class Role : IdentityRole<long>
{
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;
}
}

40
src/Model/Entity/User.cs Normal file
View File

@ -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<long>
{
[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; } = "";
/// <summary>
/// 用户微信号
/// </summary>
public string? WXNumber { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace lai_transfer.Model.Entity
{
public class UserRoles
{
public required string RoleId { get; set; }
public required string UserId { get; set; }
}
}

26
src/Program.cs Normal file
View File

@ -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<ILoggerFactory>());
ConfigHelper.Initialize();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.Run();

View File

@ -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
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,52 @@
namespace lai_transfer.Tool.Extensions
{
public class BeijingTimeExtension
{
/// <summary>
/// 获取北京时间,将时区转换为北京时间
/// </summary>
/// <returns></returns>
public static DateTime GetBeijingTime()
{
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow,
TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"));
}
/// <summary>
/// 智能转换时间为北京时间
/// 如果是UTC时间则转换否则直接返回
/// </summary>
/// <param name="dateTime">输入的时间</param>
/// <returns>北京时间</returns>
public static DateTime TransferUtcToBeijingTime(DateTime dateTime)
{
// 只有UTC时间才需要转换
if (dateTime.Kind == DateTimeKind.Utc)
{
try
{
// 优先使用系统时区信息
return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"));
}
catch (TimeZoneNotFoundException)
{
try
{
// Linux系统可能使用这个ID
return TimeZoneInfo.ConvertTimeFromUtc(dateTime,
TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai"));
}
catch (TimeZoneNotFoundException)
{
// 找不到时区就手动加8小时
return dateTime.AddHours(8);
}
}
}
// 非UTC时间直接返回
return dateTime;
}
}
}

View File

@ -0,0 +1,61 @@
namespace lai_transfer.Tool.Extensions
{
public class ConvertExtension
{
/// <summary>
/// 将字符串转换为long默认或者是转换错误返回0
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
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
}
/// <summary>
/// 将字符串转换为int默认或者是转换错误返回0
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static int ConvertStringToIntOrDefault(string input)
{
if (int.TryParse(input, out int result))
{
return result;
}
else
{
return 0;
}
}
}
}

View File

@ -0,0 +1,73 @@
using lai_transfer.Tool.Attributes;
namespace lai_transfer.Tool.Extensions
{
public static class EnumExtensions
{
/// <summary>
/// 判断是否为有效的权限类型
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
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;
}
}
/// <summary>
/// 获取对应的枚举的描述
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 获取对应的枚举的结果
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
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;
}
}
}

View File

@ -0,0 +1,107 @@
using System.Text.Json;
using System.Text.Json.Nodes;
namespace lai_transfer.Tool.Extensions
{
/// <summary>
/// 通用JSON配置文件读取器
/// </summary>
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);
/// <summary>
/// 读取整个JSON文件并返回根节点
/// </summary>
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;
}
/// <summary>
/// 获取指定路径的配置节点
/// </summary>
/// <param name="path">使用点分隔的路径,例如"Origin.BaseUrl"</param>
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;
}
/// <summary>
/// 将指定路径的节点转换为指定类型
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <param name="path">使用点分隔的路径,例如"Origin"</param>
public T? GetValue<T>(string path, bool forceReload = false)
{
var section = GetSection(path, forceReload);
if (section == null)
return default;
return section.Deserialize<T>();
}
/// <summary>
/// 获取指定路径的字符串值
/// </summary>
public string? GetString(string path, bool forceReload = false)
{
var section = GetSection(path, forceReload);
return section?.GetValue<string>();
}
/// <summary>
/// 获取指定路径的整数值
/// </summary>
public int? GetInt(string path, bool forceReload = false)
{
var section = GetSection(path, forceReload);
return section?.GetValue<int>();
}
/// <summary>
/// 获取指定路径的布尔值
/// </summary>
public bool? GetBool(string path, bool forceReload = false)
{
var section = GetSection(path, forceReload);
return section?.GetValue<bool>();
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
src/appsettings.json Normal file
View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

14
src/docker_release.sh Normal file
View File

@ -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"

39
src/lai_transfer.csproj Normal file
View File

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>f320e299-f265-4200-aa03-9de3060d270c</UserSecretsId>
<ContainerRegistry>docker.io</ContainerRegistry>
<ContainerImageName>yuzhile/lai_transfer</ContainerImageName>
<ContainerImageTag>latest</ContainerImageTag>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="12.0.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.2.efcore.9.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.4" />
</ItemGroup>
<ItemGroup>
<Folder Include="Common\Middleware\" />
</ItemGroup>
</Project>

6
src/lai_transfer.http Normal file
View File

@ -0,0 +1,6 @@
@lai_transfer_HostAddress = http://localhost:5141
GET {{lai_transfer_HostAddress}}/weatherforecast/
Accept: application/json
###

25
src/lai_transfer.sln Normal file
View File

@ -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