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; }