feat: 完成图片异步生成
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.BackgroundJobs;
|
||||
using Volo.Abp.Guids;
|
||||
using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Chat;
|
||||
using Yi.Framework.AiHub.Application.Jobs;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// AI图片生成服务
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class AiImageService : ApplicationService
|
||||
{
|
||||
private readonly ISqlSugarRepository<ImageStoreTaskAggregateRoot> _imageTaskRepository;
|
||||
private readonly IBackgroundJobManager _backgroundJobManager;
|
||||
private readonly AiBlacklistManager _aiBlacklistManager;
|
||||
private readonly PremiumPackageManager _premiumPackageManager;
|
||||
private readonly IGuidGenerator _guidGenerator;
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
|
||||
public AiImageService(
|
||||
ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageTaskRepository,
|
||||
IBackgroundJobManager backgroundJobManager,
|
||||
AiBlacklistManager aiBlacklistManager,
|
||||
PremiumPackageManager premiumPackageManager,
|
||||
IGuidGenerator guidGenerator,
|
||||
IWebHostEnvironment webHostEnvironment)
|
||||
{
|
||||
_imageTaskRepository = imageTaskRepository;
|
||||
_backgroundJobManager = backgroundJobManager;
|
||||
_aiBlacklistManager = aiBlacklistManager;
|
||||
_premiumPackageManager = premiumPackageManager;
|
||||
_guidGenerator = guidGenerator;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成图片(异步任务)
|
||||
/// </summary>
|
||||
/// <param name="input">图片生成输入参数</param>
|
||||
/// <returns>任务ID</returns>
|
||||
[HttpPost("ai-image/generate")]
|
||||
[Authorize]
|
||||
public async Task<Guid> GenerateAsync([FromBody] ImageGenerationInput input)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
|
||||
// 黑名单校验
|
||||
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||
|
||||
// VIP校验
|
||||
if (!CurrentUser.IsAiVip())
|
||||
{
|
||||
throw new UserFriendlyException("图片生成功能需要VIP用户才能使用,请购买VIP后重新登录重试");
|
||||
}
|
||||
|
||||
// 尊享包校验
|
||||
if (PremiumPackageConst.ModeIds.Contains(input.ModelId))
|
||||
{
|
||||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
|
||||
if (availableTokens <= 0)
|
||||
{
|
||||
throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建任务实体
|
||||
var task = new ImageStoreTaskAggregateRoot
|
||||
{
|
||||
Prompt = input.Prompt,
|
||||
ReferenceImagesBase64 = input.ReferenceImagesBase64 ?? new List<string>(),
|
||||
ReferenceImagesUrl = new List<string>(),
|
||||
TaskStatus = TaskStatusEnum.Processing,
|
||||
UserId = userId
|
||||
};
|
||||
|
||||
await _imageTaskRepository.InsertAsync(task);
|
||||
var taskId = task.Id;
|
||||
|
||||
// 构建请求JSON
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
prompt = input.Prompt,
|
||||
referenceImages = input.ReferenceImagesBase64
|
||||
});
|
||||
|
||||
// 入队后台任务
|
||||
await _backgroundJobManager.EnqueueAsync(new ImageGenerationJobArgs
|
||||
{
|
||||
TaskId = taskId,
|
||||
ModelId = input.ModelId,
|
||||
RequestJson = requestJson,
|
||||
UserId = userId
|
||||
});
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询任务状态
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务ID</param>
|
||||
/// <returns>任务详情</returns>
|
||||
[HttpGet("ai-image/task/{taskId}")]
|
||||
public async Task<ImageTaskOutput> GetTaskAsync([FromRoute] Guid taskId)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
|
||||
var task = await _imageTaskRepository.GetFirstAsync(x => x.Id == taskId && x.UserId == userId);
|
||||
if (task == null)
|
||||
{
|
||||
throw new UserFriendlyException("任务不存在或无权访问");
|
||||
}
|
||||
|
||||
return new ImageTaskOutput
|
||||
{
|
||||
Id = task.Id,
|
||||
Prompt = task.Prompt,
|
||||
ReferenceImagesBase64 = task.ReferenceImagesBase64,
|
||||
ReferenceImagesUrl = task.ReferenceImagesUrl,
|
||||
StoreBase64 = task.StoreBase64,
|
||||
StoreUrl = task.StoreUrl,
|
||||
TaskStatus = task.TaskStatus,
|
||||
CreationTime = task.CreationTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传Base64图片转换为URL
|
||||
/// </summary>
|
||||
/// <param name="base64Data">Base64图片数据(包含前缀如 data:image/png;base64,)</param>
|
||||
/// <returns>图片访问URL</returns>
|
||||
[HttpPost("ai-image/upload-base64")]
|
||||
public async Task<string> UploadBase64ToUrlAsync([FromBody] string base64Data)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(base64Data))
|
||||
{
|
||||
throw new UserFriendlyException("Base64数据不能为空");
|
||||
}
|
||||
|
||||
// 解析Base64数据
|
||||
string mimeType = "image/png";
|
||||
string base64Content = base64Data;
|
||||
|
||||
if (base64Data.Contains(","))
|
||||
{
|
||||
var parts = base64Data.Split(',');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
// 提取MIME类型
|
||||
var header = parts[0];
|
||||
if (header.Contains(":") && header.Contains(";"))
|
||||
{
|
||||
mimeType = header.Split(':')[1].Split(';')[0];
|
||||
}
|
||||
base64Content = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文件扩展名
|
||||
var extension = mimeType switch
|
||||
{
|
||||
"image/png" => ".png",
|
||||
"image/jpeg" => ".jpg",
|
||||
"image/jpg" => ".jpg",
|
||||
"image/gif" => ".gif",
|
||||
"image/webp" => ".webp",
|
||||
_ => ".png"
|
||||
};
|
||||
|
||||
// 解码Base64
|
||||
byte[] imageBytes;
|
||||
try
|
||||
{
|
||||
imageBytes = Convert.FromBase64String(base64Content);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
throw new UserFriendlyException("Base64格式无效");
|
||||
}
|
||||
|
||||
// 创建存储目录
|
||||
var uploadPath = Path.Combine(_webHostEnvironment.ContentRootPath, "wwwroot", "ai-images");
|
||||
if (!Directory.Exists(uploadPath))
|
||||
{
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
}
|
||||
|
||||
// 生成文件名并保存
|
||||
var fileId = _guidGenerator.Create();
|
||||
var fileName = $"{fileId}{extension}";
|
||||
var filePath = Path.Combine(uploadPath, fileName);
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, imageBytes);
|
||||
|
||||
// 返回访问URL
|
||||
return $"/ai-images/{fileName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询任务列表
|
||||
/// </summary>
|
||||
/// <param name="input">分页查询参数</param>
|
||||
/// <returns>任务列表</returns>
|
||||
[HttpGet("ai-image/tasks")]
|
||||
public async Task<PagedResult<ImageTaskOutput>> GetTaskPageAsync([FromQuery] ImageTaskPageInput input)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
|
||||
var query = _imageTaskRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.WhereIF(input.TaskStatus.HasValue, x => x.TaskStatus == input.TaskStatus!.Value)
|
||||
.OrderByDescending(x => x.CreationTime);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query
|
||||
.Skip((input.PageIndex - 1) * input.PageSize)
|
||||
.Take(input.PageSize)
|
||||
.Select(x => new ImageTaskOutput
|
||||
{
|
||||
Id = x.Id,
|
||||
Prompt = x.Prompt,
|
||||
ReferenceImagesBase64 = x.ReferenceImagesBase64,
|
||||
ReferenceImagesUrl = x.ReferenceImagesUrl,
|
||||
StoreBase64 = x.StoreBase64,
|
||||
StoreUrl = x.StoreUrl,
|
||||
TaskStatus = x.TaskStatus,
|
||||
CreationTime = x.CreationTime
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return new PagedResult<ImageTaskOutput>(total, items);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页结果
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
public class PagedResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 总数
|
||||
/// </summary>
|
||||
public long Total { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据列表
|
||||
/// </summary>
|
||||
public List<T> Items { get; set; }
|
||||
|
||||
public PagedResult(long total, List<T> items)
|
||||
{
|
||||
Total = total;
|
||||
Items = items;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user