增加违规记录页面

This commit is contained in:
chfei committed 2026-11-02 17:17:44 +08:00
1 parent 25d5b5d61c
commit 6252901a94
8 files changed
+884 -18

No files matched your search

+175 -11
View File
@@ -164,14 +164,46 @@ namespace Work.Services
}
mappedItems.Add(new ReportGoodsViolationItem { GoodsId = goodsId, Reasons = item.Reasons });
}
var reports = mappedItems.GroupBy(item => item.GoodsId).Select(group => new ReportGoodsViolationItem
{
GoodsId = group.Key,
Reasons = string.Join(";", group.Select(item => item.Reasons).Distinct())
}).ToList();
var reports = mappedItems.GroupBy(item => new { item.GoodsId, item.Reasons }).Select(group => group.First()).ToList();
return reports.Count == 0
? new List<ReportGoodsViolationResult>() : await ReportViolations(reports);
var results = new List<ReportGoodsViolationResult>();
for (int offset = 0; offset < reports.Count && GlobalConfig.GlobalSettings.AutoReportViolationProducts; offset += 1000)
{
var batch = reports.Skip(offset).Take(1000).ToList();
results.AddRange(await ReportViolations(batch));
await RecordReportLogs(shop.ShopId, batch);
}
return results;
}
private static async Task RecordReportLogs(string shopId, List<ReportGoodsViolationItem> batch)
{
if (!GlobalConfig.GlobalSettings.AutoReportViolationProducts) return;
Dictionary<string, int> reasonIds;
try
{
// 上报成功后读取字典,才能取得本批新增违规原因的编号。
reasonIds = (await GetViolationReasons()).ToDictionary(reason => reason.Name, reason => reason.Id);
}
catch (Exception ex)
{
LogSave.AddFile($"店铺 {shopId} 已上报违规商品,读取日志原因字典失败:{ex.Message}");
return;
}
var logs = new List<RecordViolationLogRequest>(batch.Count);
foreach (var item in batch)
{
if (!reasonIds.TryGetValue(item.Reasons, out var reasonId))
{
LogSave.AddFile($"店铺 {shopId} 商品 {item.GoodsId} 找不到违规原因编号:{item.Reasons}");
continue;
}
logs.Add(new RecordViolationLogRequest
{
ShopId = shopId, GoodsId = item.GoodsId, ReasonId = reasonId, Action = ViolationLogAction.Report
});
}
await TryRecordLogs(logs);
}
private static bool TryGetGoodsId(string code, out long goodsId)
@@ -231,19 +263,24 @@ namespace Work.Services
{
var batch = goodsIds.Skip(offset).Take(50).ToList();
var statuses = GetViolationStatus(batch);
var violations = new HashSet<long>(statuses.Where(item => item != null && item.IsViolation && batch.Contains(item.GoodsId))
.Select(item => item.GoodsId));
var productIds = mappings.Where(item => violations.Contains(item.Value)).Select(item => item.Key).ToList();
var violations = statuses.Where(item => item != null && item.IsViolation && batch.Contains(item.GoodsId))
.ToDictionary(item => item.GoodsId);
var productIds = mappings.Where(item => violations.ContainsKey(item.Value)).Select(item => item.Key).ToList();
// 同一云商品可能对应多个微信商品,每次下架最多 50 个。
for (var index = 0; index < productIds.Count; index += 50)
{
var ids = string.Join(",", productIds.Skip(index).Take(50));
if (!GlobalConfig.GlobalSettings.AutoDelistViolationProducts)
return;
var page = productIds.Skip(index).Take(50).ToArray();
var ids = string.Join(",", page);
var response = JObject.Parse(WxProductApi.DownProd(shop, ids));
if (response["code"]?.ToString() != "0")
{
throw new Exception($"微信商品 {ids} 下架失败:{response}");
}
LogSave.AddFile($"店铺 {shop.ShopId} 云端检测违规,已下架微信商品:{ids}");
var downed = page.Select(id => mappings[id]).Distinct().Select(id => violations[id]);
await RecordShopActionLogs(shop.ShopId, downed, ViolationLogAction.AutoOffShelf);
}
}
}
@@ -337,6 +374,97 @@ namespace Work.Services
return ret.Data ?? throw new Exception("批量查询商品违规状态返回数据为空。");
}
/// <summary>逐原因记录店铺动作。日志请求失败不能改变剔除决定或中断后续下架。</summary>
public static Task RecordShopActionLogs(string shopId, GoodsViolationStatus status, ViolationLogAction action)
=> RecordShopActionLogs(shopId, new[] { status }, action);
private static async Task RecordShopActionLogs(string shopId, IEnumerable<GoodsViolationStatus> statuses, ViolationLogAction action)
{
if (!(action == ViolationLogAction.AutoListingExcluded && GlobalConfig.GlobalSettings.AutoExcludeViolationProductsOnUpload
|| action == ViolationLogAction.AutoOffShelf && GlobalConfig.GlobalSettings.AutoDelistViolationProducts))
return;
var logs = new List<RecordViolationLogRequest>();
foreach (var status in statuses)
{
if (status.ReasonIds == null || status.ReasonIds.Length == 0)
{
LogSave.AddFile($"店铺 {shopId} 商品 {status.GoodsId} 缺少违规原因编号,无法上传动作 {action} 日志。");
continue;
}
logs.AddRange(status.ReasonIds.Select(id => new RecordViolationLogRequest
{
ShopId = shopId, GoodsId = status.GoodsId, ReasonId = id, Action = action
}));
}
await TryRecordLogs(logs).ConfigureAwait(false);
}
private static async Task TryRecordLogs(List<RecordViolationLogRequest> logs)
{
if (logs.Count == 0) return;
try
{
await RecordViolationLogs(logs).ConfigureAwait(false);
}
catch (Exception ex)
{
LogSave.AddFile($"店铺 {logs[0].ShopId} 动作 {logs[0].Action} 的 {logs.Count} 条日志上传失败:{ex.Message}");
}
}
/// <summary>记录店铺已完成的违规上报、上货剔除或下架动作。</summary>
public static Task RecordViolationLog(RecordViolationLogRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
return RecordViolationLogs(new[] { request });
}
public static async Task RecordViolationLogs(IReadOnlyList<RecordViolationLogRequest> requests)
{
if (requests == null || requests.Count == 0 || requests.Any(item => item == null))
throw new ArgumentException("请提供有效的违规日志。", nameof(requests));
var html = await GetHtml.PostJsonAsync(ServerUrl + "/api/CloudGoods/RecordViolationLog", requests, timeOut: 5);
var ret = JsonConvert.DeserializeObject<Result>(html) ?? throw new Exception("记录违规日志返回为空。");
if (ret.Code != 200)
throw new Exception(ret.Msg);
}
/// <summary>读取日志筛选和显示使用的违规原因字典。</summary>
public static async Task<List<ViolationReason>> GetViolationReasons()
{
using (var web = new System.Net.WebClient { Encoding = Encoding.UTF8 })
{
var html = await web.DownloadStringTaskAsync(ServerUrl + "/api/CloudGoods/GetViolationReasons");
var ret = JsonConvert.DeserializeObject<Result<List<ViolationReason>>>(html);
if (ret?.Code != 200 || ret.Data == null)
throw new Exception(ret?.Msg ?? "查询违规类型返回为空。");
return ret.Data;
}
}
public class ViolationReason
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsViolation { get; set; }
}
/// <summary>按日志编号倒序查询,下一页传入上一页最后一条日志的 Id。</summary>
public static async Task<List<GoodsViolationLogEntity>> GetViolationLogs(string[] shopIds, long? goodsId = null,
long beforeId = 0, int size = 50)
{
if (shopIds == null || shopIds.Length == 0 || shopIds.Any(string.IsNullOrWhiteSpace))
throw new ArgumentException("请提供有效的店铺编号。", nameof(shopIds));
var request = new GetViolationLogsRequest { ShopIds = shopIds, GoodsId = goodsId, BeforeId = beforeId, Size = size };
var html = await GetHtml.PostJsonAsync(ServerUrl + "/api/CloudGoods/GetViolationLogs", request, timeOut: 5);
var ret = JsonConvert.DeserializeObject<Result<List<GoodsViolationLogEntity>>>(html)
?? throw new Exception("查询违规日志返回为空。");
if (ret.Code != 200)
throw new Exception(ret.Msg);
return ret.Data ?? throw new Exception("查询违规日志返回数据为空。");
}
#region user
/// <summary>
/// 搜索收藏
@@ -408,6 +536,39 @@ namespace Work.Services
return string.IsNullOrEmpty(BaseClass.ParentUserName) ? BaseClass.UserName : BaseClass.ParentUserName;
}
}
public enum ViolationLogAction
{
Report = 1,
AutoListingExcluded = 2,
AutoOffShelf = 3
}
public sealed class RecordViolationLogRequest
{
public string ShopId { get; set; } = string.Empty;
public long GoodsId { get; set; }
public int ReasonId { get; set; }
public ViolationLogAction Action { get; set; }
}
public sealed class GetViolationLogsRequest
{
public string[] ShopIds { get; set; } = Array.Empty<string>();
public long? GoodsId { get; set; }
public long BeforeId { get; set; }
public int Size { get; set; } = 50;
}
public class GoodsViolationLogEntity
{
public long Id { get; set; }
public string? ShopId { get; set; }
public long GoodsId { get; set; }
public int ReasonId { get; set; }
public ViolationLogAction Action { get; set; }
public DateTime CreatedTime { get; set; }
}
/// <summary>
/// 违规商品上报项。
/// </summary>
@@ -437,6 +598,9 @@ namespace Work.Services
/// </summary>
public class GoodsViolationStatus
{
[JsonProperty("reasonIds")]
public int[] ReasonIds { get; set; }
[JsonProperty("goodsId")]
public long GoodsId { get; set; }