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); } } }