using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NCC.Common.Configuration;
using NCC.Dependency;
using NCC.Extend.Entitys.Enum;
using NCC.Extend.Entitys.lq_upload_record;
using SqlSugar;
using OSS = AlibabaCloud.OSS.V2;
namespace NCC.Extend
{
///
/// 图片审核后台任务(Hangfire Job)
///
public class ImageAuditJob : ITransient
{
private readonly ISqlSugarClient _db;
private readonly IConfiguration _configuration;
private readonly ImageModerationService _moderationService;
private readonly ILogger _logger;
///
/// 初始化图片审核任务
///
public ImageAuditJob(
ISqlSugarClient db,
IConfiguration configuration,
ImageModerationService moderationService,
ILogger logger)
{
_db = db;
_configuration = configuration;
_moderationService = moderationService;
_logger = logger;
}
///
/// 执行单条记录的图片审核
///
/// 上传记录 ID
public async Task ExecuteAsync(string uploadRecordId)
{
var entity = await _db.Queryable()
.Where(x => x.Id == uploadRecordId)
.FirstAsync();
if (entity == null || entity.AuditStatus != (int)ImageAuditStatusEnum.Pending)
return;
try
{
var imageBytes = await DownloadFromOssAsync(entity.ObjectKey);
if (imageBytes == null || imageBytes.Length == 0)
{
entity.AuditStatus = (int)ImageAuditStatusEnum.Failed;
entity.AuditReason = "从 OSS 下载文件失败或文件为空";
entity.AuditRawResponse = null;
entity.AuditTime = DateTime.Now;
await _db.Updateable(entity)
.UpdateColumns(e => new { e.AuditStatus, e.AuditReason, e.AuditRawResponse, e.AuditTime })
.ExecuteCommandAsync();
return;
}
var result = await _moderationService.ScanImageAsync(imageBytes, backgroundQueue: true);
var rawSnap = TruncateRawResponse(result?.RawResponse);
if (result.Passed)
{
entity.AuditStatus = (int)ImageAuditStatusEnum.Pass;
entity.AuditReason = result.Message;
entity.AuditRawResponse = rawSnap;
entity.AuditTime = DateTime.Now;
await _db.Updateable(entity)
.UpdateColumns(e => new { e.AuditStatus, e.AuditReason, e.AuditRawResponse, e.AuditTime })
.ExecuteCommandAsync();
}
else
{
entity.AuditStatus = (int)ImageAuditStatusEnum.Reject;
entity.AuditReason = result.Message;
entity.AuditRawResponse = rawSnap;
entity.AuditTime = DateTime.Now;
await SaveIllegalImageLocallyAsync(entity, imageBytes);
if (!string.IsNullOrEmpty(entity.IllegalLocalPath)
&& !entity.IllegalLocalPath.StartsWith("保存失败", StringComparison.Ordinal))
{
var illegalApiPath = $"/api/Extend/OssDirectUpload/IllegalImage/{entity.Id}";
var pubBase = (_configuration["NCC_App:LocalFileBaseUrl"] ?? _configuration["NCC_App:Domain"] ?? "")
.Trim().TrimEnd('/');
entity.AccessUrl = string.IsNullOrEmpty(pubBase)
? illegalApiPath
: $"{pubBase}{illegalApiPath}";
entity.ThumbnailPublicUrl = entity.AccessUrl;
}
await _db.Updateable(entity)
.UpdateColumns(e => new
{
e.AuditStatus,
e.AuditReason,
e.AuditRawResponse,
e.AuditTime,
e.IllegalLocalPath,
e.AccessUrl,
e.ThumbnailPublicUrl
})
.ExecuteCommandAsync();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "图片审核任务异常,uploadRecordId={Id}", uploadRecordId);
entity.AuditStatus = (int)ImageAuditStatusEnum.Failed;
entity.AuditReason = $"审核异常:{ex.Message}";
entity.AuditRawResponse = TruncateRawResponse(ex.ToString());
entity.AuditTime = DateTime.Now;
await _db.Updateable(entity)
.UpdateColumns(e => new { e.AuditStatus, e.AuditReason, e.AuditRawResponse, e.AuditTime })
.ExecuteCommandAsync();
}
}
///
/// 定时扫描待审核的图片(兜底任务)
///
public async Task ScanPendingAsync()
{
var threshold = DateTime.Now.AddMinutes(-2);
var pendingRecords = await _db.Queryable()
.Where(x => x.AuditStatus == (int)ImageAuditStatusEnum.Pending && x.CreateTime < threshold)
.Take(50)
.ToListAsync();
foreach (var record in pendingRecords)
{
try
{
await ExecuteAsync(record.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "兜底审核任务异常,uploadRecordId={Id}", record.Id);
}
}
}
///
/// 从 OSS 下载文件到内存
///
private async Task DownloadFromOssAsync(string objectKey)
{
var accessKeyId = _configuration["NCC_App:AliyunOSS:AccessKeyId"];
var accessKeySecret = _configuration["NCC_App:AliyunOSS:AccessKeySecret"];
var endpoint = _configuration["NCC_App:AliyunOSS:Endpoint"];
var region = _configuration["NCC_App:AliyunOSS:Region"] ?? "cn-chengdu";
var bucketName = _configuration["NCC_App:BucketName"];
if (!endpoint.StartsWith("http", StringComparison.OrdinalIgnoreCase))
endpoint = "https://" + endpoint;
var ossCfg = OSS.Configuration.LoadDefault();
ossCfg.CredentialsProvider = new OSS.Credentials.StaticCredentialsProvider(accessKeyId, accessKeySecret);
ossCfg.Region = region;
ossCfg.Endpoint = endpoint;
using (var ossClient = new OSS.Client(ossCfg))
{
var response = await ossClient.GetObjectAsync(new OSS.Models.GetObjectRequest
{
Bucket = bucketName,
Key = objectKey
});
using (var ms = new MemoryStream())
{
await response.Body.CopyToAsync(ms);
return ms.ToArray();
}
}
}
///
/// 将违规图片保存到本地
///
private async Task SaveIllegalImageLocallyAsync(LqUploadRecordEntity entity, byte[] imageBytes)
{
try
{
var dirRoot = FileVariable.IllegalImageFilePath;
var now = DateTime.Now;
var dirPath = Path.Combine(dirRoot, now.ToString("yyyy"), now.ToString("MM"), now.ToString("dd"));
if (!Directory.Exists(dirPath))
Directory.CreateDirectory(dirPath);
var fileName = Path.GetFileName(entity.ObjectKey);
var filePath = Path.Combine(dirPath, fileName);
await File.WriteAllBytesAsync(filePath, imageBytes);
entity.IllegalLocalPath = Path.GetFullPath(filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "保存违规图片到本地失败,uploadRecordId={Id}", entity.Id);
entity.IllegalLocalPath = $"保存失败:{ex.Message}";
}
}
///
/// 限制原始响应长度,避免超过 MySQL TEXT 与 ORM 映射问题
///
private static string TruncateRawResponse(string raw)
{
if (string.IsNullOrEmpty(raw)) return null;
const int maxLen = 60000;
return raw.Length <= maxLen ? raw : raw.Substring(0, maxLen);
}
}
}