Files

166 lines
7.9 KiB
C#

using MAX.Core.DependencyInjection;
using MAX.Framework.Commons;
using MAX.Framework.Dtos;
using Microsoft.Extensions.Logging;
using Store.Modules.CloudProduct.Entities;
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();
}
/// <summary>整批校验后统一写入日志,每个事件保留一条记录。</summary>
public async Task RecordLogsAsync(RecordViolationLogRequest[]? items)
{
if (items == null || items.Length is < 1 or > 1000)
throw new AppException("每次请提供1-1000条违规日志", ResultCode.ValidationError);
var logs = new GoodsViolationLogEntity[items.Length];
var now = DateTime.Now;
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
if (item == null || string.IsNullOrWhiteSpace(item.ShopId) || item.ShopId.Trim().Length > 64
|| item.GoodsId <= 0 || item.ReasonId <= 0
|| item.Action is not (ViolationLogAction.Report or ViolationLogAction.AutoListingExcluded or ViolationLogAction.AutoOffShelf))
throw new AppException($"第{i + 1}条日志须提供有效店铺编号、商品编号、违规类型和动作(1、2或3)", ResultCode.ValidationError);
logs[i] = new GoodsViolationLogEntity
{
ShopId = item.ShopId.Trim(), GoodsId = item.GoodsId, ReasonId = item.ReasonId,
Action = item.Action, CreatedTime = now
};
}
var reasonIds = logs.Select(x => (uint)x.ReasonId).Distinct().ToArray();
if (await repository.CountReasonsAsync(reasonIds) != reasonIds.Length)
throw new AppException("违规类型不存在,请从违规字典中选择", ResultCode.ValidationError);
await repository.InsertLogsAsync(logs);
}
/// <summary>分页读取违规事件,beforeId 为上一页最后一条日志编号。</summary>
public Task<List<GoodsViolationLogEntity>> GetLogsAsync(string[]? shopIds, long? goodsId, long beforeId, int size)
{
if (shopIds == null || shopIds.Length is < 1 or > 1000
|| shopIds.Any(id => string.IsNullOrWhiteSpace(id) || id.Trim().Length > 64)
|| goodsId <= 0 || beforeId < 0 || size is < 1 or > 200)
throw new AppException("请提供1-1000个有效商铺编号、有效的商品及分页参数,每页允许1-200条", ResultCode.ValidationError);
return repository.GetLogsAsync(shopIds.Select(id => id.Trim()).Distinct().ToArray(), goodsId, beforeId, size);
}
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 reasons = await repository.GetReasonIdsByGoodsAsync(distinct);
return distinct.Select(id => new GoodsViolationStatus
{
GoodsId = id, IsViolation = reasons.Contains(id), ReasonIds = reasons[id].ToArray()
}).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];
}
}
}