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 logger) : IScopedDependency { public Task> GetReasonsAsync() => repository.GetReasonsAsync(); public static Dictionary 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> 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> 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 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> 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]; } } }