114 lines
4.6 KiB
C#
114 lines
4.6 KiB
C#
using MAX.Core.DependencyInjection;
|
|
using MAX.Framework.Commons;
|
|
using MAX.Framework.Security;
|
|
using MAX.Framework.Validation;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Store.Modules.CloudProduct.Contracts;
|
|
using Store.Modules.CloudProduct.Dtos;
|
|
using Store.Modules.CloudProduct.Entities;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.Json;
|
|
using System.Text.Unicode;
|
|
|
|
namespace Store.Modules.CloudProduct.Services;
|
|
|
|
/// <summary>商品检测业务入口,负责上传图片识别、MD5 缓存和当前用户的商品安全记录。</summary>
|
|
public sealed class ProductInspectionService(IInspectionIntegration integration, ICurrentUser user, ImageAnalysisQuotaService imageAnalysisQuotaService,
|
|
[FromKeyedServices("CloudProductConnection")] IFreeSql database,
|
|
[FromKeyedServices("ProductInspectionAnalysisGate")] SemaphoreSlim gate,
|
|
ILogger<ProductInspectionService> logger
|
|
) : IScopedDependency
|
|
{
|
|
public const int MaxBytes = 10 * 1024 * 1024;
|
|
|
|
// 缓存 JSON 保留中文原文,字符串中的引号和控制字符仍由序列化器转义。
|
|
private static readonly JsonSerializerOptions CacheJsonOptions = new()
|
|
{
|
|
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
|
|
};
|
|
|
|
// 每三个图片字节编码为四个 Base64 字符,末组向上取整。
|
|
public const int MaxBase64Length = (MaxBytes + 2) / 3 * 4;
|
|
|
|
public async Task<string[]> InspectAsync(InspectImageRequest request, CancellationToken cancellationToken)
|
|
{
|
|
RequestValidator.Validate(request);
|
|
if (request.Md5.Length != 32 || !request.Md5.All(char.IsAsciiHexDigit))
|
|
throw new ValidationException("md5 必须是 32 位十六进制缓存键。");
|
|
var md5 = request.Md5.ToLowerInvariant();
|
|
if (request.Image.Length > MaxBase64Length)
|
|
throw new ValidationException("图片超过 10 MiB 或 Base64 文本过长。");
|
|
|
|
byte[] image;
|
|
try { image = Convert.FromBase64String(request.Image); }
|
|
catch (FormatException) { throw new ValidationException("image 必须是有效的 Base64 字符串。"); }
|
|
if (image.Length is < 12 or > MaxBytes)
|
|
throw new ValidationException("图片为空、格式无效或超过 10 MiB。");
|
|
// 依据文件签名识别允许的图片类型,避免信任客户端文件名和 MIME 类型。
|
|
string mediaType;
|
|
if (image[0] == 0xff && image[1] == 0xd8 && image[2] == 0xff) mediaType = "image/jpeg";
|
|
else if (image.AsSpan(0, 8).SequenceEqual(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 })) mediaType = "image/png";
|
|
else if (image.AsSpan(0, 4).SequenceEqual("RIFF"u8) && image.AsSpan(8, 4).SequenceEqual("WEBP"u8)) mediaType = "image/webp";
|
|
else throw new ValidationException("请上传 JPEG、PNG 或 WebP 图片。");
|
|
var cached = await database.Select<ImageAnalysisCache>().Where(x => x.CacheKey == md5).FirstAsync();
|
|
var result = cached?.ReadFreshResult(DateTime.UtcNow);
|
|
if (result is null)
|
|
{
|
|
await gate.WaitAsync(cancellationToken);
|
|
|
|
try
|
|
{
|
|
var useImageQuota = await imageAnalysisQuotaService.GetUserImageAnalysisQuota(new UseImageAnalysisQuotaRequest
|
|
{
|
|
UserName = request.UserName
|
|
});
|
|
if (useImageQuota <= 0)
|
|
{
|
|
throw new AppException("AI检测可用数量为0");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "调用图片识别扣费程序失败");
|
|
throw new AppException("AI检测可用数量为0");
|
|
}
|
|
|
|
try
|
|
{
|
|
using var stream = new MemoryStream(image, writable: false);
|
|
var recognized = await integration.AnalyzeAsync(stream, mediaType, cancellationToken);
|
|
result = recognized.Distinct(StringComparer.Ordinal).ToArray();
|
|
var entry = cached ?? new ImageAnalysisCache { CacheKey = md5 };
|
|
var previousUpdate = entry.UpdatedAt;
|
|
entry.RecognitionResult = JsonSerializer.Serialize(result, CacheJsonOptions);
|
|
entry.RecordExpiration(result.Length == 0, DateTime.UtcNow);
|
|
// 同一缓存失效后可能同时触发多次识别,只有首个写入者推进级别。
|
|
if (cached is null)
|
|
await database.InsertOrUpdate<ImageAnalysisCache>().SetSource(entry).IfExistsDoNothing().ExecuteAffrowsAsync();
|
|
else
|
|
await database.Update<ImageAnalysisCache>().SetSource(entry)
|
|
.Where(x => x.UpdatedAt == previousUpdate).ExecuteAffrowsAsync();
|
|
}
|
|
finally { gate.Release(); }
|
|
}
|
|
try
|
|
{
|
|
var useRet = await imageAnalysisQuotaService.UseImageAnalysisQuotaAsync(new UseImageAnalysisQuotaRequest
|
|
{
|
|
UserName = request.UserName
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "调用图片识别扣费程序失败");
|
|
//扣费失败显示空
|
|
result = [];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
}
|