using AllBase; using AllBase.Helper; using AllBase.Model.Result; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using Work.Store.Api; using System.Threading.Tasks; using Work.Api.Wx; using AllBase.Model; namespace Work.Services { /// /// 云商品数据服务 /// public class CloudGoodsService { public static List UserGroups { get; private set; } static CloudGoodsService() { GetGroups(BaseClass.UserName).ContinueWith(t => { if (t.IsFaulted) { return; } if (t.IsCanceled) { return; } UserGroups = t.Result; }); } public static async Task> GetGoods(GoodsSearchRequest request) { return await StoreApiClient.Instance.CloudGoods.GetGoods(request); } public static string GetGoodsDetail(long goodsId) { return StoreApiClient.Instance.CloudGoods.GetGoodsDetail(goodsId); } /// /// 在后台解析微信处罚 JSON、匹配店铺商品编码并上报,异常在内部记录。 /// public static void ReportViolationsFromJson(string json, ShopLoginInfo shop) { _ = Task.Run(async () => { try { await ReportViolationsFromJsonCore(json, shop).ConfigureAwait(false); } catch (Exception ex) { try { LogSave.AddFile("解析并上报违规商品失败:" + ex.Message); } catch { // 日志失败也不能影响调用方。 } } }); } private static async Task> ReportViolationsFromJsonCore(string json, ShopLoginInfo shop) { if (string.IsNullOrWhiteSpace(json)) { throw new ArgumentException("处罚 JSON 文本不能为空。", nameof(json)); } JObject root; try { root = JObject.Parse(json); } catch (JsonException ex) { throw new ArgumentException("处罚 JSON 格式不正确。", nameof(json), ex); } var record = root["record"] as JObject; if (root["code"]?.ToString() != "0" || record?["code"]?.ToString() != "0") { throw new ArgumentException("处罚数据的 code 或 record.code 不为 0,或缺少 record。", nameof(json)); } if (!(record["items"] is JArray records)) { throw new ArgumentException("处罚数据缺少 record.items 数组。", nameof(json)); } if (records.Count == 0) return new List(); var items = new List(); foreach (var token in records) { if (!(token is JObject punishment)) { throw new ArgumentException("record.items 中的处罚记录必须是对象。", nameof(json)); } var objects = punishment["punishObject"]; if (objects == null || objects.Type == JTokenType.Null) { continue; } if (!(objects is JArray products)) { throw new ArgumentException("punishObject 必须是数组。", nameof(json)); } foreach (var product in products) { if (!(product is JObject target)) { throw new ArgumentException("punishObject 中的处罚对象必须是对象。", nameof(json)); } var id = target["productId"]; // 订单等非商品处罚对象不包含 productId。 if (id == null) { continue; } if ((id.Type != JTokenType.String && id.Type != JTokenType.Integer) || !long.TryParse(id.ToString(), out var goodsId) || goodsId <= 0) { throw new ArgumentException("商品 productId 必须是有效的正整数编号。", nameof(json)); } var reason = punishment["punishReason"]; if (reason?.Type != JTokenType.String || string.IsNullOrWhiteSpace((string)reason)) { throw new ArgumentException($"商品 {goodsId} 缺少有效的 punishReason。", nameof(json)); } items.Add(new ReportGoodsViolationItem { GoodsId = goodsId, Reasons = ((string)reason).Trim() }); } } if (items.Count == 0) { return new List(); } var productCodes = GetWxProductCodes(shop, items.Select(item => item.GoodsId)); var mappedItems = new List(); foreach (var item in items) { if (!productCodes.TryGetValue(item.GoodsId, out var code) || string.IsNullOrWhiteSpace(code)) { LogSave.AddFile($"跳过违规商品 {item.GoodsId}:未找到对应的 spuCode。"); continue; } if (!TryGetGoodsId(code, out var goodsId)) { LogSave.AddFile($"跳过违规商品 {item.GoodsId}:spuCode 无法转换为有效商品编号。"); continue; } mappedItems.Add(new ReportGoodsViolationItem { GoodsId = goodsId, Reasons = item.Reasons }); } var reports = mappedItems.GroupBy(item => new { item.GoodsId, item.Reasons }).Select(group => group.First()).ToList(); var results = new List(); 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 batch) { if (!GlobalConfig.GlobalSettings.AutoReportViolationProducts) return; Dictionary 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(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) { code = code?.Trim() ?? string.Empty; if (code.StartsWith("P1-", StringComparison.Ordinal)) { code = code.Substring(3); } else if (code.StartsWith("EC1", StringComparison.Ordinal) && code.EndsWith("A", StringComparison.Ordinal)) { code = code.Substring(3, code.Length - 4); } return long.TryParse(code, out goodsId) && goodsId > 0; } /// /// 后台检查当前店铺所有在售商品,云端确认违规后立即下架;异常仅记录日志。 /// public static void CheckAndOffShelfViolations(ShopLoginInfo shop) { _ = Task.Run(async () => { try { await CheckAndOffShelfViolationsCore(shop).ConfigureAwait(false); } catch (Exception ex) { try { LogSave.AddFile($"店铺 {shop?.ShopId} 在售商品违规检查失败:{ex.Message}"); } catch { } } }); } private static async Task CheckAndOffShelfViolationsCore(ShopLoginInfo shop) { // 先读取完整列表,避免下架改变分页位置而漏查后面的商品。 var products = GetWxProductCodes(shop, status: 5); var mappings = new Dictionary(); foreach (var product in products) { if (TryGetGoodsId(product.Value, out var goodsId)) { mappings.Add(product.Key, goodsId); } else { LogSave.AddFile($"店铺 {shop.ShopId} 跳过商品 {product.Key}:spuCode 无法转换为有效商品编号。"); } } var goodsIds = mappings.Values.Distinct().ToList(); for (var offset = 0; offset < goodsIds.Count; offset += 50) { var batch = goodsIds.Skip(offset).Take(50).ToList(); var statuses = GetViolationStatus(batch); 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) { 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); } } } private static Dictionary GetWxProductCodes(ShopLoginInfo shop, IEnumerable productIds = null, int status = -1) { if (shop == null) { throw new ArgumentNullException(nameof(shop)); } var codes = new Dictionary(); var prodIds = productIds == null ? null : string.Join(",", productIds.Distinct()); long received = 0; for (var page = 1; ; page++) { var response = JObject.Parse(WxProductApi.SearchProduct(shop, page, pageSize: 50, prodIds: prodIds, status: status)); if (response["ret"]?.ToString() != "0" || response["success"]?.Type != JTokenType.Boolean || !(bool)response["success"] || !(response["productList"] is JArray products) || !long.TryParse(response["totalNum"]?.ToString(), out var total) || total < 0) { throw new Exception($"获取微信商品列表第 {page} 页失败。"); } var previousCount = codes.Count; foreach (var product in products) { if (!(product is JObject item) || !long.TryParse(item["productId"]?.ToString(), out var id) || id <= 0) { throw new Exception("微信商品列表包含无效的 productId。"); } codes[id] = item["spuCode"]?.Type == JTokenType.String ? (string)item["spuCode"] : null; } received += products.Count; if (received >= total) { return codes; } if (codes.Count == previousCount) { throw new Exception("微信商品列表分页未返回新商品,无法获取全部搜索结果。"); } } } /// /// 批量上报违规商品。 /// public static async Task> ReportViolations(List items) { if (items == null) { throw new ArgumentNullException(nameof(items)); } if (items.Any(item => item == null)) { throw new ArgumentException("违规商品列表不能包含空项。", nameof(items)); } return await StoreApiClient.Instance.CloudGoods.ReportViolations(items); } /// /// 批量查询商品违规状态。 /// public static List GetViolationStatus(List goodsIds) { if (goodsIds == null) { throw new ArgumentNullException(nameof(goodsIds)); } return StoreApiClient.Instance.CloudGoods.GetViolationStatus(goodsIds); } /// 逐原因记录店铺动作。日志请求失败不能改变剔除决定或中断后续下架。 public static Task RecordShopActionLogs(string shopId, GoodsViolationStatus status, ViolationLogAction action) => RecordShopActionLogs(shopId, new[] { status }, action); private static async Task RecordShopActionLogs(string shopId, IEnumerable statuses, ViolationLogAction action) { if (!(action == ViolationLogAction.AutoListingExcluded && GlobalConfig.GlobalSettings.AutoExcludeViolationProductsOnUpload || action == ViolationLogAction.AutoOffShelf && GlobalConfig.GlobalSettings.AutoDelistViolationProducts)) return; var logs = new List(); 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 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}"); } } /// 记录店铺已完成的违规上报、上货剔除或下架动作。 public static Task RecordViolationLog(RecordViolationLogRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); return RecordViolationLogs(new[] { request }); } public static async Task RecordViolationLogs(IReadOnlyList requests) { if (requests == null || requests.Count == 0 || requests.Any(item => item == null)) throw new ArgumentException("请提供有效的违规日志。", nameof(requests)); await StoreApiClient.Instance.CloudGoods.RecordViolationLogs(requests); } /// 读取日志筛选和显示使用的违规原因字典。 public static async Task> GetViolationReasons() { return await StoreApiClient.Instance.CloudGoods.GetViolationReasons(); } public class ViolationReason { public int Id { get; set; } public string Name { get; set; } public bool IsViolation { get; set; } } /// 按日志编号倒序查询,下一页传入上一页最后一条日志的 Id。 public static async Task> 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 }; return await StoreApiClient.Instance.CloudGoods.GetViolationLogs(request); } #region user /// /// 创建分组 /// /// /// /// public static async Task CreateGroup(string userName, string groupName) { bool ret = await StoreApiClient.Instance.CloudGoods.CreateGroup(userName, groupName); if (ret) { UserGroups = await GetGroups(userName); } return ret; } public static async Task DelGroupAsync(string userName, string groupName) { bool ret = await StoreApiClient.Instance.CloudGoods.DelGroupAsync(userName, groupName); if (ret) { UserGroups = await GetGroups(userName); } return ret; } public static async Task SetFavoriteGroupAsync(string userName, int groupId, IEnumerable goodsIds) { bool ret = await StoreApiClient.Instance.CloudGoods.SetFavoriteGroupAsync(userName, groupId, goodsIds); if (ret) { UserGroups = await GetGroups(userName); } return ret; } /// /// 获取分组 /// /// /// public static async Task> GetGroups(string userName) { return await StoreApiClient.Instance.CloudGoods.GetGroups(userName); } /// /// 搜索收藏 /// /// /// /// public static async Task> SearchFavorites(UserFavoritesGoodsSearchRequest request) { request.UserName = GetUserName(); var page = await StoreApiClient.Instance.CloudGoods.SearchFavorites(request); foreach (var item in page.List) { item.DetectionResult = item.IsSafe == null ? "未检测" : item.IsSafe.Value ? "安全" : "违规"; } return page; } /// /// 收藏 /// /// /// /// public static async Task Favorite(List goodsIds, int groupId) { return await StoreApiClient.Instance.CloudGoods.Favorite(GetUserName(), goodsIds, groupId); } /// /// 取消收藏 /// /// /// /// public static async Task RemoveFavorites(List goodsIds) { return await StoreApiClient.Instance.CloudGoods.RemoveFavorites(GetUserName(), goodsIds); } #endregion public static string GetUserName() { 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(); 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; } } /// /// 违规商品上报项。 /// public class ReportGoodsViolationItem { [JsonProperty("goodsId")] public long GoodsId { get; set; } [JsonProperty("reasons")] public string Reasons { get; set; } = string.Empty; } /// /// 单个商品的违规上报结果。 /// public class ReportGoodsViolationResult { [JsonProperty("goodsId")] public long GoodsId { get; set; } [JsonProperty("isOffShelf")] public bool IsOffShelf { get; set; } } /// /// 商品违规状态。 /// public class GoodsViolationStatus { [JsonProperty("reasonIds")] public int[] ReasonIds { get; set; } [JsonProperty("goodsId")] public long GoodsId { get; set; } [JsonProperty("isViolation")] public bool IsViolation { get; set; } } /// /// 商品搜索请求 /// public class GoodsSearchRequest { /// /// 标题(模糊搜索) /// public string Title { get; set; } = string.Empty; /// /// 分组ID /// public int? GroupId { get; set; } /// /// 最低价格(元) /// public long? MinPrice { get; set; } /// /// 最高价格(元) /// public long? MaxPrice { get; set; } /// /// 一级类目ID /// public int? CatId1 { get; set; } /// /// 二级类目ID /// public int? CatId2 { get; set; } /// /// 三级类目ID /// public int? CatId3 { get; set; } /// /// 四级类目ID /// public int? CatId4 { get; set; } /// /// 最低销量 /// public int? MinSoldQuantity { get; set; } /// /// 最高销量 /// public int? MaxSoldQuantity { get; set; } /// /// 平台类型 /// public PlatformType Platform { get; set; } /// 安全状态:0 全部(默认),1 安全,2 违规,3 未检测。 public int SafeStatus { get; set; } /// /// 页码(默认1) /// public int PageIndex { get; set; } = 1; /// /// 每页数量(默认20) /// public int PageSize { get; set; } = 20; } public class UserFavoritesGoodsSearchRequest : GoodsSearchRequest { public string UserName { get; set; } } /// /// 商品搜索返回结果 /// public class GoodsSearchResponse { /// /// 商品列表 /// [JsonProperty("list")] public List List { get; set; } = new List(); /// /// 总记录数 /// [JsonProperty("total")] public int Total { get; set; } /// /// 当前页码 /// [JsonProperty("pageIndex")] public int PageIndex { get; set; } /// /// 每页数量 /// [JsonProperty("pageSize")] public int PageSize { get; set; } /// /// 总页数 /// [JsonProperty("totalPages")] public int TotalPages { get; set; } } public class CloudGoods : BindableBase { public bool IsSelected { get => GetValue(); set => SetValue(value); } /// /// 自增ID /// public long Id { get; set; } /// /// 平台类型 /// public byte Platform { get; set; } /// /// 商品编号 /// public long GoodsId { get; set; } /// /// 标题 /// public string Title { get; set; } = string.Empty; public int GroupId { get; set; } public string GroupName { get; set; } /// /// 描述 /// public string? Description { get; set; } /// /// 缩略图 /// public string? ThumbUrl { get; set; } /// /// 一级类目ID /// public int? CatId1 { get; set; } /// /// 二级类目ID /// public int? CatId2 { get; set; } /// /// 三级类目ID /// public int? CatId3 { get; set; } /// /// 四级类目ID /// public int? CatId4 { get; set; } /// /// 最低价快照(元),导入时计算 /// public decimal MinPrice { get; set; } /// /// 最高价快照(元),导入时计算 /// public decimal MaxPrice { get; set; } public string Price => $"{MinPrice / 100m:N2}"; /// /// 销量 /// public int SoldQuantity { get; set; } /// /// 状态:0正常 1已删除 /// public byte Status { get; set; } /// /// 创建时间 /// public DateTime CreatedTime { get; set; } /// /// 更新时间(自动更新) /// public DateTime UpdatedTime { get; set; } /// /// 索引同步状态:0未同步 1已同步 /// public byte IndexStatus { get; set; } /// /// 安全检测 /// public bool? IsSafe { get; set; } [JsonIgnore] public string? DetectionResult { get => GetValue(); set => SetValue(value); } } public class UserFavoriteGroup { public long Id { get; set; } public string GroupName { get; set; } } }