修改mj转发 新增图片合并

This commit is contained in:
lq1405 2025-10-23 18:12:06 +08:00
parent 1f71879e0f
commit 8e53379176
44 changed files with 2536 additions and 444 deletions

View File

@ -27,11 +27,17 @@ namespace lai_transfer.Common.Extensions
{ {
string? token = httpContext.GetContextItem("AuthToken"); string? token = httpContext.GetContextItem("AuthToken");
string? baseUrl = httpContext.GetContextItem("BaseUrl"); string? baseUrl = httpContext.GetContextItem("BaseUrl");
if (!String.IsNullOrWhiteSpace(baseUrl) && baseUrl.EndsWith('/'))
{
baseUrl = baseUrl.TrimEnd('/');
}
bool storage = Convert.ToBoolean(httpContext.GetContextItem("Storage") ?? "false");
bool splice = Convert.ToBoolean(httpContext.GetContextItem("Splice") ?? "false");
if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(baseUrl)) if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(baseUrl))
{ {
throw new InvalidOperationException("Authentication token or base URL is not set in the context."); throw new InvalidOperationException("Authentication token or base URL is not set in the context.");
} }
return new TransferAuthorizationResult(token, baseUrl); return new TransferAuthorizationResult(token, baseUrl, storage, splice, "");
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,7 @@
namespace lai_transfer.Common.Filters using lai_transfer.Common.Helper;
using Newtonsoft.Json.Linq;
namespace lai_transfer.Common.Filters
{ {
public class SplitMJAuthorizationFilter(ILogger<SplitMJAuthorizationFilter> logger) : IEndpointFilter public class SplitMJAuthorizationFilter(ILogger<SplitMJAuthorizationFilter> logger) : IEndpointFilter
{ {
@ -25,43 +28,43 @@
_logger.LogWarning("Authorization header is missing"); _logger.LogWarning("Authorization header is missing");
return TypedResults.Unauthorized(); return TypedResults.Unauthorized();
} }
else
// 2. 处理令牌,判断是不是有前缀 删除前缀
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{ {
// 2. 处理令牌,判断是不是有前缀 删除前缀 authorization = authorization["Bearer ".Length..];
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();
}
} }
var result1 = QueryStringHelper.Parse(authorization);
string token = result1.Prefix;
string baseUrl = result1.GetString("url");
// 是否存储
bool storage = result1.GetBool("storage", false);
// 是否拼接
bool splice = result1.GetBool("splice", false);
// token 不能为空
if (String.IsNullOrWhiteSpace(token))
{
_logger.LogWarning("Authorization Invalid");
return TypedResults.Unauthorized();
}
if (String.IsNullOrWhiteSpace(baseUrl))
{
_logger.LogWarning("BaseUrl Invalid");
return TypedResults.BadRequest("BaseUrl Invalid");
}
httpContext.Items["AuthToken"] = token;
// 将baseUrl也存入HttpContext以便后续使用
httpContext.Items["BaseUrl"] = baseUrl;
httpContext.Items["Storage"] = storage;
httpContext.Items["Splice"] = splice;
return await next(context);
} }
} }
} }

View File

@ -21,6 +21,8 @@ namespace lai_transfer.Common.Helper
Translate.Init(reader); Translate.Init(reader);
Qiniu.Init(reader);
_logger.LogInformation("配置加载完成"); _logger.LogInformation("配置加载完成");
} }
catch (Exception ex) catch (Exception ex)
@ -88,6 +90,43 @@ namespace lai_transfer.Common.Helper
} }
// 存储Origin配置 // 存储Origin配置
public static class Qiniu
{
public static string AccessKey { get; internal set; } = string.Empty;
public static string SecretKey { get; internal set; } = string.Empty;
public static string BucketName { get; internal set; } = string.Empty;
public static string Domain { get; internal set; } = string.Empty;
public static double MaxFileSize { get; internal set; } = 10; // MB
public static bool OriginImage { get; internal set; } = false;
/// <summary>
/// 初始化Qiniu配置
/// </summary>
public static void Init(JsonConfigReader reader)
{
try
{
_logger.LogInformation("正在加载Qiniu配置...");
// 加载Qiniu配置
AccessKey = reader.GetString("QiNiu.AccessKey") ?? string.Empty;
SecretKey = reader.GetString("QiNiu.SecretKey") ?? string.Empty;
BucketName = reader.GetString("QiNiu.BucketName") ?? string.Empty;
Domain = reader.GetString("QiNiu.Domain") ?? string.Empty;
MaxFileSize = reader.GetDouble("QiNiu.MaxFileSize") ?? 10; // MB
OriginImage = reader.GetBool("QiNiu.OriginImage") ?? false;
_logger.LogInformation("QiNiu配置加载完成");
}
catch (Exception ex)
{
_logger.LogError(ex, "加载Qiniu配置失败");
// 报错,结束程序运行
throw new InvalidOperationException("无法加载Qiniu配置请检查配置文件是否存在或格式是否正确。", ex);
}
}
}
public static class Origin public static class Origin
{ {
// 将private set改为internal set允许同一程序集中的代码设置属性值 // 将private set改为internal set允许同一程序集中的代码设置属性值

View File

@ -0,0 +1,132 @@
namespace lai_transfer.Common.Helper
{
public static class QueryStringHelper
{
public class ParseResult
{
public string Prefix { get; set; }
public Dictionary<string, string> Parameters { get; set; }
public ParseResult()
{
Parameters = new Dictionary<string, string>();
}
// 获取字符串值
public string GetString(string key, string defaultValue = "")
{
return Parameters.TryGetValue(key, out string value) ? value : defaultValue;
}
// 获取布尔值
public bool GetBool(string key, bool defaultValue = false)
{
if (Parameters.TryGetValue(key, out string value))
{
// 支持多种布尔值格式
string lowerValue = value.ToLower();
if (lowerValue == "true" || lowerValue == "1" || lowerValue == "yes")
return true;
if (lowerValue == "false" || lowerValue == "0" || lowerValue == "no")
return false;
}
return defaultValue;
}
// 获取整数值
public int GetInt(string key, int defaultValue = 0)
{
if (Parameters.TryGetValue(key, out string value))
{
if (int.TryParse(value, out int result))
return result;
}
return defaultValue;
}
// 获取长整数值
public long GetLong(string key, long defaultValue = 0)
{
if (Parameters.TryGetValue(key, out string value))
{
if (long.TryParse(value, out long result))
return result;
}
return defaultValue;
}
// 获取双精度浮点数
public double GetDouble(string key, double defaultValue = 0.0)
{
if (Parameters.TryGetValue(key, out string value))
{
if (double.TryParse(value, out double result))
return result;
}
return defaultValue;
}
// 尝试获取布尔值
public bool TryGetBool(string key, out bool result)
{
result = false;
if (Parameters.TryGetValue(key, out string value))
{
string lowerValue = value.ToLower();
if (lowerValue == "true" || lowerValue == "1" || lowerValue == "yes")
{
result = true;
return true;
}
if (lowerValue == "false" || lowerValue == "0" || lowerValue == "no")
{
result = false;
return true;
}
}
return false;
}
}
public static ParseResult Parse(string input)
{
var result = new ParseResult();
if (string.IsNullOrEmpty(input))
return result;
// 找到第一个 ? 的位置
int firstQuestionMark = input.IndexOf('?');
if (firstQuestionMark == -1)
{
// 没有参数,整个字符串都是前缀
result.Prefix = input;
return result;
}
// 提取前缀部分(第一个 ? 之前的内容)
result.Prefix = input.Substring(0, firstQuestionMark);
// 从第一个 ? 之后开始处理参数
string queryString = input.Substring(firstQuestionMark + 1);
// 按 ? 分割参数
var pairs = queryString.Split('?');
foreach (var pair in pairs)
{
if (string.IsNullOrWhiteSpace(pair))
continue;
var keyValue = pair.Split(new[] { '=' }, 2);
if (keyValue.Length == 2)
{
result.Parameters[keyValue[0]] = keyValue[1];
}
}
return result;
}
}
}

View File

@ -0,0 +1,199 @@
namespace lai_transfer.Common.Helper;
public class RetryHelper
{
private static readonly ILogger _logger = LogHelper.GetLogger<RetryHelper>();
/// <summary>
/// 异步重试执行函数(无返回值)
/// </summary>
/// <param name="action">要执行的函数</param>
/// <param name="retryCount">重试次数</param>
/// <param name="delay">重试间隔</param>
/// <param name="cancellationToken">取消令牌</param>
public static async Task RetryAsync(
Func<Task> action,
int retryCount = 3,
TimeSpan? delay = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(action);
var exceptions = new List<Exception>();
var currentDelay = delay ?? TimeSpan.FromSeconds(1);
for (int attempt = 0; attempt <= retryCount; attempt++)
{
try
{
if (attempt > 0)
{
_logger?.LogInformation("重试执行,第 {Attempt} 次尝试", attempt);
}
await action().ConfigureAwait(false);
return; // 执行成功,直接返回
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
exceptions.Add(ex);
_logger?.LogWarning(ex, "第 {Attempt} 次执行失败", attempt + 1);
if (attempt == retryCount)
{
throw new AggregateException($"在 {retryCount + 1} 次尝试后操作仍失败", exceptions);
}
// 等待一段时间后重试
if (currentDelay > TimeSpan.Zero)
{
_logger?.LogInformation("等待 {Delay} 后重试", currentDelay);
await Task.Delay(currentDelay, cancellationToken).ConfigureAwait(false);
}
}
}
}
/// <summary>
/// 异步重试执行函数(有返回值)
/// </summary>
/// <typeparam name="TResult">返回值类型</typeparam>
/// <param name="func">要执行的函数</param>
/// <param name="retryCount">重试次数</param>
/// <param name="delay">重试间隔(默认1s)</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>函数执行结果</returns>
public static async Task<TResult> RetryAsync<TResult>(
Func<Task<TResult>> func,
int retryCount = 3,
TimeSpan? delay = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(func);
var exceptions = new List<Exception>();
var currentDelay = delay ?? TimeSpan.FromSeconds(1);
for (int attempt = 0; attempt <= retryCount; attempt++)
{
try
{
if (attempt > 0)
{
_logger?.LogInformation("重试执行,第 {Attempt} 次尝试", attempt);
}
return await func().ConfigureAwait(false);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
exceptions.Add(ex);
_logger?.LogWarning(ex, "第 {Attempt} 次执行失败", attempt + 1);
if (attempt == retryCount)
{
throw new AggregateException($"在 {retryCount + 1} 次尝试后操作仍失败", exceptions);
}
// 等待一段时间后重试
if (currentDelay > TimeSpan.Zero)
{
_logger?.LogInformation("等待 {Delay} 后重试", currentDelay);
await Task.Delay(currentDelay, cancellationToken).ConfigureAwait(false);
}
}
}
throw new InvalidOperationException("不应该执行到这里");
}
/// <summary>
/// 同步重试执行函数(无返回值)
/// </summary>
public static void Retry(
Action action,
int retryCount = 3,
TimeSpan? delay = null)
{
ArgumentNullException.ThrowIfNull(action);
var exceptions = new List<Exception>();
var currentDelay = delay ?? TimeSpan.FromSeconds(1);
for (int attempt = 0; attempt <= retryCount; attempt++)
{
try
{
if (attempt > 0)
{
_logger?.LogInformation("重试执行,第 {Attempt} 次尝试", attempt);
}
action();
return; // 执行成功,直接返回
}
catch (Exception ex)
{
exceptions.Add(ex);
_logger?.LogWarning(ex, "第 {Attempt} 次执行失败", attempt + 1);
if (attempt == retryCount)
{
throw new AggregateException($"在 {retryCount + 1} 次尝试后操作仍失败", exceptions);
}
// 等待一段时间后重试
if (currentDelay > TimeSpan.Zero)
{
_logger?.LogInformation("等待 {Delay} 后重试", currentDelay);
Thread.Sleep(currentDelay);
}
}
}
}
/// <summary>
/// 同步重试执行函数(有返回值)
/// </summary>
public static TResult Retry<TResult>(
Func<TResult> func,
int retryCount = 3,
TimeSpan? delay = null)
{
ArgumentNullException.ThrowIfNull(func);
var exceptions = new List<Exception>();
var currentDelay = delay ?? TimeSpan.FromSeconds(1);
for (int attempt = 0; attempt <= retryCount; attempt++)
{
try
{
if (attempt > 0)
{
_logger?.LogInformation("重试执行,第 {Attempt} 次尝试", attempt);
}
return func();
}
catch (Exception ex)
{
exceptions.Add(ex);
_logger?.LogWarning(ex, "第 {Attempt} 次执行失败", attempt + 1);
if (attempt == retryCount)
{
throw new AggregateException($"在 {retryCount + 1} 次尝试后操作仍失败", exceptions);
}
// 等待一段时间后重试
if (currentDelay > TimeSpan.Zero)
{
_logger?.LogInformation("等待 {Delay} 后重试", currentDelay);
Thread.Sleep(currentDelay);
}
}
}
throw new InvalidOperationException("不应该执行到这里");
}
}

View File

@ -1,4 +1,22 @@
namespace lai_transfer.Common.Results using Newtonsoft.Json.Linq;
namespace lai_transfer.Common.Results
{ {
public record TransferAuthorizationResult(string Token, string BaseUrl); public class TransferAuthorizationResult
{
public string Token { get; set; }
public string BaseUrl { get; set; }
public bool Storage { get; set; }
public bool Splice { get; set; }
public string FullPath { get; set; }
public TransferAuthorizationResult(string Token, string BaseUrl, bool Storage, bool Splice, string FullPath)
{
this.Token = Token;
this.BaseUrl = BaseUrl;
this.Storage = Storage;
this.Splice = Splice;
this.FullPath = FullPath;
}
}
} }

View File

@ -0,0 +1,16 @@
namespace lai_transfer.Common.Types
{
public class MJType
{
public class MJOptionType
{
public const string IMAGINE = "IMAGINE";
}
public class MJStatus
{
public const string NOT_START = "not_start";
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -15,5 +15,13 @@
"OpenaiGptMaxTokens": 2048, "OpenaiGptMaxTokens": 2048,
"OpenaiGptTemperature": 0, "OpenaiGptTemperature": 0,
"TimeOut": 20 "TimeOut": 20
},
"QiNiu": {
"AccessKey": "akGxVLyV-xxx",
"SecretKey": "a0zHruUrouxCxxx",
"BucketName": "open-laitoolxxx",
"Domain": "https://opss.laitool.cn",
"MaxFileSize": 10,
"OriginImage": true
} }
} }

View File

@ -1,17 +1,20 @@
using FluentValidation; using FluentValidation;
using lai_transfer.Common.Helper;
using lai_transfer.Configuration; using lai_transfer.Configuration;
using lai_transfer.Model.Entity; using lai_transfer.Model.Entity;
using lai_transfer.Services; using lai_transfer.Services.Midjourney;
using lai_transfer.EndpointServices.MJTransferEndpoint; using lai_transfer.Services.PeriodicTimeService;
using lai_transfer.Services.RealmService;
using lai_transfer.Services.Translate;
using lai_transfer.Tool.HttpTool;
using lai_transfer.Tool.ImageTool;
using lai_transfer.Tool.QiniuService;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Serilog; using Serilog;
using System.Text; using System.Text;
using Midjourney.Infrastructure.Services;
using lai_transfer.Common.Helper;
namespace lai_transfer namespace lai_transfer
{ {
@ -58,6 +61,19 @@ namespace lai_transfer
/// <param name="services"></param> /// <param name="services"></param>
private static void AddService(this IServiceCollection services) private static void AddService(this IServiceCollection services)
{ {
// 注册翻译服务
services.AddScoped<IGlobalTranslateService, TranslateService>();
services.AddSingleton<RealmService>();
services.AddHttpClient();
services.AddScoped<IHttpService, HttpService>();
services.AddScoped<IMidjourneyService, MidjourneyService>();
services.AddScoped<IQiniuService, QiniuService>();
services.AddScoped<IImageService, ImageService>();
services.AddScoped<IMJTaskRealmService, MJTaskRealmService>();
services.AddHostedService<PeriodicTimedService>();
if (ConfigHelper.Translate.Model == "GPT") if (ConfigHelper.Translate.Model == "GPT")
{ {
// 翻译服务注入 // 翻译服务注入

View File

@ -2,6 +2,7 @@
using lai_transfer.Common.Helper; using lai_transfer.Common.Helper;
using lai_transfer.Common.Results; using lai_transfer.Common.Results;
using lai_transfer.Endpoints; using lai_transfer.Endpoints;
using lai_transfer.Services.Midjourney;
using lai_transfer.Tool.Extensions; using lai_transfer.Tool.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -18,16 +19,10 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
public static async Task<IResult> Handle(string id, HttpContext httpContext) public static async Task<IResult> Handle(string id, HttpContext httpContext)
{ {
// 先尝试调用原始API TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation();
TransferResult? originTransferResult = await TryOriginApiAsync(id); authorizationResult.FullPath = httpContext.GetFullRequestPath();
if (originTransferResult != null) IMidjourneyService midjourneyService = httpContext.RequestServices.GetRequiredService<IMidjourneyService>();
{ return await midjourneyService.MidjourneyFetchTaskById(id, authorizationResult);
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> /// <summary>

View File

@ -24,21 +24,26 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
{ {
try try
{ {
//string baseUrl =
TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation();
string url = $"{transferAuthorizationResult.BaseUrl}/mj/task/{id}/image-seed"; string url = $"{transferAuthorizationResult.BaseUrl}/mj/task/{id}/image-seed";
// 设置HttpClient TransferResult res = await RetryHelper.RetryAsync<TransferResult>(async () =>
using HttpClient client = new HttpClient(); {
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}"); // 设置HttpClient
client.Timeout = Timeout.InfiniteTimeSpan; using HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}");
client.DefaultRequestHeaders.Add("mj-api-secret", transferAuthorizationResult.Token);
client.Timeout = Timeout.InfiniteTimeSpan;
// 发送请求 // 发送请求
var response = await client.GetAsync(url); var response = await client.GetAsync(url);
string content = await response.Content.ReadAsStringAsync(); string content = await response.Content.ReadAsStringAsync();
// 返回结果 // 返回结果
return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode);
}, 5);
return res;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -27,16 +27,22 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
{ {
TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation(); TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation();
string url = $"{authorizationResult.BaseUrl}/mj/submit/action"; 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"); TransferResult res = await RetryHelper.RetryAsync<TransferResult>(async () =>
// 发送请求 {
var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); // 设置HttpClient
string content = await response.Content.ReadAsStringAsync(); using HttpClient client = new HttpClient();
return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}");
client.DefaultRequestHeaders.Add("mj-api-secret", 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);
}, 5);
return res;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -26,17 +26,22 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
{ {
TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation(); TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation();
string url = $"{authorizationResult.BaseUrl}/mj/submit/blend"; string url = $"{authorizationResult.BaseUrl}/mj/submit/blend";
// 设置HttpClient TransferResult res = await RetryHelper.RetryAsync<TransferResult>(async () =>
using HttpClient client = new HttpClient(); {
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}"); // 设置HttpClient
client.Timeout = Timeout.InfiniteTimeSpan; using HttpClient client = new HttpClient();
// 删除回调参数 notifyHook client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}");
string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); client.DefaultRequestHeaders.Add("mj-api-secret", authorizationResult.Token);
// 发送请求 client.Timeout = Timeout.InfiniteTimeSpan;
var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); // 删除回调参数 notifyHook
string content = await response.Content.ReadAsStringAsync(); 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); return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode);
}, 5);
return res;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -26,18 +26,23 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
{ {
TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation(); TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation();
string url = $"{authorizationResult.BaseUrl}/mj/submit/describe"; string url = $"{authorizationResult.BaseUrl}/mj/submit/describe";
// 设置HttpClient TransferResult res = await RetryHelper.RetryAsync<TransferResult>(async () =>
using HttpClient client = new HttpClient(); {
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}"); // 设置HttpClient
client.Timeout = Timeout.InfiniteTimeSpan; using HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}");
client.DefaultRequestHeaders.Add("mj-api-secret", authorizationResult.Token);
client.Timeout = Timeout.InfiniteTimeSpan;
// 删除回调参数 notifyHook // 删除回调参数 notifyHook
string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); string body = JSONHelper.RemoveJsonProperties(model, "notifyHook");
// 发送请求 // 发送请求
var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json"));
string content = await response.Content.ReadAsStringAsync(); string content = await response.Content.ReadAsStringAsync();
return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode);
}, 5);
return res;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -2,10 +2,8 @@
using lai_transfer.Common.Helper; using lai_transfer.Common.Helper;
using lai_transfer.Common.Results; using lai_transfer.Common.Results;
using lai_transfer.Endpoints; using lai_transfer.Endpoints;
using lai_transfer.Tool.Extensions; using lai_transfer.Services.Midjourney;
using Microsoft.AspNetCore.Authorization; using static lai_transfer.Model.Midjourney.MidjourneyRequest;
using Newtonsoft.Json;
using System.Text.Json;
namespace lai_transfer.EndpointServices.MJTransferEndpoint namespace lai_transfer.EndpointServices.MJTransferEndpoint
{ {
@ -17,133 +15,13 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
.WithSummary("Midjourney 根据ID列表查询任务") .WithSummary("Midjourney 根据ID列表查询任务")
.WithMJAuthorizationHeader(); .WithMJAuthorizationHeader();
private static async Task<IResult> Handle(JsonElement model, HttpContext httpContext) private static async Task<IResult> Handle(FetchListByCondition model, HttpContext httpContext)
{ {
TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation();
// 先尝试调用原始API IMidjourneyService midjourneyService = httpContext.RequestServices.GetRequiredService<IMidjourneyService>();
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 await midjourneyService.FetchListByCondition(model, transferAuthorizationResult);
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

@ -2,7 +2,8 @@
using lai_transfer.Common.Helper; using lai_transfer.Common.Helper;
using lai_transfer.Common.Results; using lai_transfer.Common.Results;
using lai_transfer.Endpoints; using lai_transfer.Endpoints;
using lai_transfer.Services; using lai_transfer.Services.Midjourney;
using lai_transfer.Services.Translate;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -20,8 +21,7 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
private static async Task<IResult> Handle(JsonElement model, HttpContext httpContext) private static async Task<IResult> Handle(JsonElement model, HttpContext httpContext)
{ {
(string content, string contentType, int statusCode) = await SendOriginalImagine(model, httpContext); return await SendOriginalImagine(model, httpContext);
return Results.Text(content, contentType, statusCode: statusCode);
} }
/// <summary> /// <summary>
@ -30,111 +30,23 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
/// <param name="model"></param> /// <param name="model"></param>
/// <param name="httpContext"></param> /// <param name="httpContext"></param>
/// <returns></returns> /// <returns></returns>
private static async Task<(string content, string contentType, int statusCode)> SendOriginalImagine(JsonElement model, HttpContext httpContext) private static async Task<IResult> SendOriginalImagine(JsonElement model, HttpContext httpContext)
{ {
try try
{ {
TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation(); TransferAuthorizationResult authorizationResult = httpContext.GetAuthorizationItemsAndValidation();
string url = $"{authorizationResult.BaseUrl}/mj/submit/imagine"; string requestUrl = httpContext.GetFullRequestPath();
authorizationResult.FullPath = requestUrl;
IMidjourneyService midjourneyService = httpContext.RequestServices.GetRequiredService<IMidjourneyService>();
// 设置HttpClient IResult t = await midjourneyService.MidjourneyImagine(model, authorizationResult);
using HttpClient client = new HttpClient(); return t;
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {authorizationResult.Token}");
client.Timeout = Timeout.InfiniteTimeSpan;
// 强制翻译提示词
string prompt = JSONHelper.GetJsonPropertyString(model, "prompt") ?? string.Empty;
// 使用HttpContext扩展方法获取翻译服务
var translateService = ServiceLocator.GetRequiredService<ITranslateService>();
prompt = await TranslatePrompt(prompt, translateService);
model = JSONHelper.SetJsonProperty(model, "prompt", prompt);
// 删除回调参数 notifyHook
string body = JSONHelper.RemoveJsonProperties(model, "notifyHook");
//return (body, "application/json", 200);
// 发送请求
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) catch (Exception ex)
{ {
_logger.LogError(ex, $"请求 {httpContext.GetFullRequestPath()} 失败"); _logger.LogError(ex, $"请求 {httpContext.GetFullRequestPath()} 失败");
// 处理异常,返回错误信息 // 处理异常,返回错误信息
return (ex.Message, "application/json", StatusCodes.Status500InternalServerError); return Results.BadRequest("request error : " + ex.Message);
}
}
/// <summary>
/// 翻译提示词
/// </summary>
/// <param name="prompt">原始提示词</param>
/// <param name="translateService">翻译服务</param>
/// <returns>翻译后的提示词</returns>
public static async Task<string> TranslatePrompt(string prompt, ITranslateService translateService)
{
if (string.IsNullOrWhiteSpace(prompt))
return prompt;
try
{
// 检查是否启用翻译功能
if (!ConfigHelper.Translate.Enable)
return prompt;
// 如果不包含中文,直接返回
if (string.IsNullOrWhiteSpace(prompt) || !translateService.ContainsChinese(prompt))
return prompt;
string paramStr = "";
var paramMatcher = Regex.Match(prompt, "\\x20+--[a-z]+.*$", RegexOptions.IgnoreCase);
if (paramMatcher.Success)
{
paramStr = paramMatcher.Value;
}
string promptWithoutParam = prompt.Substring(0, prompt.Length - paramStr.Length);
List<string> imageUrls = new List<string>();
var imageMatcher = Regex.Matches(promptWithoutParam, "https?://[a-z0-9-_:@&?=+,.!/~*'%$]+\\x20+", RegexOptions.IgnoreCase);
foreach (Match match in imageMatcher)
{
imageUrls.Add(match.Value);
}
string text = promptWithoutParam;
foreach (string imageUrl in imageUrls)
{
text = text.Replace(imageUrl, "");
}
text = text.Trim();
if (!string.IsNullOrWhiteSpace(text))
{
text = await translateService.TranslateToEnglish(text);
}
if (!string.IsNullOrWhiteSpace(paramStr))
{
// 当有 --no 参数时, 翻译 --no 参数, 并替换原参数
// --sref https://mjcdn.googlec.cc/1.jpg --no aa, bb, cc
var paramNomatcher = Regex.Match(paramStr, "--no\\s+(.*?)(?=--|$)");
if (paramNomatcher.Success)
{
string paramNoStr = paramNomatcher.Groups[1].Value.Trim();
string paramNoStrEn = await translateService.TranslateToEnglish(paramNoStr);
// 提取 --no 之前的参数
paramStr = paramStr.Substring(0, paramNomatcher.Index);
// 替换 --no 参数
paramStr = paramStr + paramNomatcher.Result("--no " + paramNoStrEn + " ");
}
}
return string.Concat(imageUrls) + text.Trim() + paramStr;
}
catch (Exception ex)
{
LogHelper.LogError(ex, "翻译提示词失败,使用原始提示词");
return prompt;
} }
} }
} }

View File

@ -2,7 +2,8 @@
using lai_transfer.Common.Helper; using lai_transfer.Common.Helper;
using lai_transfer.Common.Results; using lai_transfer.Common.Results;
using lai_transfer.Endpoints; using lai_transfer.Endpoints;
using lai_transfer.Services; using lai_transfer.Services.Translate;
using Microsoft.AspNetCore.Authorization;
using System.Text.Json; using System.Text.Json;
namespace lai_transfer.EndpointServices.MJTransferEndpoint namespace lai_transfer.EndpointServices.MJTransferEndpoint
@ -27,26 +28,35 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
{ {
TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation();
string url = $"{transferAuthorizationResult.BaseUrl}/mj/submit/modal"; string url = $"{transferAuthorizationResult.BaseUrl}/mj/submit/modal";
// 设置HttpClient
using HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}");
client.Timeout = Timeout.InfiniteTimeSpan;
// 强制翻译提示词 // 强制翻译提示词
string prompt = JSONHelper.GetJsonPropertyString(model, "prompt") ?? string.Empty; string prompt = JSONHelper.GetJsonPropertyString(model, "prompt") ?? string.Empty;
if (string.IsNullOrWhiteSpace(prompt))
{
prompt = JSONHelper.GetJsonPropertyString(model, "Prompt") ?? string.Empty;
}
// 使用HttpContext扩展方法获取翻译服务 // 使用HttpContext扩展方法获取翻译服务
var translateService = ServiceLocator.GetRequiredService<ITranslateService>(); var translateService = ServiceLocator.GetRequiredService<IGlobalTranslateService>();
prompt = await MJPostImagineService.TranslatePrompt(prompt, translateService); prompt = await translateService.TranslatePrompt(prompt);
model = JSONHelper.SetJsonProperty(model, "prompt", prompt); model = JSONHelper.SetJsonProperty(model, "prompt", prompt);
// 删除回调参数 notifyHook // 删除回调参数 notifyHook
string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); string body = JSONHelper.RemoveJsonProperties(model, "notifyHook");
// 发送请求
var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); TransferResult res = await RetryHelper.RetryAsync<TransferResult>(async () =>
string content = await response.Content.ReadAsStringAsync(); {
return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); // 设置HttpClient
using HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}");
client.DefaultRequestHeaders.Add("mj-api-secret", transferAuthorizationResult.Token);
client.Timeout = Timeout.InfiniteTimeSpan;
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);
}, 5);
return res;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -2,6 +2,7 @@
using lai_transfer.Common.Helper; using lai_transfer.Common.Helper;
using lai_transfer.Common.Results; using lai_transfer.Common.Results;
using lai_transfer.Endpoints; using lai_transfer.Endpoints;
using Microsoft.AspNetCore.Authorization;
using System.Text.Json; using System.Text.Json;
namespace lai_transfer.EndpointServices.MJTransferEndpoint namespace lai_transfer.EndpointServices.MJTransferEndpoint
@ -26,17 +27,22 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
{ {
TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation();
string url = $"{transferAuthorizationResult.BaseUrl}/mj/submit/shorten"; 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 // 删除回调参数 notifyHook
string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); string body = JSONHelper.RemoveJsonProperties(model, "notifyHook");
// 发送请求
var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); TransferResult res = await RetryHelper.RetryAsync<TransferResult>(async () =>
string content = await response.Content.ReadAsStringAsync(); {
return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); // 设置HttpClient
using HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}");
client.DefaultRequestHeaders.Add("mj-api-secret", transferAuthorizationResult.Token);
client.Timeout = Timeout.InfiniteTimeSpan;
// 发送请求
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);
}, 5);
return res;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -2,6 +2,7 @@
using lai_transfer.Common.Helper; using lai_transfer.Common.Helper;
using lai_transfer.Common.Results; using lai_transfer.Common.Results;
using lai_transfer.Endpoints; using lai_transfer.Endpoints;
using Microsoft.AspNetCore.Authorization;
using System.Text.Json; using System.Text.Json;
namespace lai_transfer.EndpointServices.MJTransferEndpoint namespace lai_transfer.EndpointServices.MJTransferEndpoint
@ -26,17 +27,22 @@ namespace lai_transfer.EndpointServices.MJTransferEndpoint
{ {
TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation(); TransferAuthorizationResult transferAuthorizationResult = httpContext.GetAuthorizationItemsAndValidation();
string url = $"{transferAuthorizationResult.BaseUrl}/mj/insight-face/swap"; 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 // 删除回调参数 notifyHook
string body = JSONHelper.RemoveJsonProperties(model, "notifyHook"); string body = JSONHelper.RemoveJsonProperties(model, "notifyHook");
// 发送请求
var response = await client.PostAsync(url, new StringContent(body, System.Text.Encoding.UTF8, "application/json")); TransferResult res = await RetryHelper.RetryAsync<TransferResult>(async () =>
string content = await response.Content.ReadAsStringAsync(); {
return new TransferResult(content, response.Content.Headers.ContentType?.ToString() ?? "application/json", (int)response.StatusCode); // 设置HttpClient
using HttpClient client = new();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {transferAuthorizationResult.Token}");
client.DefaultRequestHeaders.Add("mj-api-secret", transferAuthorizationResult.Token);
client.Timeout = Timeout.InfiniteTimeSpan;
// 发送请求
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);
}, 5);
return res;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -1,88 +0,0 @@
using lai_transfer.Common.Extensions;
using lai_transfer.Common.Helper;
using lai_transfer.Services;
using System.Threading.Tasks;
namespace lai_transfer.Examples
{
/// <summary>
/// 展示如何使用服务获取扩展方法的示例
/// </summary>
public static class ServiceUsageExamples
{
/// <summary>
/// 在有HttpContext的场景下使用服务
/// </summary>
/// <param name="httpContext">Http上下文</param>
public static async Task ExampleWithHttpContext(HttpContext httpContext)
{
// 方式1使用HttpContext扩展方法获取服务
var translateService = httpContext.GetRequiredService<ITranslateService>();
var loggerFactory = httpContext.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger("ServiceUsageExamples");
// 使用服务
string result = await translateService.TranslateToEnglish("你好世界");
logger?.LogInformation($"翻译结果: {result}");
}
/// <summary>
/// 在没有HttpContext的场景下使用服务如静态方法、后台任务等
/// </summary>
public static async Task ExampleWithoutHttpContext()
{
// 方式2使用全局服务定位器获取服务
var translateService = ServiceLocator.GetRequiredService<ITranslateService>();
var loggerFactory = ServiceLocator.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger("ServiceUsageExamples");
// 使用服务
string result = await translateService.TranslateToEnglish("你好世界");
logger?.LogInformation($"翻译结果: {result}");
}
/// <summary>
/// 在需要服务作用域的场景下使用
/// </summary>
public static async Task ExampleWithScope()
{
// 创建新的作用域
using var scope = ServiceLocator.CreateScope();
var scopedServices = scope.ServiceProvider;
// 从作用域中获取服务
var translateService = scopedServices.GetRequiredService<ITranslateService>();
// 使用服务
string result = await translateService.TranslateToEnglish("你好世界");
// 作用域结束时会自动释放资源
}
/// <summary>
/// 在端点处理器中的使用示例
/// </summary>
public static async Task<IResult> EndpointExample(HttpContext httpContext)
{
try
{
// 直接从HttpContext获取服务无需在方法参数中声明
var translateService = httpContext.GetRequiredService<ITranslateService>();
var loggerFactory = httpContext.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("ServiceUsageExamples");
// 业务逻辑
string prompt = "测试文本";
string translatedPrompt = await translateService.TranslateToEnglish(prompt);
logger.LogInformation($"翻译完成: {prompt} -> {translatedPrompt}");
return Results.Ok(new { original = prompt, translated = translatedPrompt });
}
catch (Exception ex)
{
return Results.Problem($"处理失败: {ex.Message}");
}
}
}
}

3
src/FodyWeavers.xml Normal file
View File

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Realm />
</Weavers>

View File

@ -0,0 +1,59 @@
using lai_transfer.Tool.Extensions;
using Realms;
namespace lai_transfer.Model.Entity
{
[MapTo("MJTask")]
public class MJTask : RealmObject
{
[PrimaryKey]
[MapTo("_id")]
public string ID { get; set; } = string.Empty;
[MapTo("mjId")]
public string MJId { get; set; }
[MapTo("type")]
public string Type { get; set; }
[MapTo("status")]
public string Status { get; set; }
[MapTo("baseUrl")]
public string BaseUrl { get; set; }
[MapTo("property")]
public string Property { get; set; }
[MapTo("authorizationResult")]
public string AuthorizationResult { get; set; }
[MapTo("createdTime")]
public DateTimeOffset CreatedTime { get; set; } = BeijingTimeExtension.GetBeijingTime();
[MapTo("checkedTime")]
public DateTimeOffset CheckedTime { get; set; } = BeijingTimeExtension.GetBeijingTime();
}
public class MJTaskDTO
{
public string ID { get; set; } = string.Empty;
public string MJId { get; set; }
public string Type { get; set; }
public string Status { get; set; }
public string BaseUrl { get; set; }
public string Property { get; set; }
public string AuthorizationResult { get; set; }
public DateTimeOffset CreatedTime { get; set; } = BeijingTimeExtension.GetBeijingTime();
public DateTimeOffset CheckedTime { get; set; } = BeijingTimeExtension.GetBeijingTime();
}
}

View File

@ -0,0 +1,10 @@
namespace lai_transfer.Model.Midjourney
{
public class MidjourneyRequest
{
public class FetchListByCondition
{
public List<string> Ids { get; set; } = [];
}
}
}

View File

@ -0,0 +1,158 @@
using System.Text.Json.Serialization;
namespace lai_transfer.Model.Midjourney
{
public class MidjourneyTaskResponse
{
public class TaskResponse
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("accountId")]
public string AccountId { get; set; }
[JsonPropertyName("action")]
public string Action { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("mode")]
public string Mode { get; set; }
[JsonPropertyName("prompt")]
public string Prompt { get; set; }
[JsonPropertyName("promptEn")]
public string PromptEn { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("submitTime")]
public long? SubmitTime { get; set; }
[JsonPropertyName("startTime")]
public long? StartTime { get; set; }
[JsonPropertyName("finishTime")]
public long? FinishTime { get; set; }
[JsonPropertyName("progress")]
public string Progress { get; set; }
[JsonPropertyName("imageUrl")]
public string ImageUrl { get; set; }
[JsonPropertyName("imageUrls")]
public List<ImageUrlItem> ImageUrls { get; set; }
[JsonPropertyName("videoUrl")]
public string? VideoUrl { get; set; }
[JsonPropertyName("videoUrls")]
public object? VideoUrls { get; set; }
[JsonPropertyName("imageHeight")]
public int? ImageHeight { get; set; }
[JsonPropertyName("imageWidth")]
public int? ImageWidth { get; set; }
[JsonPropertyName("failReason")]
public string? FailReason { get; set; }
[JsonPropertyName("notifyHook")]
public string NotifyHook { get; set; }
[JsonPropertyName("state")]
public object? State { get; set; }
[JsonPropertyName("buttons")]
public List<Button> Buttons { get; set; }
[JsonPropertyName("properties")]
public Properties Properties { get; set; }
}
public class ImageUrlItem
{
[JsonPropertyName("url")]
public string Url { get; set; }
[JsonPropertyName("webp")]
public string Webp { get; set; }
[JsonPropertyName("no")]
public int? No { get; set; }
[JsonPropertyName("thumbnail")]
public string Thumbnail { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
}
public class Button
{
[JsonPropertyName("customId")]
public string CustomId { get; set; }
[JsonPropertyName("emoji")]
public string Emoji { get; set; }
[JsonPropertyName("label")]
public string Label { get; set; }
[JsonPropertyName("type")]
public int? Type { get; set; }
[JsonPropertyName("style")]
public int? Style { get; set; }
}
public class Properties
{
[JsonPropertyName("botType")]
public string BotType { get; set; }
[JsonPropertyName("messageId")]
public string MessageId { get; set; }
[JsonPropertyName("notifyHook")]
public string NotifyHook { get; set; }
[JsonPropertyName("finalPrompt")]
public string FinalPrompt { get; set; }
[JsonPropertyName("discordInstanceId")]
public string DiscordInstanceId { get; set; }
}
}
public class MidjourneySubmitResponse
{
public class Response
{
[JsonPropertyName("code")]
public int? Code { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
[JsonPropertyName("result")]
public string Result { get; set; } = string.Empty;
[JsonPropertyName("properties")]
public ResponseProperties Properties { get; set; } = new();
}
public class ResponseProperties
{
[JsonPropertyName("discordInstanceId")]
public string DiscordInstanceId { get; set; } = string.Empty;
}
}
}

View File

@ -1,8 +1,13 @@
using lai_transfer; using lai_transfer;
using lai_transfer.Common.Helper; using lai_transfer.Common.Helper;
using Serilog;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// 临时初始化LogHelper用于配置加载
LogHelper.Initialize(LoggerFactory.Create(builder => builder.AddSerilog()));
ConfigHelper.Initialize();
// Add services to the container. // Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
@ -14,7 +19,8 @@ app.Configure();
// 初始化日志帮助类 // 初始化日志帮助类
LogHelper.Initialize(app.Services.GetRequiredService<ILoggerFactory>()); LogHelper.Initialize(app.Services.GetRequiredService<ILoggerFactory>());
ConfigHelper.Initialize();
// 初始化服务定位器 // 初始化服务定位器
ServiceLocator.Initialize(app.Services); ServiceLocator.Initialize(app.Services);

View File

@ -0,0 +1,43 @@
using lai_transfer.Common.Results;
using lai_transfer.Model.Midjourney;
using Microsoft.AspNetCore.Authorization;
using System.Text.Json;
using static lai_transfer.Model.Midjourney.MidjourneyRequest;
namespace lai_transfer.Services.Midjourney
{
public interface IMidjourneyService
{
/// <summary>
/// Midjourney 提交出图任务的方法
/// </summary>
/// <param name="model"></param>
/// <param name="authorizationResult"></param>
/// <returns></returns>
Task<IResult> MidjourneyImagine(JsonElement model, TransferAuthorizationResult authorizationResult);
/// <summary>
/// Midjourney 获取指定ID的任务直接从数据库获取
/// </summary>
/// <param name="id"></param>
/// <param name="authorizationResult"></param>
/// <returns></returns>
Task<IResult> MidjourneyFetchTaskById(string id, TransferAuthorizationResult authorizationResult);
/// <summary>
/// Midjourney 获取指定ID的任务并且检查图片进行拼接
/// </summary>
/// <param name="id"></param>
/// <param name="authorizationResult"></param>
/// <returns></returns>
Task<MidjourneyTaskResponse.TaskResponse?> MidjourneyFetchTaskByIdAndImageOption(string id, TransferAuthorizationResult authorizationResult);
/// <summary>
/// Midjourney 批量查询任务接口
/// </summary>
/// <param name="model"></param>
/// <param name="authorizationResult"></param>
/// <returns></returns>
Task<IResult> FetchListByCondition(FetchListByCondition model, TransferAuthorizationResult authorizationResult);
}
}

View File

@ -0,0 +1,332 @@
using lai_transfer.Common.Helper;
using lai_transfer.Common.Results;
using lai_transfer.Model.Entity;
using lai_transfer.Model.Midjourney;
using lai_transfer.Services.RealmService;
using lai_transfer.Services.Translate;
using lai_transfer.Tool.Extensions;
using lai_transfer.Tool.HttpTool;
using lai_transfer.Tool.ImageTool;
using lai_transfer.Tool.QiniuService;
using System.Text.Json;
using static lai_transfer.Tool.QiniuService.QiniuDataModel;
namespace lai_transfer.Services.Midjourney
{
public class MidjourneyService(IHttpService httpService,
IGlobalTranslateService globalTranslateService,
IQiniuService qiniuService,
IImageService imageService,
IMJTaskRealmService mJTaskRealmService) : IMidjourneyService
{
private static readonly ILogger _logger = LogHelper.GetLogger<MidjourneyService>();
private readonly IHttpService _httpService = httpService;
private readonly IGlobalTranslateService _globalTranslateService = globalTranslateService;
private readonly IQiniuService _qiniuService = qiniuService;
private readonly IImageService _imageService = imageService;
private readonly IMJTaskRealmService _mJTaskRealmService = mJTaskRealmService;
#region Public Method
public async Task<IResult> MidjourneyImagine(JsonElement model, TransferAuthorizationResult authorizationResult)
{
try
{
string url = $"{authorizationResult.BaseUrl}/mj/submit/imagine";
// 处理提示词
string prompt = JSONHelper.GetJsonPropertyString(model, "prompt") ?? string.Empty;
if (string.IsNullOrWhiteSpace(prompt))
{
prompt = JSONHelper.GetJsonPropertyString(model, "Prompt") ?? string.Empty;
}
_logger.LogInformation($"原始提示词: {prompt}");
// 开始调用翻译服务
prompt = await _globalTranslateService.TranslatePrompt(prompt);
_logger.LogInformation($"翻译后提示词: {prompt}");
model = JSONHelper.SetJsonProperty(model, "prompt", prompt);
// 删除回调参数 notifyHook
string body = JSONHelper.RemoveJsonProperties(model, "notifyHook");
// 开始提交任务,重试五次
MidjourneySubmitResponse.Response? d = await RetryHelper.RetryAsync<MidjourneySubmitResponse.Response?>(async () =>
{
return await _httpService.PostAsync<MidjourneySubmitResponse.Response?>(url, body, new HttpRequestConfig
{
Timeout = Timeout.InfiniteTimeSpan,
BearerToken = authorizationResult.Token,
Headers = new Dictionary<string, string>()
{
{ "mj-api-secret", authorizationResult.Token }
}
});
}, 5, TimeSpan.FromSeconds(1));
if (d == null)
{
_logger.LogError("请求失败,返回数据为空,请求数据{data}", body);
return Results.BadRequest("reponse is null");
}
string mjId = d.Result;
if (String.IsNullOrWhiteSpace(mjId))
{
_logger.LogError("请求失败,返回 mj id 为空,请求数据{data}", body);
return Results.BadRequest("mj id is null");
}
// 处理任务
await MidjourneyFetchTaskByIdAndImageOption(mjId, authorizationResult);
// 返回数据
return Results.Json(d);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error: {ex.Message}");
return Results.BadRequest("request error : " + ex.Message);
}
}
public async Task<IResult> MidjourneyFetchTaskById(string id, TransferAuthorizationResult authorizationResult)
{
try
{
MidjourneyTaskResponse.TaskResponse? d = await MidjourneyFetchTaskByIdResponse(id, authorizationResult);
return Results.Json(d);
}
catch (Exception ex)
{
_logger.LogError(ex, "{url} 请求失败,{error}", authorizationResult.FullPath, ex.Message);
return Results.BadRequest("request error : " + ex.Message);
}
}
public async Task<MidjourneyTaskResponse.TaskResponse?> MidjourneyFetchTaskByIdAndImageOption(string id, TransferAuthorizationResult authorizationResult)
{
try
{
string originUrl = $"{authorizationResult.BaseUrl}/mj/task/{id}/fetch";
// 提交获取任务
MidjourneyTaskResponse.TaskResponse? d =
await RetryHelper.RetryAsync<MidjourneyTaskResponse.TaskResponse?>(async () =>
{
return await _httpService.GetAsync<MidjourneyTaskResponse.TaskResponse?>(originUrl, new HttpRequestConfig
{
BearerToken = authorizationResult.Token,
Headers = new Dictionary<string, string>()
{
{ "mj-api-secret", authorizationResult.Token }
}
});
}, 5, TimeSpan.FromSeconds(1));
// 开始处理图片
if (d is null)
{
_logger.LogError("请求失败,返回数据为空,请求数据{data}", id);
return d;
}
if (d.Status != null && d.Status.Equals("failure", StringComparison.CurrentCultureIgnoreCase))
{
_logger.LogError("任务失败,请求数据{data},返回原始数据", id);
}
bool isImagine = d.Action.Equals("imagine", StringComparison.OrdinalIgnoreCase);
// 判断Action
if (!isImagine)
{
_logger.LogError("当前任务的Action不是绘图直接返回原始数据");
}
string newImageUrl = string.Empty;
if (isImagine && d.Status != null && d.Status.Equals("success", StringComparison.CurrentCultureIgnoreCase))
{
byte[] files = [];
string imageUrl = d.ImageUrl;
// 转存
if (!authorizationResult.Splice && authorizationResult.Storage)
{
if (!string.IsNullOrWhiteSpace(imageUrl))
{
files = await RetryHelper.RetryAsync<byte[]>(async () =>
{
return await _httpService.DownloadFileAsync(imageUrl);
}, 5);
}
else
{
_logger.LogError("请求失败,返回图片链接不存在,请求数据{data},返回原始数据", id);
}
}
// 重新拼接
if (authorizationResult.Splice)
{
List<string> imageUrls = [];
for (int i = 0; i < d.ImageUrls.Count; i++)
{
if (ConfigHelper.Qiniu.OriginImage)
{
imageUrls.Add(d.ImageUrls[i].Url ?? "");
}
else
{
imageUrls.Add(d.ImageUrls[i].Webp ?? "");
}
}
// 图片不够,不做后续的修改
if (imageUrls.Count == 4)
{
files = await _imageService.ImageCombiner(imageUrls, (int)d.ImageWidth, (int)d.ImageHeight);
}
}
if (files == null || files.Length <= 0)
{
_logger.LogError("请求失败,下载图片链接失败,请求数据 {data},图片链接 {imageUrl},返回原始数据", id, imageUrl);
}
else
{
// 开始转存
string fileName;
string fileKey;
if (ConfigHelper.Qiniu.OriginImage)
{
fileName = "orgin_" + id + ".png";
fileKey = "mj/orgin/" + fileName;
}
else
{
fileName = "orgin_" + id + ".webp";
fileKey = "mj/webp/" + fileName;
}
UploadResult u = await _qiniuService.UploadFileToQiNiu(files, fileName, fileKey);
if (u.Success)
{
// 成功
newImageUrl = u.Url;
}
else
{
// 失败,不修改图片链接
newImageUrl = imageUrl;
}
}
d.ImageUrl = String.IsNullOrWhiteSpace(newImageUrl) ? d.ImageUrl : newImageUrl;
}
// 开始修改数据
MJTaskDTO? mjDto = await _mJTaskRealmService.GetMjTaskByMjId(d.Id);
if (mjDto is null)
{
// 新增
mjDto = new MJTaskDTO()
{
ID = Guid.NewGuid().ToString(),
MJId = d.Id,
Type = d.Action,
Status = d.Status,
BaseUrl = authorizationResult.BaseUrl,
Property = JsonSerializer.Serialize(d),
AuthorizationResult = JsonSerializer.Serialize(authorizationResult),
CreatedTime = BeijingTimeExtension.GetBeijingTime(),
CheckedTime = BeijingTimeExtension.GetBeijingTime()
};
}
else
{
// 修改
mjDto.Status = d.Status;
mjDto.Property = JsonSerializer.Serialize(d);
mjDto.AuthorizationResult = JsonSerializer.Serialize(authorizationResult);
mjDto.CheckedTime = BeijingTimeExtension.GetBeijingTime();
}
// 调用修改或新增函数
await _mJTaskRealmService.ModifyOrAddMjTask(d.Id, mjDto);
return d;
}
catch (Exception ex)
{
_logger.LogError("{url} 请求失败,{error}", authorizationResult.FullPath, ex.Message);
throw;
}
}
public async Task<IResult> FetchListByCondition(MidjourneyRequest.FetchListByCondition model, TransferAuthorizationResult authorizationResult)
{
try
{
List<string> ids = model.Ids;
List<MidjourneyTaskResponse.TaskResponse> responses = [];
if (ids.Count <= 0)
{
return Results.Json(responses);
}
// 开始处理数据
for (int i = 0; i < ids.Count; i++)
{
string id = ids[i];
MidjourneyTaskResponse.TaskResponse? d = await MidjourneyFetchTaskByIdResponse(id, authorizationResult);
if (d is null)
{
continue;
}
responses.Add(d);
}
return Results.Json(responses);
}
catch (Exception ex)
{
_logger.LogError(ex, "{url} 请求失败,{error}", authorizationResult.FullPath, ex.Message);
return Results.BadRequest("request error : " + ex.Message);
}
}
#endregion
#region Private Method
/// <summary>
/// 获取MJTask的实际任务
/// </summary>
/// <param name="id"></param>
/// <param name="authorizationResult"></param>
/// <returns></returns>
private async Task<MidjourneyTaskResponse.TaskResponse?> MidjourneyFetchTaskByIdResponse(string id, TransferAuthorizationResult authorizationResult)
{
try
{
MJTaskDTO? mjDto = await _mJTaskRealmService.GetMjTaskByMjId(id);
if (mjDto is not null)
{
return JsonSerializer.Deserialize<MidjourneyTaskResponse.TaskResponse>(mjDto.Property);
}
MidjourneyTaskResponse.TaskResponse? d = await MidjourneyFetchTaskByIdAndImageOption(id, authorizationResult);
return d;
}
catch (Exception ex)
{
_logger.LogError(ex, "{url} 请求失败,获取任务失败,{error}", authorizationResult.FullPath, ex.Message);
throw;
}
}
#endregion
}
}

View File

@ -0,0 +1,105 @@
using lai_transfer.Common.Helper;
using lai_transfer.Common.Results;
using lai_transfer.EndpointServices.MJTransferEndpoint;
using lai_transfer.Model.Entity;
using lai_transfer.Services.Midjourney;
using lai_transfer.Services.RealmService;
using lai_transfer.Tool.Extensions;
using System.Diagnostics;
using System.Text.Json;
namespace lai_transfer.Services.PeriodicTimeService
{
public class PeriodicTimedService(IServiceScopeFactory serviceScopeFactory) : BackgroundService
{
private static readonly ILogger _logger = LogHelper.GetLogger<MJPostActionService>();
private readonly TimeSpan _interval = TimeSpan.FromSeconds(20);
private readonly IServiceScopeFactory _serviceScopeFactory = serviceScopeFactory;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("周期性定时服务已启动");
await ExecuteTaskAsync(stoppingToken);
}
private async Task ExecuteTaskAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var stopwatch = Stopwatch.StartNew();
var startTime = BeijingTimeExtension.GetBeijingTime();
_logger.LogInformation("{time} 开始执行定时任务...", startTime);
try
{
using var scope = _serviceScopeFactory.CreateScope();
var serviceProvider = scope.ServiceProvider;
var _midjourneyService = serviceProvider.GetRequiredService<IMidjourneyService>();
var _mJTaskRealmService = serviceProvider.GetRequiredService<IMJTaskRealmService>();
List<MJTaskDTO> mJTasks = await _mJTaskRealmService.GetMJTaskStatusNotFinish();
if (mJTasks.Count == 0)
{
_logger.LogInformation("不存在未完成的任务,执行完毕!");
}
else
{
for (int i = 0; i < mJTasks.Count; i++)
{
MJTaskDTO task = mJTasks[i];
if (task.AuthorizationResult == null)
{
// 删除当前这个,不处理 等外部请求再次触发
_logger.LogInformation("{mjid} 任务未找到对应的数据信息开始删除数据用户使用fetch接口时自动创建", task.MJId);
await _mJTaskRealmService.DeleteMJTaskById(task.ID);
continue;
}
TransferAuthorizationResult? authorizationResult = JsonSerializer.Deserialize<TransferAuthorizationResult>(task.AuthorizationResult);
if (authorizationResult is null)
{
// 删除当前这个,不处理 等外部请求再次触发
_logger.LogInformation("{mjid} 任务未找到对应的数据信息开始删除数据用户使用fetch接口时自动创建", task.MJId);
await _mJTaskRealmService.DeleteMJTaskById(task.ID);
}
else
{
_logger.LogInformation("{mjid} 开始处理", task.MJId);
await _midjourneyService.MidjourneyFetchTaskByIdAndImageOption(task.MJId, authorizationResult);
}
// 等一秒执行下一个
await Task.Delay(1000, stoppingToken);
}
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "任务执行失败");
}
finally
{
stopwatch.Stop();
var executionTime = stopwatch.Elapsed;
_logger.LogInformation("任务执行完成,耗时: {ExecutionTime}", executionTime);
// 计算剩余等待时间
var remainingTime = _interval - executionTime;
if (remainingTime > TimeSpan.Zero)
{
_logger.LogDebug("等待 {RemainingTime} 后执行下一次任务", remainingTime);
await Task.Delay(remainingTime, stoppingToken);
}
else
{
_logger.LogWarning("任务执行时间 {ExecutionTime} 超过间隔 {Interval}, 立即开始下一次",
executionTime, _interval);
}
}
}
}
}
}

View File

@ -0,0 +1,192 @@
using lai_transfer.Model.Entity;
using lai_transfer.Tool.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
namespace lai_transfer.Services.RealmService
{
public interface IMJTaskRealmService
{
/// <summary>
/// 修改或者是创建MJTask
/// 判断mjId存在修改 不存再创建
/// </summary>
/// <param name="mjId"></param>
/// <param name="mJTask"></param>
/// <returns></returns>
Task<MJTaskDTO?> ModifyOrAddMjTask(string mjId, MJTaskDTO mJTask);
/// <summary>
/// 获取指定的ID的任务
/// </summary>
/// <param name="Id"></param>
/// <returns></returns>
Task<MJTaskDTO?> GetMjTaskById(string Id);
/// <summary>
/// 获取指定的 MJ ID 的任务
/// </summary>
/// <param name="mjId"></param>
/// <returns></returns>
Task<MJTaskDTO?> GetMjTaskByMjId(string mjId);
/// <summary>
/// 获取没有的完成的MJ的任务
/// </summary>
/// <returns></returns>
Task<List<MJTaskDTO>> GetMJTaskStatusNotFinish();
/// <summary>
/// 删除指定ID的MJTask
/// </summary>
/// <param name="Id"></param>
/// <returns></returns>
Task DeleteMJTaskById(string Id);
}
public class MJTaskRealmService(RealmService realmService) : IMJTaskRealmService
{
private readonly RealmService _realmService = realmService;
public async Task<MJTaskDTO?> ModifyOrAddMjTask(string mjId, MJTaskDTO mJTask)
{
// 查找数据是否存在
return await _realmService.ExecuteTransactionAsync<MJTaskDTO?>(realm =>
{
MJTask? mj = realm.All<MJTask>().FirstOrDefault(x => x.MJId == mjId);
if (mj is null)
{
// 新增
MJTask newMj = new()
{
ID = mJTask.ID,
MJId = mJTask.MJId,
Type = mJTask.Type,
Status = mJTask.Status,
BaseUrl = mJTask.BaseUrl,
Property = mJTask.Property,
AuthorizationResult = mJTask.AuthorizationResult,
CreatedTime = BeijingTimeExtension.GetBeijingTime(),
CheckedTime = BeijingTimeExtension.GetBeijingTime(),
};
realm.Add(newMj);
mj = newMj;
return null;
}
else
{
// 修改
mj.Type = mJTask.Type;
mj.Status = mJTask.Status;
mj.BaseUrl = mJTask.BaseUrl;
mj.Property = mJTask.Property;
mj.AuthorizationResult = mJTask.AuthorizationResult;
mj.CreatedTime = mJTask.CreatedTime;
mj.CheckedTime = mJTask.CheckedTime;
return mJTask;
}
});
}
public async Task<MJTaskDTO?> GetMjTaskById(string Id)
{
// 查找数据是否存在
return await _realmService.ExecuteQueryAsync<MJTaskDTO?>(realm =>
{
MJTask? mj = realm.Find<MJTask>(Id);
if (mj is null)
return null;
// 开始处理返回数据
MJTaskDTO mjDto = new()
{
ID = mj.ID,
MJId = mj.MJId,
Type = mj.Type,
Status = mj.Status,
BaseUrl = mj.BaseUrl,
Property = mj.Property,
AuthorizationResult = mj.AuthorizationResult,
CreatedTime = mj.CreatedTime,
CheckedTime = mj.CheckedTime,
};
return mjDto;
});
}
public async Task<MJTaskDTO?> GetMjTaskByMjId(string mjId)
{
// 查找数据是否存在
return await _realmService.ExecuteQueryAsync<MJTaskDTO?>(realm =>
{
MJTask? mj = realm.All<MJTask>().FirstOrDefault(x => x.MJId == mjId);
if (mj is null)
return null;
// 开始处理返回数据
MJTaskDTO mjDto = new()
{
ID = mj.ID,
MJId = mj.MJId,
Type = mj.Type,
Status = mj.Status,
BaseUrl = mj.BaseUrl,
Property = mj.Property,
AuthorizationResult = mj.AuthorizationResult,
CreatedTime = mj.CreatedTime,
CheckedTime = mj.CheckedTime,
};
return mjDto;
});
}
public async Task<List<MJTaskDTO>> GetMJTaskStatusNotFinish()
{
// 查找数据是否存在
return await _realmService.ExecuteQueryAsync<List<MJTaskDTO>>(realm =>
{
// 使用 StringComparison.OrdinalIgnoreCase 进行不区分大小写的比较
var query = realm.All<MJTask>()
.Where(x => !x.Status.Equals("success", StringComparison.OrdinalIgnoreCase)
&& !x.Status.Equals("failure", StringComparison.OrdinalIgnoreCase));
List<MJTask> mjs = query.ToList();
List<MJTaskDTO> mJTaskDTOs = new List<MJTaskDTO>();
for (int i = 0; i < mjs.Count; i++)
{
MJTask mj = mjs[i];
// 开始处理返回数据
MJTaskDTO mjDto = new()
{
ID = mj.ID,
MJId = mj.MJId,
Type = mj.Type,
Status = mj.Status,
BaseUrl = mj.BaseUrl,
Property = mj.Property,
AuthorizationResult = mj.AuthorizationResult,
CreatedTime = mj.CreatedTime,
CheckedTime = mj.CheckedTime,
};
mJTaskDTOs.Add(mjDto);
}
return mJTaskDTOs;
});
}
public async Task DeleteMJTaskById(string Id)
{
await _realmService.ExecuteTransactionAsync(realm =>
{
MJTask? mj = realm.Find<MJTask>(Id);
if (mj is not null)
realm.Remove(mj);
});
}
}
}

View File

@ -0,0 +1,205 @@
using lai_transfer.Common.Helper;
using Realms;
namespace lai_transfer.Services.RealmService
{
public class RealmService : IDisposable
{
private readonly RealmConfiguration _config;
private bool _disposed;
private static readonly ILogger _logger = LogHelper.GetLogger<RealmService>();
public RealmService()
{
string dataBaseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Configuration", "DataBase");
Directory.CreateDirectory(dataBaseDirectory);
string realmPath = Path.Combine(dataBaseDirectory, "db.realm");
_config = new RealmConfiguration(realmPath)
{
SchemaVersion = 3,
MigrationCallback = HandleMigration
};
}
/// <summary>
/// 获取 Realm 实例,用于直接操作数据库(同步方法,注意线程安全)
/// </summary>
public Realm GetInstance() => Realm.GetInstance(_config);
/// <summary>
/// 异步执行事务操作(无返回值)
/// </summary>
public async Task ExecuteTransactionAsync(Action<Realm> action)
{
await Task.Run(async () =>
{
using var realm = await Realm.GetInstanceAsync(_config);
await realm.WriteAsync(() =>
{
action(realm);
});
});
}
// 在 RealmService 中添加支持返回值的事务方法
public async Task<T> ExecuteTransactionWithResultAsync<T>(Func<Realm, T> action)
{
using var realm = await Realm.GetInstanceAsync(_config);
using var transaction = realm.BeginWrite();
try
{
var result = action(realm);
transaction.Commit();
return result;
}
catch
{
transaction.Rollback();
throw;
}
}
/// <summary>
/// 异步执行事务操作(有返回值)
/// </summary>
public async Task<T> ExecuteTransactionAsync<T>(Func<Realm, T> func)
{
return await Task.Run(async () =>
{
using var realm = await Realm.GetInstanceAsync(_config);
return await realm.WriteAsync(() =>
{
return func(realm);
});
});
}
/// <summary>
/// 同步执行事务操作(无返回值)
/// </summary>
public void ExecuteTransaction(Action<Realm> action)
{
using var realm = Realm.GetInstance(_config);
realm.Write(() =>
{
action(realm);
});
}
/// <summary>
/// 同步执行事务操作(有返回值)
/// </summary>
public T ExecuteTransaction<T>(Func<Realm, T> func)
{
using var realm = Realm.GetInstance(_config);
return realm.Write(() =>
{
return func(realm);
});
}
/// <summary>
/// 安全执行事务(无返回值)
/// </summary>
public async Task<bool> TryExecuteTransactionAsync(Action<Realm> action)
{
try
{
await ExecuteTransactionAsync(action);
return true;
}
catch (Exception ex)
{
return false;
}
}
/// <summary>
/// 安全执行事务(有返回值)
/// </summary>
public async Task<(bool Success, T? Result)> TryExecuteTransactionAsync<T>(Func<Realm, T> func)
{
try
{
var result = await ExecuteTransactionAsync(func);
return (true, result);
}
catch (Exception ex)
{
Console.WriteLine($"Transaction failed: {ex.Message}");
return (false, default);
}
}
/// <summary>
/// 异步查询操作(只读)
/// </summary>
public async Task<T> ExecuteQueryAsync<T>(Func<Realm, T> query)
{
return await Task.Run(async () =>
{
using var realm = await Realm.GetInstanceAsync(_config);
return query(realm);
});
}
/// <summary>
/// 同步查询操作(只读)
/// </summary>
public T ExecuteQuery<T>(Func<Realm, T> query)
{
using var realm = Realm.GetInstance(_config);
return query(realm);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// 压缩数据库以释放空间
try
{
Realm.Compact(_config);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to compact database: {ex.Message}");
}
}
_disposed = true;
}
}
~RealmService() => Dispose(false);
private void HandleMigration(Migration migration, ulong oldSchemaVersion)
{
//if (oldSchemaVersion < 2)
//{
// _logger.LogInformation("正在执行 Realm 数据库迁移...");
// var newObjects = migration.NewRealm.DynamicApi.All("Siliconflow");
// Console.WriteLine($"正在为 {newObjects.Count()} 个对象设置 newId 字段...");
// foreach (dynamic obj in newObjects)
// {
// obj.NewId = string.Empty;
// }
// _logger.LogInformation("Realm 数据库迁移完成。");
//}
}
}
}

View File

@ -1,12 +1,10 @@
 using lai_transfer.Common.Helper;
using lai_transfer.Common.Helper;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace lai_transfer.Services namespace lai_transfer.Services.Translate
{ {
/// <summary> /// <summary>
/// 百度翻译服务 /// 百度翻译服务

View File

@ -1,10 +1,9 @@
using lai_transfer.Common.Helper; using lai_transfer.Common.Helper;
using lai_transfer.Services;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Midjourney.Infrastructure.Services namespace lai_transfer.Services.Translate
{ {
/// <summary> /// <summary>
/// OpenAI GPT翻译服务 /// OpenAI GPT翻译服务

View File

@ -1,4 +1,4 @@
namespace lai_transfer.Services namespace lai_transfer.Services.Translate
{ {
public interface ITranslateService public interface ITranslateService
{ {

View File

@ -0,0 +1,80 @@
using lai_transfer.Common.Helper;
using System.Text.RegularExpressions;
namespace lai_transfer.Services.Translate
{
public interface IGlobalTranslateService
{
Task<string> TranslatePrompt(string prompt);
}
public class TranslateService(ITranslateService translateService) : IGlobalTranslateService
{
private readonly ITranslateService _translateService = translateService;
public async Task<string> TranslatePrompt(string prompt)
{
if (string.IsNullOrWhiteSpace(prompt))
return prompt;
try
{
// 检查是否启用翻译功能
if (!ConfigHelper.Translate.Enable)
return prompt;
// 如果不包含中文,直接返回
if (string.IsNullOrWhiteSpace(prompt) || !_translateService.ContainsChinese(prompt))
return prompt;
string paramStr = "";
var paramMatcher = Regex.Match(prompt, "\\x20+--[a-z]+.*$", RegexOptions.IgnoreCase);
if (paramMatcher.Success)
{
paramStr = paramMatcher.Value;
}
string promptWithoutParam = prompt.Substring(0, prompt.Length - paramStr.Length);
List<string> imageUrls = new List<string>();
var imageMatcher = Regex.Matches(promptWithoutParam, "https?://[a-z0-9-_:@&?=+,.!/~*'%$]+\\x20+", RegexOptions.IgnoreCase);
foreach (Match match in imageMatcher)
{
imageUrls.Add(match.Value);
}
string text = promptWithoutParam;
foreach (string imageUrl in imageUrls)
{
text = text.Replace(imageUrl, "");
}
text = text.Trim();
if (!string.IsNullOrWhiteSpace(text))
{
text = await _translateService.TranslateToEnglish(text);
}
if (!string.IsNullOrWhiteSpace(paramStr))
{
// 当有 --no 参数时, 翻译 --no 参数, 并替换原参数
// --sref https://mjcdn.googlec.cc/1.jpg --no aa, bb, cc
var paramNomatcher = Regex.Match(paramStr, "--no\\s+(.*?)(?=--|$)");
if (paramNomatcher.Success)
{
string paramNoStr = paramNomatcher.Groups[1].Value.Trim();
string paramNoStrEn = await _translateService.TranslateToEnglish(paramNoStr);
// 提取 --no 之前的参数
paramStr = paramStr.Substring(0, paramNomatcher.Index);
// 替换 --no 参数
paramStr = paramStr + paramNomatcher.Result("--no " + paramNoStrEn + " ");
}
}
return string.Concat(imageUrls) + text.Trim() + paramStr;
}
catch (Exception ex)
{
LogHelper.LogError(ex, "翻译提示词失败,使用原始提示词");
return prompt;
}
}
}
}

View File

@ -0,0 +1,302 @@
using Microsoft.Extensions.Logging;
using System.Text;
using System.Text.Json;
namespace lai_transfer.Tool.HttpTool;
public class HttpRequestConfig
{
public string? BaseUrl { get; set; }
public Dictionary<string, string> Headers { get; set; } = new();
public Dictionary<string, string> QueryParams { get; set; } = new();
public string? BearerToken { get; set; }
public TimeSpan? Timeout { get; set; }
public string? MediaType { get; set; } = "application/json";
public CancellationToken CancellationToken { get; set; } = CancellationToken.None;
}
public class HttpRequestConfig<T> : HttpRequestConfig
{
public T? Data { get; set; }
}
public interface IHttpService
{
Task<T?> GetAsync<T>(string url, HttpRequestConfig? config = null);
Task<string> GetStringAsync(string url, HttpRequestConfig? config = null);
Task<T?> PostAsync<T>(string url, object data, HttpRequestConfig? config = null);
Task<T?> PostAsync<T>(string url, HttpRequestConfig<T> config);
Task<T?> PutAsync<T>(string url, object data, HttpRequestConfig? config = null);
Task<bool> DeleteAsync(string url, HttpRequestConfig? config = null);
Task<Stream> GetStreamAsync(string url, HttpRequestConfig? config = null);
Task<byte[]> DownloadFileAsync(string url, HttpRequestConfig? config = null);
}
public class HttpService : IHttpService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<HttpService> _logger;
public HttpService(IHttpClientFactory httpClientFactory, ILogger<HttpService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
/// <summary>
/// 配置HttpRequestMessage
/// </summary>
private HttpRequestMessage CreateHttpRequest(HttpMethod method, string url, HttpRequestConfig? config)
{
var requestUrl = BuildUrl(url, config);
var request = new HttpRequestMessage(method, requestUrl);
// 设置请求头
SetRequestHeaders(request, config);
return request;
}
/// <summary>
/// 构建完整URL包含查询参数
/// </summary>
private string BuildUrl(string url, HttpRequestConfig? config)
{
if (config?.QueryParams == null || !config.QueryParams.Any())
return url;
var uriBuilder = new UriBuilder(url);
var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
foreach (var param in config.QueryParams)
{
query[param.Key] = param.Value;
}
uriBuilder.Query = query.ToString();
return uriBuilder.ToString();
}
/// <summary>
/// 设置请求头
/// </summary>
private void SetRequestHeaders(HttpRequestMessage request, HttpRequestConfig? config)
{
if (config == null) return;
// 设置Bearer Token
if (!string.IsNullOrEmpty(config.BearerToken))
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", config.BearerToken);
}
// 设置自定义请求头
foreach (var header in config.Headers)
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
// 设置Content-Type
if (request.Content != null && !string.IsNullOrEmpty(config.MediaType))
{
request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(config.MediaType);
}
}
/// <summary>
/// 发送HTTP请求
/// </summary>
private async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpRequestConfig? config, string clientName = "DefaultClient")
{
using var httpClient = _httpClientFactory.CreateClient(clientName);
// 设置超时
if (config?.Timeout.HasValue == true)
{
httpClient.Timeout = config.Timeout.Value;
}
return await httpClient.SendAsync(request, config?.CancellationToken ?? CancellationToken.None);
}
/// <summary>
/// GET请求 - 返回泛型类型
/// </summary>
public async Task<T?> GetAsync<T>(string url, HttpRequestConfig? config = null)
{
try
{
var request = CreateHttpRequest(HttpMethod.Get, url, config);
var response = await SendAsync(request, config);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (Exception ex)
{
_logger.LogError(ex, "GET请求失败: {Url}", url);
throw;
}
}
/// <summary>
/// GET请求 - 返回字符串
/// </summary>
public async Task<string> GetStringAsync(string url, HttpRequestConfig? config = null)
{
try
{
var request = CreateHttpRequest(HttpMethod.Get, url, config);
var response = await SendAsync(request, config);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "GET字符串请求失败: {Url}", url);
throw;
}
}
/// <summary>
/// POST请求
/// </summary>
public async Task<T?> PostAsync<T>(string url, object data, HttpRequestConfig? config = null)
{
try
{
var request = CreateHttpRequest(HttpMethod.Post, url, config);
// 判断data是不是string 不是的话才序列话
string json;
if (data is string str)
{
// 如果 data 已经是字符串,直接使用
json = str;
}
else
{
// 如果是对象,序列化为 JSON
json = JsonSerializer.Serialize(data);
}
request.Content = new StringContent(json, Encoding.UTF8, config?.MediaType ?? "application/json");
var response = await SendAsync(request, config);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (Exception ex)
{
_logger.LogError(ex, "POST请求失败: {Url}", url);
throw;
}
}
/// <summary>
/// POST请求 - 泛型配置版本
/// </summary>
public async Task<T?> PostAsync<T>(string url, HttpRequestConfig<T> config)
{
return await PostAsync<T>(url, config.Data!, config);
}
/// <summary>
/// PUT请求
/// </summary>
public async Task<T?> PutAsync<T>(string url, object data, HttpRequestConfig? config = null)
{
try
{
var request = CreateHttpRequest(HttpMethod.Put, url, config);
var json = JsonSerializer.Serialize(data);
request.Content = new StringContent(json, Encoding.UTF8, config?.MediaType ?? "application/json");
var response = await SendAsync(request, config);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
catch (Exception ex)
{
_logger.LogError(ex, "PUT请求失败: {Url}", url);
throw;
}
}
/// <summary>
/// DELETE请求
/// </summary>
public async Task<bool> DeleteAsync(string url, HttpRequestConfig? config = null)
{
try
{
var request = CreateHttpRequest(HttpMethod.Delete, url, config);
var response = await SendAsync(request, config);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
_logger.LogError(ex, "DELETE请求失败: {Url}", url);
throw;
}
}
/// <summary>
/// 下载文件流
/// </summary>
public async Task<Stream> GetStreamAsync(string url, HttpRequestConfig? config = null)
{
try
{
var request = CreateHttpRequest(HttpMethod.Get, url, config);
var response = await SendAsync(request, config);
response.EnsureSuccessStatusCode();
var stream = await response.Content.ReadAsStreamAsync();
// 转移所有权,调用方负责释放
response = null;
return stream;
}
catch (Exception ex)
{
_logger.LogError(ex, "获取流失败: {Url}", url);
throw;
}
}
/// <summary>
/// 下载文件字节数组
/// </summary>
public async Task<byte[]> DownloadFileAsync(string url, HttpRequestConfig? config = null)
{
try
{
using var stream = await GetStreamAsync(url, config);
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "下载文件失败: {Url}", url);
throw;
}
}
}

View File

@ -0,0 +1,50 @@
namespace LMS.Tools.HttpTool
{
/// <summary>
/// HTTP服务接口
/// </summary>
public interface IHttpService
{
/// <summary>
/// 下载文件并返回字节数组
/// </summary>
/// <param name="url">文件URL</param>
/// <param name="maxFileSize">最大文件大小限制(字节)</param>
/// <returns>文件字节数组</returns>
Task<byte[]?> DownloadFileAsync(string url, double maxFileSize);
/// <summary>
/// 执行自定义异步HTTP请求
/// </summary>
/// <typeparam name="T">自定义请求函数返回的结果类型</typeparam>
/// <param name="requestFunc">接收已初始化HttpClient的函数返回表示要执行的异步操作的任务。不能为null。</param>
/// <returns>表示异步操作的任务。任务结果包含自定义请求函数返回的值。</returns>
/// <remarks>
/// 此方法允许您使用预配置的HttpClient执行完全自定义的HTTP请求。
/// 您可以在requestFunc中执行任何HTTP操作GET、POST、PUT、DELETE等
/// 并返回任何类型的结果。HttpClient的生命周期由服务管理。
///
/// 使用示例:
/// var result = await httpService.CustomRequestAsync(async client =>
/// {
/// var response = await client.GetAsync("https://api.example.com/data");
/// return await response.Content.ReadAsStringAsync();
/// });
/// </remarks>
Task<T> CustomRequestAsync<T>(Func<HttpClient, Task<T>> requestFunc);
/// <summary>
/// 检查URL是否可访问
/// </summary>
/// <param name="url">要检查的URL</param>
/// <returns>是否可访问</returns>
Task<bool> IsUrlAccessibleAsync(string url);
/// <summary>
/// 获取URL的Content-Type
/// </summary>
/// <param name="url">要检查的URL</param>
/// <returns>Content-Type</returns>
Task<string?> GetContentTypeAsync(string url);
}
}

View File

@ -0,0 +1,14 @@
namespace lai_transfer.Tool.ImageTool
{
public interface IImageService
{
/// <summary>
/// 合并图片 生成一个四宫格
/// </summary>
/// <param name="imageUrls"></param>
/// <param name="width"></param>
/// <param name="hight"></param>
/// <returns></returns>
Task<byte[]> ImageCombiner(List<string> imageUrls, int width, int hight);
}
}

View File

@ -0,0 +1,118 @@

using lai_transfer.Common.Helper;
using lai_transfer.Tool.HttpTool;
using SkiaSharp;
namespace lai_transfer.Tool.ImageTool
{
public class ImageService(IHttpService httpService) : IImageService
{
private static readonly ILogger _logger = LogHelper.GetLogger<ImageService>();
private readonly TimeSpan _downloadTimeout = TimeSpan.FromMinutes(3);
private readonly IHttpService _httpService = httpService;
#region Public Method
public async Task<byte[]> ImageCombiner(List<string> imageUrls, int width, int height)
{
if (imageUrls == null || imageUrls.Count != 4)
throw new ArgumentException("必须提供4个图片URL");
int finalWidth = width * 2;
int finalHeight = height * 2;
// 创建画布
using var surface = SKSurface.Create(new SKImageInfo(finalWidth, finalHeight));
var canvas = surface.Canvas;
// 填充白色背景
canvas.Clear(SKColors.White);
// 下载并绘制图片
var tasks = new List<Task>();
for (int i = 0; i < 4; i++)
{
tasks.Add(DrawImageAsync(canvas, imageUrls[i], i, width, height));
}
await Task.WhenAll(tasks);
// 编码为JPEG
using var image = surface.Snapshot();
SKEncodedImageFormat sif = SKEncodedImageFormat.Png;
if (!ConfigHelper.Qiniu.OriginImage)
{
sif = SKEncodedImageFormat.Webp;
}
using var data = image.Encode(sif, 100);
byte[] spliceImageByte = data.ToArray();
return spliceImageByte;
}
#endregion
#region Private Method
private async Task DrawImageAsync(SKCanvas canvas, string imageUrl, int position, int width, int height)
{
int row = position / 2;
int col = position % 2;
int x = col * width;
int y = row * height;
SKBitmap bitmap = null;
try
{
if (!string.IsNullOrEmpty(imageUrl))
{
// 下载图片
byte[] imageData =
await RetryHelper.RetryAsync<byte[]>(async () =>
{
return await _httpService.DownloadFileAsync(imageUrl);
}, 5);
bitmap = SKBitmap.Decode(imageData);
if (bitmap != null)
{
// 调整尺寸并绘制
canvas.DrawBitmap(bitmap, x, y);
return;
}
}
// 如果URL为空或下载失败绘制白色方块
DrawWhiteBlock(canvas, x, y, width, height);
}
catch (Exception ex)
{
// 下载或处理失败,绘制白色方块
Console.WriteLine($"图片 {position} 处理失败: {ex.Message}");
DrawWhiteBlock(canvas, x, y, width, height);
}
finally
{
bitmap?.Dispose();
}
}
private void DrawWhiteBlock(SKCanvas canvas, float x, float y, float width, float height)
{
using var paint = new SKPaint { Color = SKColors.White };
canvas.DrawRect(x, y, width, height, paint);
}
#endregion
}
}

View File

@ -0,0 +1,57 @@
using static lai_transfer.Tool.QiniuService.QiniuDataModel;
namespace lai_transfer.Tool.QiniuService
{
public interface IQiniuService
{
/// <summary>
/// 检查文件的字节大小是否符合要求
/// </summary>
/// <param name="fileBytes"></param>
/// <returns></returns>
public UploadResult CheckFileBytesSize(byte[] fileBytes);
/// <summary>
/// 生成七牛云上传的路径 key
/// </summary>
/// <param name="userId"></param>
/// <param name="fileName"></param>
/// <returns></returns>
string GenerateFileKey(long userId, string fileName);
/// <summary>
/// 计算文件的 SHA1 哈希值
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
string ComputeSHA1Hash(byte[] data);
/// <summary>
/// 获取七牛云的配置 用于上传图片
/// </summary>
/// <returns></returns>
Task<QiniuSettings> InitQiniuSetting();
/// <summary>
/// 生成七牛的上传凭证
/// </summary>
/// <param name="qiniuSettings"></param>
/// <returns></returns>
string GeneratePolicy(QiniuSettings qiniuSettings);
/// <summary>
/// 将 byte 数组上传到七牛云
/// </summary>
/// <param name="fileBytes"></param>
/// <returns></returns>
Task<UploadResult> UploadFileToQiNiu(byte[] fileBytes, string fileName, string fileKey);
/// <summary>
/// 构建文件的访问 URL
/// </summary>
/// <param name="domain"></param>
/// <param name="fileKey"></param>
/// <returns></returns>
string BuildFileUrl(string domain, string fileKey);
}
}

View File

@ -0,0 +1,33 @@
namespace lai_transfer.Tool.QiniuService
{
public class QiniuDataModel
{
public class QiniuSettings
{
public string AccessKey { get; set; }
public string SecretKey { get; set; }
public string BucketName { get; set; }
public string Domain { get; set; }
public double MaxFileSize { get; set; }
/// <summary>
/// 删除时间 天数 没有值 则不删除
/// </summary>
public int? DeleteDay { get; set; }
}
public class UploadResult
{
public bool Success { get; set; }
public string Message { get; set; }
public string Url { get; set; }
public string FileKey { get; set; }
public string Hash { get; set; }
public long FileId { get; set; }
public long FileSize { get; set; }
}
}
}

View File

@ -0,0 +1,145 @@
using lai_transfer.Common.Helper;
using Qiniu.Http;
using Qiniu.IO;
using Qiniu.IO.Model;
using Qiniu.Util;
using System.Security.Cryptography;
using static lai_transfer.Tool.QiniuService.QiniuDataModel;
namespace lai_transfer.Tool.QiniuService
{
public class QiniuService(ILogger<QiniuService> logger) : IQiniuService
{
private readonly UploadManager _uploadManager = new();
private readonly ILogger<QiniuService> _logger = logger;
/// <summary>
/// 检查文件的字节大小是否符合要求
/// </summary>
/// <param name="fileBytes"></param>
/// <returns></returns>
public UploadResult CheckFileBytesSize(byte[] fileBytes)
{
if (fileBytes == null || fileBytes.Length == 0)
{
return new UploadResult
{
Success = false,
Message = "文件字节数据不能为空"
};
}
double MaxFileSize = ConfigHelper.Qiniu.MaxFileSize;
if (fileBytes.Length > MaxFileSize * 1024 * 1024)
{
return new UploadResult
{
Success = false,
Message = $"文件大小不能超过 {MaxFileSize}MB"
};
}
return new UploadResult { Success = true, Message = string.Empty };
}
public string ComputeSHA1Hash(byte[] data)
{
var hash = SHA1.HashData(data);
return Convert.ToHexString(hash).ToLower();
}
public string GenerateFileKey(long userId, string fileName)
{
var date = DateTime.Now.ToString("yyyyMMdd");
//var extension = Path.GetExtension(fileName);
return $"user/{userId}/{date}/{fileName}";
}
public async Task<QiniuSettings> InitQiniuSetting()
{
QiniuSettings qiniuSettings = new()
{
AccessKey = ConfigHelper.Qiniu.AccessKey,
Domain = ConfigHelper.Qiniu.Domain,
SecretKey = ConfigHelper.Qiniu.SecretKey,
BucketName = ConfigHelper.Qiniu.BucketName,
};
if (qiniuSettings == null || string.IsNullOrEmpty(qiniuSettings.AccessKey) || string.IsNullOrEmpty(qiniuSettings.SecretKey) || string.IsNullOrEmpty(qiniuSettings.BucketName) || string.IsNullOrEmpty(qiniuSettings.Domain))
{
throw new Exception("七牛云配置不完整,请检查配置,请联系管理员");
}
return qiniuSettings;
}
public string GeneratePolicy(QiniuSettings qiniuSettings)
{
Mac mac = new(qiniuSettings.AccessKey, qiniuSettings.SecretKey);
var putPolicy = new PutPolicy
{
Scope = qiniuSettings.BucketName
};
if (qiniuSettings.DeleteDay != null)
{
putPolicy.DeleteAfterDays = qiniuSettings.DeleteDay.Value; // 设置过期时间
}
putPolicy.SetExpires(3600);
string token = Auth.CreateUploadToken(mac, putPolicy.ToJsonString());
return token;
}
public string BuildFileUrl(string domain, string fileKey)
{
return $"{domain}/{fileKey}";
}
public async Task<UploadResult> UploadFileToQiNiu(byte[] fileBytes, string fileName, string fileKey)
{
try
{
QiniuSettings qiniuSettings = await InitQiniuSetting();
string token = GeneratePolicy(qiniuSettings);
string hash = ComputeSHA1Hash(fileBytes);
HttpResult uploadResult;
_logger.LogInformation("开始上传文件, 文件名: {fileName}, 文件大小: {fileLength} 字节, 文件Key: {fileKey}", fileName, fileBytes.Length, fileKey);
using (var stream = new MemoryStream(fileBytes))
{
uploadResult = await _uploadManager.UploadStreamAsync(stream, fileKey, token);
}
// 8. 检查上传结果
if (uploadResult.Code != 200)
{
_logger.LogError("文件上传失败, 错误信息: {error}", uploadResult.Text);
throw new Exception(uploadResult.Text);
}
var qiniuUrl = BuildFileUrl(qiniuSettings.Domain, fileKey);
_logger.LogInformation("文件上传成功, 文件Key: {fileKey},文件链接: {url}", fileKey, qiniuUrl);
UploadResult uploadResult1 = new()
{
Success = true,
Message = "upload success",
Url = qiniuUrl,
FileKey = fileKey,
Hash = hash,
FileId = 0,
FileSize = fileBytes.LongLength,
};
return uploadResult1;
}
catch (Exception ex)
{
_logger.LogError(ex, "上传文件到七牛云失败, 文件名: {fileName},失败原因: {message}", fileName, ex.Message);
return new UploadResult
{
Success = false,
Message = ex.Message
};
}
}
}
}

View File

@ -26,14 +26,20 @@
</PackageReference> </PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <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="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.2.efcore.9.0.0" />
<PackageReference Include="Qiniu.Shared" Version="7.2.15" />
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SkiaSharp" Version="3.119.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.119.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Common\Middleware\" /> <Folder Include="Common\Middleware\" />
<Folder Include="Examples\" />
</ItemGroup> </ItemGroup>
</Project> </Project>