275 lines
8.8 KiB
C#
275 lines
8.8 KiB
C#
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后重新登录重试");
|
||
}
|
||
|
||
// 尊享包校验
|
||
// 注意: AiImageService目前没有注入AiModelEntity仓储
|
||
// 由于图片生成功能使用频率较低,且当前所有图片模型都不是尊享模型
|
||
// 暂时保留原逻辑,等待后续重构时再注入仓储
|
||
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;
|
||
}
|
||
}
|