395 lines
13 KiB
C#
395 lines
13 KiB
C#
using System.Text.Json;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Hosting;
|
||
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using SqlSugar;
|
||
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;
|
||
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.Entities.Model;
|
||
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 ModelManager _modelManager;
|
||
private readonly IGuidGenerator _guidGenerator;
|
||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||
private readonly TokenManager _tokenManager;
|
||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||
|
||
public AiImageService(
|
||
ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageTaskRepository,
|
||
IBackgroundJobManager backgroundJobManager,
|
||
AiBlacklistManager aiBlacklistManager,
|
||
PremiumPackageManager premiumPackageManager,
|
||
ModelManager modelManager,
|
||
IGuidGenerator guidGenerator,
|
||
IWebHostEnvironment webHostEnvironment, TokenManager tokenManager,
|
||
ISqlSugarRepository<AiModelEntity> aiModelRepository)
|
||
{
|
||
_imageTaskRepository = imageTaskRepository;
|
||
_backgroundJobManager = backgroundJobManager;
|
||
_aiBlacklistManager = aiBlacklistManager;
|
||
_premiumPackageManager = premiumPackageManager;
|
||
_modelManager = modelManager;
|
||
_guidGenerator = guidGenerator;
|
||
_webHostEnvironment = webHostEnvironment;
|
||
_tokenManager = tokenManager;
|
||
_aiModelRepository = aiModelRepository;
|
||
}
|
||
|
||
/// <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);
|
||
|
||
//校验token
|
||
if (input.TokenId is not null)
|
||
{
|
||
await _tokenManager.ValidateTokenAsync(input.TokenId, input.ModelId);
|
||
}
|
||
|
||
|
||
// VIP校验
|
||
if (!CurrentUser.IsAiVip())
|
||
{
|
||
throw new UserFriendlyException("图片生成功能需要VIP用户才能使用,请购买VIP后重新登录重试");
|
||
}
|
||
|
||
// 尊享包校验 - 使用ModelManager统一判断
|
||
var isPremium = await _modelManager.IsPremiumModelAsync(input.ModelId);
|
||
if (isPremium)
|
||
{
|
||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
|
||
if (availableTokens <= 0)
|
||
{
|
||
throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包");
|
||
}
|
||
}
|
||
|
||
// 创建任务实体
|
||
var task = new ImageStoreTaskAggregateRoot
|
||
{
|
||
Prompt = input.Prompt,
|
||
ReferenceImagesPrefixBase64 = input.ReferenceImagesPrefixBase64 ?? new List<string>(),
|
||
ReferenceImagesUrl = new List<string>(),
|
||
TaskStatus = TaskStatusEnum.Processing,
|
||
UserId = userId,
|
||
UserName = CurrentUser.UserName,
|
||
TokenId = input.TokenId,
|
||
ModelId = input.ModelId
|
||
};
|
||
|
||
await _imageTaskRepository.InsertAsync(task);
|
||
// 入队后台任务
|
||
await _backgroundJobManager.EnqueueAsync(new ImageGenerationJobArgs
|
||
{
|
||
TaskId = task.Id,
|
||
});
|
||
|
||
return task.Id;
|
||
}
|
||
|
||
/// <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,
|
||
PublishStatus = task.PublishStatus,
|
||
Categories = task.Categories,
|
||
CreationTime = task.CreationTime,
|
||
ErrorInfo = task.ErrorInfo,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 上传Base64图片转换为URL
|
||
/// </summary>
|
||
/// <param name="base64Data">Base64图片数据(包含前缀如 data:image/png;base64,)</param>
|
||
/// <returns>图片访问URL</returns>
|
||
[HttpPost("ai-image/upload-base64")]
|
||
[AllowAnonymous]
|
||
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格式无效");
|
||
}
|
||
|
||
// ==============================
|
||
// ✅ 按日期创建目录(yyyyMMdd)
|
||
// ==============================
|
||
var dateFolder = DateTime.Now.ToString("yyyyMMdd");
|
||
var uploadPath = Path.Combine(
|
||
_webHostEnvironment.ContentRootPath,
|
||
"wwwroot",
|
||
"ai-images",
|
||
dateFolder
|
||
);
|
||
|
||
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 $"/wwwroot/ai-images/{dateFolder}/{fileName}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询我的任务列表
|
||
/// </summary>
|
||
[HttpGet("ai-image/my-tasks")]
|
||
public async Task<PagedResult<ImageTaskOutput>> GetMyTaskPageAsync([FromQuery] ImageMyTaskPageInput input)
|
||
{
|
||
var userId = CurrentUser.GetId();
|
||
|
||
RefAsync<int> total = 0;
|
||
var output = await _imageTaskRepository._DbQueryable
|
||
.Where(x => x.UserId == userId)
|
||
.WhereIF(input.TaskStatus is not null, x => x.TaskStatus == input.TaskStatus)
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.Prompt), x => x.Prompt.Contains(input.Prompt))
|
||
.WhereIF(input.PublishStatus is not null, x => x.PublishStatus == input.PublishStatus)
|
||
.WhereIF(input.StartTime is not null && input.EndTime is not null,
|
||
x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime)
|
||
.OrderByDescending(x => x.CreationTime)
|
||
.Select(x => new ImageTaskOutput
|
||
{
|
||
Id = x.Id,
|
||
Prompt = x.Prompt,
|
||
StoreUrl = x.StoreUrl,
|
||
TaskStatus = x.TaskStatus,
|
||
PublishStatus = x.PublishStatus,
|
||
Categories = x.Categories,
|
||
CreationTime = x.CreationTime,
|
||
ErrorInfo = x.ErrorInfo
|
||
})
|
||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
|
||
|
||
|
||
return new PagedResult<ImageTaskOutput>(total, output);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询图片广场(已发布的图片)
|
||
/// </summary>
|
||
[HttpGet("ai-image/plaza")]
|
||
[AllowAnonymous]
|
||
public async Task<PagedResult<ImageTaskOutput>> GetPlazaPageAsync([FromQuery] ImagePlazaPageInput input)
|
||
{
|
||
RefAsync<int> total = 0;
|
||
var output = await _imageTaskRepository._DbQueryable
|
||
.Where(x => x.PublishStatus == PublishStatusEnum.Published)
|
||
.Where(x => x.TaskStatus == TaskStatusEnum.Success)
|
||
.WhereIF(input.TaskStatus is not null, x => x.TaskStatus == input.TaskStatus)
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.Prompt), x => x.Prompt.Contains(input.Prompt))
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.Categories), x => SqlFunc.JsonLike(x.Categories, input.Categories))
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.UserName),x=>x.UserName.Contains(input.UserName) )
|
||
.WhereIF(input.StartTime is not null && input.EndTime is not null,
|
||
x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime)
|
||
.OrderByDescending(x => x.CreationTime)
|
||
.Select(x => new ImageTaskOutput
|
||
{
|
||
Id = x.Id,
|
||
Prompt = x.Prompt,
|
||
IsAnonymous = x.IsAnonymous,
|
||
StoreUrl = x.StoreUrl,
|
||
TaskStatus = x.TaskStatus,
|
||
PublishStatus = x.PublishStatus,
|
||
Categories = x.Categories,
|
||
CreationTime = x.CreationTime,
|
||
ErrorInfo = null,
|
||
UserName = x.UserName,
|
||
UserId = x.UserId,
|
||
|
||
})
|
||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); ;
|
||
|
||
|
||
output.ForEach(x =>
|
||
{
|
||
if (x.IsAnonymous)
|
||
{
|
||
x.UserName = null;
|
||
x.UserId = null;
|
||
}
|
||
});
|
||
|
||
return new PagedResult<ImageTaskOutput>(total, output);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发布图片到广场
|
||
/// </summary>
|
||
[HttpPost("ai-image/publish")]
|
||
public async Task PublishAsync([FromBody] PublishImageInput input)
|
||
{
|
||
var userId = CurrentUser.GetId();
|
||
|
||
var task = await _imageTaskRepository.GetFirstAsync(x => x.Id == input.TaskId && x.UserId == userId);
|
||
if (task == null)
|
||
{
|
||
throw new UserFriendlyException("任务不存在或无权访问");
|
||
}
|
||
|
||
if (task.TaskStatus != TaskStatusEnum.Success)
|
||
{
|
||
throw new UserFriendlyException("只有已完成的任务才能发布");
|
||
}
|
||
|
||
if (task.PublishStatus == PublishStatusEnum.Published)
|
||
{
|
||
throw new UserFriendlyException("该任务已发布");
|
||
}
|
||
|
||
//设置发布
|
||
task.SetPublish(input.IsAnonymous,input.Categories);
|
||
await _imageTaskRepository.UpdateAsync(task);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取图片模型列表
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
[HttpPost("ai-image/model")]
|
||
[AllowAnonymous]
|
||
public async Task<List<ModelGetListOutput>> GetModelAsync()
|
||
{
|
||
var output = await _aiModelRepository._DbQueryable
|
||
.Where(x => x.ModelType == ModelTypeEnum.Image)
|
||
.Where(x => x.ModelApiType == ModelApiTypeEnum.GenerateContent)
|
||
.OrderByDescending(x => x.OrderNum)
|
||
.Select(x => new ModelGetListOutput
|
||
{
|
||
Id = x.Id,
|
||
ModelId = x.ModelId,
|
||
ModelName = x.Name,
|
||
ModelDescribe = x.Description,
|
||
Remark = x.Description,
|
||
IsPremiumPackage = x.IsPremium
|
||
}).ToListAsync();
|
||
return output;
|
||
}
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
} |