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("微信商品列表分页未返回新商品,无法获取全部搜索结果。");
+ }
+ }
}
///