diff --git a/TEMUKJD/KJD.csproj.user b/TEMUKJD/KJD.csproj.user
index 76ca146..0382c37 100644
--- a/TEMUKJD/KJD.csproj.user
+++ b/TEMUKJD/KJD.csproj.user
@@ -10,7 +10,7 @@
zh-CN
false
- Production
+ Development
false
@@ -182,6 +182,9 @@
Form
+
+ Form
+
Form
diff --git a/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.cs b/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.cs
index c6b83c4..2e506e2 100644
--- a/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.cs
+++ b/TEMUKJD/KJDForms/SystemForms/SystemViolationDetectionForm.cs
@@ -10,18 +10,29 @@ namespace TemuKJD.KJDForms.SystemForms
{
InitializeComponent();
- var settings = GlobalConfig.GlobalSettings;
- chkAutoReport.Checked = settings.AutoReportViolationProducts;
- chkAutoExcludeOnUpload.Checked = settings.AutoExcludeViolationProductsOnUpload;
- chkAutoDelist.Checked = settings.AutoDelistViolationProducts;
-
+ VisibleChanged += (s, e) => { if (Visible) LoadSettings(); };
+ LoadSettings();
chkAutoReport.CheckedChanged += Settings_CheckedChanged;
chkAutoExcludeOnUpload.CheckedChanged += Settings_CheckedChanged;
chkAutoDelist.CheckedChanged += Settings_CheckedChanged;
}
+ private bool loading;
+
+ private void LoadSettings()
+ {
+ loading = true;
+ var settings = GlobalConfig.GlobalSettings;
+ chkAutoReport.Checked = settings.AutoReportViolationProducts;
+ chkAutoExcludeOnUpload.Checked = settings.AutoExcludeViolationProductsOnUpload;
+ chkAutoDelist.Checked = settings.AutoDelistViolationProducts;
+
+ loading = false;
+ }
+
private void Settings_CheckedChanged(object sender, EventArgs e)
{
+ if (loading) return;
var settings = GlobalConfig.GlobalSettings;
settings.AutoReportViolationProducts = chkAutoReport.Checked;
settings.AutoExcludeViolationProductsOnUpload = chkAutoExcludeOnUpload.Checked;
diff --git a/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs b/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs
index cf3aae0..aa39fc8 100644
--- a/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs
+++ b/TEMUKJD/WXForms/ProductForms/WXDBProdUploadForm.cs
@@ -1718,7 +1718,8 @@ namespace TemuKJD.WXForms.ProductForms
// 开启上货自动剔除时,上传前检测违规状态;仅服务端明确返回违规时禁止上传。
if (GlobalConfig.GlobalSettings.AutoExcludeViolationProductsOnUpload)
{
- Invoke(new Action(() => cell.Value = "商品违规检测..."));
+ if (cell != null)
+ Invoke(new Action(() => cell.Value = "商品违规检测..."));
try
{
long goodsId = 0;
@@ -1731,7 +1732,21 @@ namespace TemuKJD.WXForms.ProductForms
if (status == null)
throw new Exception("未返回当前商品的违规状态");
if (status.IsViolation)
+ {
+ var shopId = shop.ShopId;
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await CloudGoodsService.RecordShopActionLogs(shopId, status, ViolationLogAction.AutoListingExcluded);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("剔除违规商品日志记录失败:" + ex.Message);
+ }
+ });
return "{\"success\":false,\"draftId\":0,\"status\":\"未上传\",\"errMsg\":\"当前商品违规,禁止上传\"}";
+ }
}
catch (Exception ex)
{
@@ -6329,6 +6344,37 @@ namespace TemuKJD.WXForms.ProductForms
}
}
+ ///
+ /// 图片宽高均达到指定尺寸时返回 true;尺寸不足或无效输入返回 false。
+ ///
+ public bool IsBase64ImageSizeValid(string base64Image, int width, int height)
+ {
+ if (string.IsNullOrWhiteSpace(base64Image) || width <= 0 || height <= 0)
+ return false;
+
+ try
+ {
+ using (var stream = new MemoryStream(Convert.FromBase64String(base64Image)))
+ using (var image = System.Drawing.Image.FromStream(stream))
+ {
+ return image.Width >= width && image.Height >= height;
+ }
+ }
+ catch (FormatException)
+ {
+ return false;
+ }
+ catch (ArgumentException)
+ {
+ return false;
+ }
+ catch (OutOfMemoryException)
+ {
+ // GDI+ 对不支持或损坏的图片也可能抛出此异常。
+ return false;
+ }
+ }
+
public string UploadImg(ShopLoginInfo shop, string img_url, DataGridViewCell cell = null, bool randomBlock = false)
{
string getImgError;
diff --git a/TEMUKJD/WXForms/ProductForms/WXProductForm.cs b/TEMUKJD/WXForms/ProductForms/WXProductForm.cs
index 50899df..5a8c778 100644
--- a/TEMUKJD/WXForms/ProductForms/WXProductForm.cs
+++ b/TEMUKJD/WXForms/ProductForms/WXProductForm.cs
@@ -35,9 +35,11 @@ namespace TemuKJD.WXForms.ProductForms
public Lazy prodNoCheckEditForm = new Lazy();
public Lazy cloudgoodsForm = new Lazy();
public Lazy cloudgoodsFavoriteForm = new Lazy();
+ private readonly Lazy violationLogsForm = new Lazy();
public WXProductForm()
{
InitializeComponent();
+ tv.Nodes[0].Nodes.Insert(1, "违规记录", "违规记录");
try
{
@@ -102,6 +104,9 @@ namespace TemuKJD.WXForms.ProductForms
case "云端数据":
SetForm(cloudgoodsForm.Value);
break;
+ case "违规记录":
+ SetForm(violationLogsForm.Value);
+ break;
case "我的商品库":
SetForm(userCollectAuthProdListForm.Value);
break;
diff --git a/TEMUKJD/WXForms/ProductForms/WXViolationLogsForm.Designer.cs b/TEMUKJD/WXForms/ProductForms/WXViolationLogsForm.Designer.cs
new file mode 100644
index 0000000..b28eba9
--- /dev/null
+++ b/TEMUKJD/WXForms/ProductForms/WXViolationLogsForm.Designer.cs
@@ -0,0 +1,357 @@
+using System.Drawing;
+using System.Windows.Forms;
+using DevComponents.DotNetBar;
+using TemuKJD.UserComponent;
+
+namespace TemuKJD.WXForms.ProductForms
+{
+ partial class WXViolationLogsForm
+ {
+ private TableLayoutPanel layout;
+ private FlowLayoutPanel settings;
+ private FlowLayoutPanel filters;
+ private CheckBox report;
+ private CheckBox exclude;
+ private CheckBox delist;
+ private PanelEx btnSave;
+ private Label shopLabel;
+ private ComboBox shop;
+ private Label reasonLabel;
+ private ComboBox reason;
+ private Label actionLabel;
+ private ComboBox action;
+ private PanelEx btnQuery;
+ private UserControls.UcSplitPage pagination;
+ private Label message;
+ private DataGridViewEx grid;
+ private DataGridViewTextBoxColumn colAction;
+
+ private void InitializeComponent()
+ {
+ System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
+ System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
+ this.layout = new System.Windows.Forms.TableLayoutPanel();
+ this.settings = new System.Windows.Forms.FlowLayoutPanel();
+ this.report = new System.Windows.Forms.CheckBox();
+ this.exclude = new System.Windows.Forms.CheckBox();
+ this.delist = new System.Windows.Forms.CheckBox();
+ this.btnSave = new DevComponents.DotNetBar.PanelEx();
+ this.filters = new System.Windows.Forms.FlowLayoutPanel();
+ this.shopLabel = new System.Windows.Forms.Label();
+ this.shop = new System.Windows.Forms.ComboBox();
+ this.reasonLabel = new System.Windows.Forms.Label();
+ this.reason = new System.Windows.Forms.ComboBox();
+ this.actionLabel = new System.Windows.Forms.Label();
+ this.action = new System.Windows.Forms.ComboBox();
+ this.btnQuery = new DevComponents.DotNetBar.PanelEx();
+ this.pagination = new UserControls.UcSplitPage();
+ this.message = new System.Windows.Forms.Label();
+ this.grid = new TemuKJD.UserComponent.DataGridViewEx();
+ this.ShopId = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.GoodsId = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.ReasonId = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.colAction = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.CreatedTime = new System.Windows.Forms.DataGridViewTextBoxColumn();
+ this.layout.SuspendLayout();
+ this.settings.SuspendLayout();
+ this.filters.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.grid)).BeginInit();
+ this.SuspendLayout();
+ //
+ // layout
+ //
+ this.layout.ColumnCount = 1;
+ this.layout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ this.layout.Controls.Add(this.settings, 0, 0);
+ this.layout.Controls.Add(this.filters, 0, 1);
+ this.layout.Controls.Add(this.grid, 0, 2);
+ this.layout.Controls.Add(this.pagination, 0, 3);
+ this.layout.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.layout.Location = new System.Drawing.Point(0, 0);
+ this.layout.Name = "layout";
+ this.layout.RowCount = 4;
+ this.layout.RowStyles.Add(new System.Windows.Forms.RowStyle());
+ this.layout.RowStyles.Add(new System.Windows.Forms.RowStyle());
+ this.layout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ this.layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ this.layout.Size = new System.Drawing.Size(1100, 530);
+ this.layout.TabIndex = 0;
+ //
+ // settings
+ //
+ this.settings.AutoSize = true;
+ this.settings.Controls.Add(this.report);
+ this.settings.Controls.Add(this.exclude);
+ this.settings.Controls.Add(this.delist);
+ this.settings.Controls.Add(this.btnSave);
+ this.settings.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.settings.Location = new System.Drawing.Point(3, 3);
+ this.settings.Name = "settings";
+ this.settings.Padding = new System.Windows.Forms.Padding(6, 4, 6, 4);
+ this.settings.Size = new System.Drawing.Size(1094, 39);
+ this.settings.TabIndex = 0;
+ //
+ // report
+ //
+ this.report.Anchor = System.Windows.Forms.AnchorStyles.Left;
+ this.report.AutoSize = true;
+ this.report.Location = new System.Drawing.Point(9, 9);
+ this.report.Name = "report";
+ this.report.Size = new System.Drawing.Size(123, 21);
+ this.report.TabIndex = 0;
+ this.report.Text = "自动上报违规商品";
+ //
+ // exclude
+ //
+ this.exclude.Anchor = System.Windows.Forms.AnchorStyles.Left;
+ this.exclude.AutoSize = true;
+ this.exclude.Location = new System.Drawing.Point(138, 9);
+ this.exclude.Name = "exclude";
+ this.exclude.Size = new System.Drawing.Size(147, 21);
+ this.exclude.TabIndex = 1;
+ this.exclude.Text = "上货自动剔除违规商品";
+ //
+ // delist
+ //
+ this.delist.Anchor = System.Windows.Forms.AnchorStyles.Left;
+ this.delist.AutoSize = true;
+ this.delist.Location = new System.Drawing.Point(291, 9);
+ this.delist.Name = "delist";
+ this.delist.Size = new System.Drawing.Size(123, 21);
+ this.delist.TabIndex = 2;
+ this.delist.Text = "自动下架违规商品";
+ //
+ // btnSave
+ //
+ this.btnSave.AccessibleRole = System.Windows.Forms.AccessibleRole.PushButton;
+ this.btnSave.ColorSchemeStyle = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled;
+ this.btnSave.Cursor = System.Windows.Forms.Cursors.Hand;
+ this.btnSave.DisabledBackColor = System.Drawing.Color.Empty;
+ this.btnSave.Location = new System.Drawing.Point(420, 7);
+ this.btnSave.Name = "btnSave";
+ this.btnSave.Size = new System.Drawing.Size(80, 25);
+ this.btnSave.Style.Alignment = System.Drawing.StringAlignment.Center;
+ this.btnSave.Style.BackColor1.Color = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(126)))), ((int)(((byte)(255)))));
+ this.btnSave.Style.BackColor2.Color = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(126)))), ((int)(((byte)(255)))));
+ this.btnSave.Style.BorderSide = DevComponents.DotNetBar.eBorderSide.None;
+ this.btnSave.Style.ForeColor.Color = System.Drawing.Color.White;
+ this.btnSave.StyleMouseOver.Alignment = System.Drawing.StringAlignment.Center;
+ this.btnSave.StyleMouseOver.BackColor1.Color = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(102)))), ((int)(((byte)(255)))));
+ this.btnSave.StyleMouseOver.BackColor2.Color = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(102)))), ((int)(((byte)(255)))));
+ this.btnSave.TabIndex = 4;
+ this.btnSave.Text = "保存配置";
+ this.btnSave.Click += new System.EventHandler(this.Save_Click);
+ //
+ // filters
+ //
+ this.filters.AutoSize = true;
+ this.filters.Controls.Add(this.shopLabel);
+ this.filters.Controls.Add(this.shop);
+ this.filters.Controls.Add(this.reasonLabel);
+ this.filters.Controls.Add(this.reason);
+ this.filters.Controls.Add(this.actionLabel);
+ this.filters.Controls.Add(this.action);
+ this.filters.Controls.Add(this.btnQuery);
+
+ this.filters.Controls.Add(this.message);
+ this.filters.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.filters.Location = new System.Drawing.Point(3, 48);
+ this.filters.Name = "filters";
+ this.filters.Padding = new System.Windows.Forms.Padding(6, 4, 6, 4);
+ this.filters.Size = new System.Drawing.Size(1094, 39);
+ this.filters.TabIndex = 1;
+ //
+ // shopLabel
+ //
+ this.shopLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
+ this.shopLabel.AutoSize = true;
+ this.shopLabel.Location = new System.Drawing.Point(9, 11);
+ this.shopLabel.Name = "shopLabel";
+ this.shopLabel.Size = new System.Drawing.Size(32, 17);
+ this.shopLabel.TabIndex = 0;
+ this.shopLabel.Text = "店铺";
+ //
+ // shop
+ //
+ this.shop.DisplayMember = "Value";
+ this.shop.SelectionChangeCommitted += this.Query_Click;
+ this.shop.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.shop.Location = new System.Drawing.Point(47, 7);
+ this.shop.Name = "shop";
+ this.shop.Size = new System.Drawing.Size(150, 25);
+ this.shop.TabIndex = 1;
+ this.shop.ValueMember = "Key";
+ //
+ // reasonLabel
+ //
+ this.reasonLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
+ this.reasonLabel.AutoSize = true;
+ this.reasonLabel.Location = new System.Drawing.Point(203, 11);
+ this.reasonLabel.Name = "reasonLabel";
+ this.reasonLabel.Size = new System.Drawing.Size(56, 17);
+ this.reasonLabel.TabIndex = 2;
+ this.reasonLabel.Text = "违规类型";
+ //
+ // reason
+ //
+ this.reason.DisplayMember = "Value";
+ this.reason.SelectionChangeCommitted += this.Query_Click;
+ this.reason.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.reason.Location = new System.Drawing.Point(265, 7);
+ this.reason.Name = "reason";
+ this.reason.Size = new System.Drawing.Size(150, 25);
+ this.reason.TabIndex = 3;
+ this.reason.ValueMember = "Key";
+ //
+ // actionLabel
+ //
+ this.actionLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
+ this.actionLabel.AutoSize = true;
+ this.actionLabel.Location = new System.Drawing.Point(421, 11);
+ this.actionLabel.Name = "actionLabel";
+ this.actionLabel.Size = new System.Drawing.Size(56, 17);
+ this.actionLabel.TabIndex = 4;
+ this.actionLabel.Text = "日志动作";
+ //
+ // action
+ //
+ this.action.SelectionChangeCommitted += this.Query_Click;
+ this.action.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.action.Items.AddRange(new object[] {
+ "全部动作",
+ "违规上报",
+ "上货剔除",
+ "违规下架"});
+ this.action.Location = new System.Drawing.Point(483, 7);
+ this.action.Name = "action";
+ this.action.Size = new System.Drawing.Size(150, 25);
+ this.action.TabIndex = 5;
+ //
+ // btnQuery
+ //
+ this.btnQuery.AccessibleRole = System.Windows.Forms.AccessibleRole.PushButton;
+ this.btnQuery.ColorSchemeStyle = DevComponents.DotNetBar.eDotNetBarStyle.StyleManagerControlled;
+ this.btnQuery.Cursor = System.Windows.Forms.Cursors.Hand;
+ this.btnQuery.DisabledBackColor = System.Drawing.Color.Empty;
+ this.btnQuery.Location = new System.Drawing.Point(639, 7);
+ this.btnQuery.Name = "btnQuery";
+ this.btnQuery.Size = new System.Drawing.Size(58, 25);
+ this.btnQuery.Style.Alignment = System.Drawing.StringAlignment.Center;
+ this.btnQuery.Style.BackColor1.Color = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(126)))), ((int)(((byte)(255)))));
+ this.btnQuery.Style.BackColor2.Color = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(126)))), ((int)(((byte)(255)))));
+ this.btnQuery.Style.BorderSide = DevComponents.DotNetBar.eBorderSide.None;
+ this.btnQuery.Style.ForeColor.Color = System.Drawing.Color.White;
+ this.btnQuery.StyleMouseOver.Alignment = System.Drawing.StringAlignment.Center;
+ this.btnQuery.StyleMouseOver.BackColor1.Color = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(102)))), ((int)(((byte)(255)))));
+ this.btnQuery.StyleMouseOver.BackColor2.Color = System.Drawing.Color.FromArgb(((int)(((byte)(25)))), ((int)(((byte)(102)))), ((int)(((byte)(255)))));
+ this.btnQuery.TabIndex = 6;
+ this.btnQuery.Text = "查询";
+ this.btnQuery.Click += new System.EventHandler(this.Query_Click);
+ //
+ // pagination
+ //
+ this.pagination.Dock = DockStyle.Right;
+ this.pagination.Name = "pagination";
+ this.pagination.Size = new Size(462, 25);
+ this.pagination.PageSize = 50;
+ this.pagination.IsShowPageSize = false;
+ this.pagination.IsShowTotalCount = false;
+ this.pagination.PageChange += this.Pagination_PageChange;
+ //
+ // message
+ //
+ this.message.Anchor = System.Windows.Forms.AnchorStyles.Left;
+ this.message.AutoSize = true;
+ this.message.Location = new System.Drawing.Point(789, 11);
+ this.message.Name = "message";
+ this.message.Size = new System.Drawing.Size(0, 17);
+ this.message.TabIndex = 14;
+ //
+ // grid
+ //
+ this.grid.AllowUserToAddRows = false;
+ this.grid.AllowUserToDeleteRows = false;
+ dataGridViewCellStyle1.BackColor = System.Drawing.Color.WhiteSmoke;
+ this.grid.AlternatingRowsDefaultCellStyle = dataGridViewCellStyle1;
+ this.grid.AutoRowNumber = true;
+ this.grid.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
+ this.grid.BackgroundColor = System.Drawing.Color.White;
+ this.grid.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(126)))), ((int)(((byte)(255)))));
+ this.grid.Columns.AddRange(new DataGridViewColumn[] { this.ShopId, this.GoodsId, this.ReasonId, this.colAction, this.CreatedTime });
+ this.grid.CellFormatting += new DataGridViewCellFormattingEventHandler(this.Grid_CellFormatting);
+ this.grid.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.grid.Location = new System.Drawing.Point(3, 93);
+ this.grid.MultiSelectCheckBox = true;
+ this.grid.Name = "grid";
+ this.grid.ReadOnly = true;
+ this.grid.RowHeadersVisible = true;
+ this.grid.Size = new System.Drawing.Size(1094, 434);
+ this.grid.TabIndex = 2;
+ //
+ //
+ // ShopId
+ //
+ this.ShopId.DataPropertyName = "ShopId";
+ this.ShopId.HeaderText = "店铺";
+ this.ShopId.Name = "ShopId";
+ this.ShopId.Width = 38;
+ //
+ // GoodsId
+ //
+ this.GoodsId.DataPropertyName = "GoodsId";
+ this.GoodsId.HeaderText = "商品编号";
+ this.GoodsId.Name = "GoodsId";
+ this.GoodsId.Width = 48;
+ //
+ // ReasonId
+ //
+ this.ReasonId.DataPropertyName = "ReasonId";
+ this.ReasonId.HeaderText = "违规类型";
+ this.ReasonId.Name = "ReasonId";
+ this.ReasonId.Width = 64;
+ //
+ // colAction
+ //
+ this.colAction.DataPropertyName = "Action";
+ this.colAction.HeaderText = "日志动作";
+ this.colAction.Name = "colAction";
+ this.colAction.Width = 96;
+ //
+ // CreatedTime
+ //
+ this.CreatedTime.DataPropertyName = "CreatedTime";
+ dataGridViewCellStyle2.Format = "yyyy-MM-dd HH:mm:ss";
+ this.CreatedTime.DefaultCellStyle = dataGridViewCellStyle2;
+ this.CreatedTime.HeaderText = "记录时间";
+ this.CreatedTime.Name = "CreatedTime";
+ this.CreatedTime.Width = 191;
+ //
+ // WXViolationLogsForm
+ //
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
+ this.BackColor = System.Drawing.Color.White;
+ this.ClientSize = new System.Drawing.Size(1100, 530);
+ this.Controls.Add(this.layout);
+ this.Font = new System.Drawing.Font("微软雅黑", 9F);
+ this.KeyPreview = true;
+ this.Name = "WXViolationLogsForm";
+ this.Text = "违规日志";
+ this.Shown += new System.EventHandler(this.Query_Click);
+ this.VisibleChanged += new System.EventHandler(this.Form_VisibleChanged);
+ this.layout.ResumeLayout(false);
+ this.layout.PerformLayout();
+ this.settings.ResumeLayout(false);
+ this.settings.PerformLayout();
+ this.filters.ResumeLayout(false);
+ this.filters.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.grid)).EndInit();
+ this.ResumeLayout(false);
+
+ }
+
+ private DataGridViewTextBoxColumn ShopId;
+ private DataGridViewTextBoxColumn GoodsId;
+ private DataGridViewTextBoxColumn ReasonId;
+ private DataGridViewTextBoxColumn CreatedTime;
+ }
+}
diff --git a/TEMUKJD/WXForms/ProductForms/WXViolationLogsForm.cs b/TEMUKJD/WXForms/ProductForms/WXViolationLogsForm.cs
new file mode 100644
index 0000000..07f1aec
--- /dev/null
+++ b/TEMUKJD/WXForms/ProductForms/WXViolationLogsForm.cs
@@ -0,0 +1,160 @@
+using AllBase;
+using AllBase.Helper;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using TemuKJD.BaseForms;
+using Work.Services;
+
+namespace TemuKJD.WXForms.ProductForms
+{
+ public partial class WXViolationLogsForm : BaseForm
+ {
+ private readonly BindingList rows = new BindingList();
+ private Dictionary reasons;
+ private Dictionary shops;
+ private const int PageSize = 50;
+ private readonly List pageCursors = new List { 0 };
+ private int currentPage;
+
+ public WXViolationLogsForm()
+ {
+ InitializeComponent();
+ action.SelectedIndex = 0;
+ grid.AutoGenerateColumns = false;
+ grid.DataSource = rows;
+ }
+
+ private void Save_Click(object sender, EventArgs e)
+ {
+ var config = GlobalConfig.GlobalSettings;
+ config.AutoReportViolationProducts = report.Checked;
+ config.AutoExcludeViolationProductsOnUpload = exclude.Checked;
+ config.AutoDelistViolationProducts = delist.Checked;
+
+ StartUp.SaveGlobalSettings();
+ message.Text = "配置已保存";
+ }
+
+ private void Form_VisibleChanged(object sender, EventArgs e)
+ {
+ if (!Visible) return;
+ var config = GlobalConfig.GlobalSettings;
+ report.Checked = config.AutoReportViolationProducts;
+ exclude.Checked = config.AutoExcludeViolationProductsOnUpload;
+ delist.Checked = config.AutoDelistViolationProducts;
+ var selected = shop.SelectedValue as string;
+ shops = GlobalConfig.ShopLoginList.Where(x => !string.IsNullOrWhiteSpace(x.ShopId))
+ .GroupBy(x => x.ShopId).ToDictionary(g => g.Key, g => string.IsNullOrWhiteSpace(g.First().ShopAccount)
+ ? "未知店铺" : g.First().ShopAccount);
+ shop.DataSource = new[] { new KeyValuePair("", "全部店铺") }.Concat(shops).ToList();
+ if (selected != null && shops.ContainsKey(selected)) shop.SelectedValue = selected;
+ }
+
+ private async void Query_Click(object sender, EventArgs e) => await QueryAsync(0, reset: true);
+
+ private async void Pagination_PageChange(object sender)
+ {
+ int targetPage = pagination.CurrentPage - 1;
+ if (targetPage != currentPage) await QueryAsync(targetPage);
+ }
+
+ private void Grid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
+ {
+ if (e.Value == null) return;
+ switch (grid.Columns[e.ColumnIndex].DataPropertyName)
+ {
+ case nameof(GoodsViolationLogEntity.ShopId):
+ e.Value = shops.TryGetValue((string)e.Value, out var name) ? name : "未知店铺";
+ e.FormattingApplied = true;
+ break;
+ case nameof(GoodsViolationLogEntity.ReasonId):
+ e.Value = reasons.TryGetValue((int)e.Value, out var label) ? label : "未知违规类型";
+ e.FormattingApplied = true;
+ break;
+ case nameof(GoodsViolationLogEntity.Action):
+ var index = (int)(ViolationLogAction)e.Value;
+ e.Value = index > 0 && index < action.Items.Count ? action.Items[index] : "未知日志动作";
+ e.FormattingApplied = true;
+ break;
+ }
+ }
+
+ private async Task QueryAsync(int targetPage, bool reset = false)
+ {
+ if (!filters.Enabled) return;
+ filters.Enabled = false;
+ pagination.Enabled = false;
+ try
+ {
+ if (reset)
+ {
+ currentPage = 0;
+ pageCursors.Clear();
+ pageCursors.Add(0);
+ rows.Clear();
+
+ }
+ message.Text = "正在查询…";
+ if (reasons == null)
+ {
+ var types = await CloudGoodsService.GetViolationReasons();
+ if (IsDisposed) return;
+ reasons = types.ToDictionary(type => type.Id, type => type.Name);
+ reason.DataSource = new[] { new KeyValuePair(0, "全部类型") }
+ .Concat(types.Where(type => type.IsViolation).Select(type => new KeyValuePair(type.Id, type.Name))).ToList();
+ }
+ var selected = shop.SelectedValue as string;
+ var ids = string.IsNullOrEmpty(selected) ? shops.Keys.ToArray() : new[] { selected };
+ if (ids.Length == 0) { message.Text = "暂无可查询的店铺"; return; }
+ var reasonId = (int)reason.SelectedValue;
+ var actionId = action.SelectedIndex;
+ var cursor = pageCursors[targetPage];
+ var matches = new List();
+ while (matches.Count <= PageSize)
+ {
+ var batch = await CloudGoodsService.GetViolationLogs(ids, beforeId: cursor, size: PageSize);
+ if (IsDisposed) return;
+ foreach (var log in batch)
+ {
+ if (cursor != 0 && log.Id >= cursor) throw new InvalidOperationException("违规日志分页顺序异常。");
+ cursor = log.Id;
+ if ((reasonId == 0 || reasonId == log.ReasonId) && (actionId == 0 || actionId == (int)log.Action))
+ matches.Add(log);
+ if (matches.Count > PageSize) break;
+ }
+ if (batch.Count < PageSize) break;
+ }
+
+ if (matches.Count <= PageSize || pageCursors.ElementAtOrDefault(targetPage + 1) != matches[PageSize - 1].Id)
+ {
+ pageCursors.RemoveRange(targetPage + 1, pageCursors.Count - targetPage - 1);
+ if (matches.Count > PageSize) pageCursors.Add(matches[PageSize - 1].Id);
+ }
+ currentPage = targetPage;
+ rows.Clear();
+ foreach (var log in matches.Take(PageSize)) rows.Add(log);
+
+ message.Text = rows.Count == 0 ? "无匹配日志" : $"本页 {rows.Count} 条";
+ }
+ catch (Exception ex)
+ {
+ message.Text = "查询失败:" + ex.Message;
+ }
+ finally
+ {
+ if (!IsDisposed)
+ {
+ pagination.TotalPage = pageCursors.Count;
+ if (pagination.CurrentPage != currentPage + 1) pagination.CurrentPage = currentPage + 1;
+ filters.Enabled = true;
+ pagination.Enabled = true;
+ }
+ }
+ }
+
+ }
+}
diff --git a/TEMUKJD/WXForms/ProductForms/WXViolationLogsForm.resx b/TEMUKJD/WXForms/ProductForms/WXViolationLogsForm.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/TEMUKJD/WXForms/ProductForms/WXViolationLogsForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Work/Services/CloudGoodsService.cs b/Work/Services/CloudGoodsService.cs
index 2d6b8e2..7bf30a5 100644
--- a/Work/Services/CloudGoodsService.cs
+++ b/Work/Services/CloudGoodsService.cs
@@ -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() : await ReportViolations(reports);
+ 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)
@@ -231,19 +263,24 @@ namespace Work.Services
{
var batch = goodsIds.Skip(offset).Take(50).ToList();
var statuses = GetViolationStatus(batch);
- 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();
+ 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("批量查询商品违规状态返回数据为空。");
}
+ /// 逐原因记录店铺动作。日志请求失败不能改变剔除决定或中断后续下架。
+ 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));
+ var html = await GetHtml.PostJsonAsync(ServerUrl + "/api/CloudGoods/RecordViolationLog", requests, timeOut: 5);
+ var ret = JsonConvert.DeserializeObject(html) ?? throw new Exception("记录违规日志返回为空。");
+ if (ret.Code != 200)
+ throw new Exception(ret.Msg);
+ }
+
+ /// 读取日志筛选和显示使用的违规原因字典。
+ public static async Task> GetViolationReasons()
+ {
+ using (var web = new System.Net.WebClient { Encoding = Encoding.UTF8 })
+ {
+ var html = await web.DownloadStringTaskAsync(ServerUrl + "/api/CloudGoods/GetViolationReasons");
+ var ret = JsonConvert.DeserializeObject>>(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; }
+ }
+
+ /// 按日志编号倒序查询,下一页传入上一页最后一条日志的 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 };
+ var html = await GetHtml.PostJsonAsync(ServerUrl + "/api/CloudGoods/GetViolationLogs", request, timeOut: 5);
+ var ret = JsonConvert.DeserializeObject>>(html)
+ ?? throw new Exception("查询违规日志返回为空。");
+ if (ret.Code != 200)
+ throw new Exception(ret.Msg);
+ return ret.Data ?? throw new Exception("查询违规日志返回数据为空。");
+ }
+
#region user
///
/// 搜索收藏
@@ -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();
+ 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; }
+ }
+
///
/// 违规商品上报项。
///
@@ -437,6 +598,9 @@ namespace Work.Services
///
public class GoodsViolationStatus
{
+ [JsonProperty("reasonIds")]
+ public int[] ReasonIds { get; set; }
+
[JsonProperty("goodsId")]
public long GoodsId { get; set; }