Files
StoreWeb/Modules/CloudProduct/Services/GoodsViolationService.cs
T

125 lines
5.6 KiB
C#

using MAX.Core.DependencyInjection;
using MAX.Framework.Commons;
using MAX.Framework.Dtos;
using Microsoft.Extensions.Logging;
using Store.Modules.CloudProduct.Dtos;
using Store.Modules.CloudProduct.Repositorys;
namespace Store.Modules.CloudProduct.Services;
public class GoodsViolationService(
GoodsViolationRepository repository,
GoodsIndexLock indexLock,
IOpenSearchService search,
ILogger<GoodsViolationService> logger) : IScopedDependency
{
public Task<List<ViolationReasonDto>> GetReasonsAsync() => repository.GetReasonsAsync();
public static Dictionary<long, string[]> ValidateAndGroup(ReportGoodsViolationItem[]? items)
{
if (items == null || items.Length is < 1 or > 1000)
throw new AppException("每次请提供1-1000条违规记录", ResultCode.ValidationError);
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
if (item == null || item.GoodsId <= 0 || string.IsNullOrWhiteSpace(item.Reasons)
|| item.Reasons.Trim().Length > 100)
throw new AppException($"第{i + 1}条记录须提供有效商品编号和一个违规原因,原因去除首尾空白后须为1-100个字符", ResultCode.ValidationError);
}
return items.GroupBy(x => x.GoodsId).ToDictionary(x => x.Key,
x => x.Select(y => y.Reasons.Trim()).Distinct(StringComparer.Ordinal).ToArray());
}
public async Task<List<ReportGoodsViolationsResult>> ReportAsync(ReportGoodsViolationItem[]? items)
{
var grouped = ValidateAndGroup(items);
await using var lease = await indexLock.AcquireAsync();
var pendingIds = await repository.MergeAsync(grouped);
if (pendingIds.Length > 0)
{
try
{
await search.DeleteDocumentsAsync(pendingIds);
await repository.MarkDeletedAsync(pendingIds);
}
catch (Exception ex)
{
logger.LogError(ex, "违规记录已保存,{Count}个商品等待重试下架", pendingIds.Length);
}
}
// 返回商品当前持久化状态,包括本次原因被忽略但历史已下架的商品。
var offShelfIds = (await repository.GetOffShelfGoodsIdsAsync(grouped.Keys.ToArray())).ToHashSet();
return grouped.Keys.Select(id => new ReportGoodsViolationsResult
{
GoodsId = id,
IsOffShelf = offShelfIds.Contains(id)
}).ToList();
}
public async Task<List<GoodsViolationStatus>> GetStatusAsync(long[]? ids)
{
if (ids == null || ids.Length is < 1 or > 1000 || ids.Any(x => x <= 0))
throw new AppException("请提供1-1000个正数商品编号", ResultCode.ValidationError);
var distinct = ids.Distinct().ToArray();
var violations = (await repository.GetViolationIdsAsync(distinct)).ToHashSet();
return distinct.Select(x => new GoodsViolationStatus { GoodsId = x, IsViolation = violations.Contains(x) }).ToList();
}
public async Task<RemoveGoodsViolationsResult> RemoveAsync(long[]? ids)
{
if (ids == null || ids.Length is < 1 or > 1000 || ids.Any(x => x <= 0))
throw new AppException("请提供1-1000个正数商品编号", ResultCode.ValidationError);
var distinct = ids.Distinct().ToArray();
var result = new RemoveGoodsViolationsResult();
await using (var lease = await indexLock.AcquireAsync())
result.RemovedCount = await repository.RemoveAndScheduleRestoreAsync(distinct);
// 索引服务自行持锁并再次检查违规,避免与新的上报交错时重新上架违规商品。
var goods = await repository.GetRestorableGoodsAsync(distinct);
var candidates = goods.Select(x => x.GoodsId).Distinct().ToArray();
result.SkippedGoodsIds = distinct.Except(candidates).ToArray();
if (candidates.Length == 0) return result;
try
{
var restored = await search.RebuildIndexAsync(goods);
if (restored.Length > 0)
{
await using var lease = await indexLock.AcquireAsync();
var blocked = await repository.GetViolationIdsAsync(restored);
restored = restored.Except(blocked).ToArray();
if (restored.Length > 0) await repository.MarkRestoredAsync(restored);
}
result.RestoredGoodsIds = restored;
result.SkippedGoodsIds = distinct.Except(restored).ToArray();
}
catch (Exception ex)
{
logger.LogError(ex, "已解除违规,{Count}个商品等待后台任务恢复索引", candidates.Length);
result.PendingRestoreGoodsIds = candidates;
}
return result;
}
// 暂不注册 HTTP 路由。
public Task<List<long>> GetGoodsIdsByReasonAsync(uint reasonId, long afterGoodsId = 0, int pageSize = 100)
{
if (reasonId == 0 || afterGoodsId < 0 || pageSize is < 1 or > 1000)
throw new AppException("原因ID或分页参数无效", ResultCode.ValidationError);
return repository.GetByReasonAsync(reasonId, afterGoodsId, pageSize);
}
public async Task DeleteViolationIndexesAsync(bool pendingOnly = true)
{
long afterId = 0;
while (true)
{
await using var lease = await indexLock.AcquireAsync();
var ids = await repository.GetBatchAsync(afterId, 1000, pendingOnly);
if (ids.Count == 0) return;
await search.DeleteDocumentsAsync(ids);
await repository.MarkDeletedAsync(ids.ToArray());
afterId = ids[^1];
}
}
}