diff --git a/AllBase/Helper/GlobalConfig.cs b/AllBase/Helper/GlobalConfig.cs index 674a0ab..1e41080 100644 --- a/AllBase/Helper/GlobalConfig.cs +++ b/AllBase/Helper/GlobalConfig.cs @@ -133,5 +133,14 @@ namespace AllBase.Helper /// 微信使用软件设置的默认上货地址 /// public bool IsWxUseSoftwareDefaultAddress { get; set; } = true; - } + + /// 自动上报违规商品 + public bool AutoReportViolationProducts { get; set; } = true; + + /// 上货自动剔除违规商品 + public bool AutoExcludeViolationProductsOnUpload { get; set; } = true; + + /// 自动下架违规商品 + public bool AutoDelistViolationProducts { get; set; } = true; + } } diff --git a/TEMUKJD/KJD.csproj.user b/TEMUKJD/KJD.csproj.user index af56d8a..76ca146 100644 --- a/TEMUKJD/KJD.csproj.user +++ b/TEMUKJD/KJD.csproj.user @@ -44,6 +44,9 @@ Form + + Form + Form diff --git a/TEMUKJD/KJDForms/SystemForms/SystemForm.Designer.cs b/TEMUKJD/KJDForms/SystemForms/SystemForm.Designer.cs index 1931c4e..7ad545c 100644 --- a/TEMUKJD/KJDForms/SystemForms/SystemForm.Designer.cs +++ b/TEMUKJD/KJDForms/SystemForms/SystemForm.Designer.cs @@ -29,8 +29,9 @@ private void InitializeComponent() { System.Windows.Forms.TreeNode treeNode1 = new System.Windows.Forms.TreeNode("解除限制"); + System.Windows.Forms.TreeNode violationNode = new System.Windows.Forms.TreeNode("违规检测"); System.Windows.Forms.TreeNode treeNode2 = new System.Windows.Forms.TreeNode("系统设置", new System.Windows.Forms.TreeNode[] { - treeNode1}); + treeNode1, violationNode}); this.panel1 = new System.Windows.Forms.Panel(); this.panel3 = new System.Windows.Forms.Panel(); this.panel2 = new System.Windows.Forms.Panel(); @@ -103,6 +104,8 @@ this.tv.Name = "tv"; treeNode1.Name = "解除限制"; treeNode1.Text = "解除限制"; + violationNode.Name = "违规检测"; + violationNode.Text = "违规检测"; treeNode2.Name = "系统设置"; treeNode2.Text = "系统设置"; this.tv.Nodes.AddRange(new System.Windows.Forms.TreeNode[] { @@ -162,4 +165,4 @@ private UserComponent.UcTreeView tv; private System.Windows.Forms.SplitContainer splitContainer1; } -} \ No newline at end of file +} diff --git a/TEMUKJD/KJDForms/SystemForms/SystemForm.cs b/TEMUKJD/KJDForms/SystemForms/SystemForm.cs index 4f406b3..ccfc34b 100644 --- a/TEMUKJD/KJDForms/SystemForms/SystemForm.cs +++ b/TEMUKJD/KJDForms/SystemForms/SystemForm.cs @@ -15,6 +15,7 @@ namespace TemuKJD.KJDForms.SystemForms public partial class SystemForm : BaseForm { public Lazy systemRemoveRestrictionsForm = new Lazy(); + public Lazy systemViolationDetectionForm = new Lazy(); public SystemForm() { InitializeComponent(); @@ -46,6 +47,9 @@ namespace TemuKJD.KJDForms.SystemForms case "解除限制": SetForm(systemRemoveRestrictionsForm.Value); break; + case "违规检测": + SetForm(systemViolationDetectionForm.Value); + break; //case "商品管理": // SetForm(shopProductForm.Value); // break; diff --git a/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.Designer.cs b/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.Designer.cs new file mode 100644 index 0000000..756d115 --- /dev/null +++ b/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.Designer.cs @@ -0,0 +1,61 @@ +namespace TemuKJD.KJDForms.SystemForms +{ + partial class SystemViolationDetectionForm + { + private System.ComponentModel.IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && components != null) + components.Dispose(); + base.Dispose(disposing); + } + + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.chkAutoReport = new TemuKJD.UserComponent.UcCheckBox(this.components); + this.chkAutoExcludeOnUpload = new TemuKJD.UserComponent.UcCheckBox(this.components); + this.chkAutoDelist = new TemuKJD.UserComponent.UcCheckBox(this.components); + this.SuspendLayout(); + // chkAutoReport + this.chkAutoReport.AutoSize = true; + this.chkAutoReport.Location = new System.Drawing.Point(24, 24); + this.chkAutoReport.Name = "chkAutoReport"; + this.chkAutoReport.TabIndex = 0; + this.chkAutoReport.Text = "自动上报违规商品"; + this.chkAutoReport.UseVisualStyleBackColor = true; + // chkAutoExcludeOnUpload + this.chkAutoExcludeOnUpload.AutoSize = true; + this.chkAutoExcludeOnUpload.Location = new System.Drawing.Point(24, 64); + this.chkAutoExcludeOnUpload.Name = "chkAutoExcludeOnUpload"; + this.chkAutoExcludeOnUpload.TabIndex = 1; + this.chkAutoExcludeOnUpload.Text = "上货自动剔除违规商品"; + this.chkAutoExcludeOnUpload.UseVisualStyleBackColor = true; + // chkAutoDelist + this.chkAutoDelist.AutoSize = true; + this.chkAutoDelist.Location = new System.Drawing.Point(24, 104); + this.chkAutoDelist.Name = "chkAutoDelist"; + this.chkAutoDelist.TabIndex = 2; + this.chkAutoDelist.Text = "自动下架违规商品"; + this.chkAutoDelist.UseVisualStyleBackColor = true; + // SystemViolationDetectionForm + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; + this.AutoScroll = true; + this.BackColor = System.Drawing.Color.White; + this.ClientSize = new System.Drawing.Size(800, 450); + this.Controls.Add(this.chkAutoReport); + this.Controls.Add(this.chkAutoExcludeOnUpload); + this.Controls.Add(this.chkAutoDelist); + this.Font = new System.Drawing.Font("微软雅黑", 9F); + this.Name = "SystemViolationDetectionForm"; + this.Text = "违规检测"; + this.ResumeLayout(false); + this.PerformLayout(); + } + + private UserComponent.UcCheckBox chkAutoReport; + private UserComponent.UcCheckBox chkAutoExcludeOnUpload; + private UserComponent.UcCheckBox chkAutoDelist; + } +} diff --git a/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.cs b/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.cs new file mode 100644 index 0000000..c6b83c4 --- /dev/null +++ b/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.cs @@ -0,0 +1,32 @@ +using System; +using AllBase.Helper; +using TemuKJD.BaseForms; + +namespace TemuKJD.KJDForms.SystemForms +{ + public partial class SystemViolationDetectionForm : BaseForm + { + public SystemViolationDetectionForm() + { + InitializeComponent(); + + var settings = GlobalConfig.GlobalSettings; + chkAutoReport.Checked = settings.AutoReportViolationProducts; + chkAutoExcludeOnUpload.Checked = settings.AutoExcludeViolationProductsOnUpload; + chkAutoDelist.Checked = settings.AutoDelistViolationProducts; + + chkAutoReport.CheckedChanged += Settings_CheckedChanged; + chkAutoExcludeOnUpload.CheckedChanged += Settings_CheckedChanged; + chkAutoDelist.CheckedChanged += Settings_CheckedChanged; + } + + private void Settings_CheckedChanged(object sender, EventArgs e) + { + var settings = GlobalConfig.GlobalSettings; + settings.AutoReportViolationProducts = chkAutoReport.Checked; + settings.AutoExcludeViolationProductsOnUpload = chkAutoExcludeOnUpload.Checked; + settings.AutoDelistViolationProducts = chkAutoDelist.Checked; + StartUp.SaveGlobalSettings(); + } + } +} diff --git a/TEMUKJD/PDDForms/ProductForms/WXProdViolationManage.cs b/TEMUKJD/PDDForms/ProductForms/WXProdViolationManage.cs index 90c491d..a64b4db 100644 --- a/TEMUKJD/PDDForms/ProductForms/WXProdViolationManage.cs +++ b/TEMUKJD/PDDForms/ProductForms/WXProdViolationManage.cs @@ -34,6 +34,10 @@ namespace TemuKJD.PDDForms.ProductForms if (shop.LoginStatus != "登录成功") continue; + // 开启自动下架时,后台检测当前店铺在售商品并下架违规商品。 + if (GlobalConfig.GlobalSettings.AutoDelistViolationProducts) + CloudGoodsService.CheckAndOffShelfViolations(shop); + Thread.Sleep(Randomx.GetId(500, 1500)); WXProdViolationGetInfo info = new WXProdViolationGetInfo() { shopId = shop.ShopId, updateDay = DateTime.Now.ToString("yyMMdd") }; @@ -52,7 +56,9 @@ namespace TemuKJD.PDDForms.ProductForms info.failReason = ""; var result = getShopiolationProd(shop); - CloudGoodsService.ReportViolationsFromJson(result); + // 开启自动上报时,后台解析店铺处罚记录并上报违规商品。 + if (GlobalConfig.GlobalSettings.AutoReportViolationProducts) + CloudGoodsService.ReportViolationsFromJson(result, shop); Thread.Sleep(Randomx.GetId(1000, 2000)); if (!result.Contains("\"code\":0")) diff --git a/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs b/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs index 799f8d5..f716e3c 100644 --- a/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs +++ b/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs @@ -675,7 +675,7 @@ namespace TemuKJD.WXForms.ProductForms item.Cells["colSelect"].Value = wxUploadRule.SelectedShopIds.Contains(shopId); } - + } isInit = true; } @@ -1715,6 +1715,36 @@ namespace TemuKJD.WXForms.ProductForms })); } + // 开启上货自动剔除时,上传前检测违规状态;违规或检测失败均禁止上传。 + if (GlobalConfig.GlobalSettings.AutoExcludeViolationProductsOnUpload) + { + try + { + if (!long.TryParse(dbProd.spu_id, out var goodsId) || goodsId <= 0) + throw new Exception("商品ID无效"); + + var statuses = CloudGoodsService.GetViolationStatus(new List { goodsId }).GetAwaiter().GetResult(); + var status = statuses.FirstOrDefault(item => item.GoodsId == goodsId); + if (status == null) + throw new Exception("未返回当前商品的违规状态"); + if (status.IsViolation) + return "{\"success\":false,\"draftId\":0,\"status\":\"未上传\",\"errMsg\":\"当前商品违规,禁止上传\"}"; + } + catch (Exception ex) + { + if (cell != null) + Invoke(new Action(() => cell.Value = "商品违规检测失败,禁止上传")); + + return JsonConvert.SerializeObject(new + { + success = false, + draftId = 0, + status = "未上传", + errMsg = "商品违规检测失败,禁止上传:" + ex.Message + }); + } + } + #region 裂变标题 if (BaseClass.UserAuthority.Contains("裂变调试") && chkFissionGoodsName.Checked) { diff --git a/Work/Services/CloudGoodsService.cs b/Work/Services/CloudGoodsService.cs index ce00b46..1488e26 100644 --- a/Work/Services/CloudGoodsService.cs +++ b/Work/Services/CloudGoodsService.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Work.Api.Wx; namespace Work.Services { @@ -48,28 +49,31 @@ namespace Work.Services } /// - /// 在后台解析微信处罚 JSON 并上报;无商品或处理失败时返回空列表,异常在内部记录。 + /// 在后台解析微信处罚 JSON、匹配店铺商品编码并上报,异常在内部记录。 /// - public static void ReportViolationsFromJson(string json) + public static void ReportViolationsFromJson(string json, ShopLoginInfo shop) { - try - { - Task.Run(() => ReportViolationsFromJsonCore(json)).ConfigureAwait(false); - } - catch (Exception ex) + _ = Task.Run(async () => { try { - LogSave.AddFile("解析并上报违规商品失败:" + ex.Message); + await ReportViolationsFromJsonCore(json, shop).ConfigureAwait(false); } - catch + catch (Exception ex) { - // 日志失败也不能影响调用方。 + try + { + LogSave.AddFile("解析并上报违规商品失败:" + ex.Message); + } + catch + { + // 日志失败也不能影响调用方。 + } } - } + }); } - private static async Task> ReportViolationsFromJsonCore(string json) + private static async Task> ReportViolationsFromJsonCore(string json, ShopLoginInfo shop) { if (string.IsNullOrWhiteSpace(json)) { @@ -96,6 +100,8 @@ namespace Work.Services throw new ArgumentException("处罚数据缺少 record.items 数组。", nameof(json)); } + if (records.Count == 0) + return new List(); var items = new List(); foreach (var token in records) { @@ -138,12 +144,147 @@ namespace Work.Services } } - var reports = items.GroupBy(item => item.GoodsId).Select(group => new ReportGoodsViolationItem + 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 => item.GoodsId).Select(group => new ReportGoodsViolationItem { GoodsId = group.Key, Reasons = string.Join(";", group.Select(item => item.Reasons).Distinct()) }).ToList(); - return reports.Count == 0 ? new List() : await ReportViolations(reports); + + return reports.Count == 0 + ? new List() : await ReportViolations(reports); + } + + 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 = await GetViolationStatus(batch).ConfigureAwait(false); + var violations = new HashSet(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(); + // 同一云商品可能对应多个微信商品,每次下架最多 50 个。 + for (var index = 0; index < productIds.Count; index += 50) + { + var ids = string.Join(",", productIds.Skip(index).Take(50)); + var response = JObject.Parse(WxProductApi.DownProd(shop, ids)); + if (response["code"]?.ToString() != "0") + { + throw new Exception($"微信商品 {ids} 下架失败:{response}"); + } + LogSave.AddFile($"店铺 {shop.ShopId} 云端检测违规,已下架微信商品:{ids}"); + } + } + } + + 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("微信商品列表分页未返回新商品,无法获取全部搜索结果。"); + } + } } ///