This commit is contained in:
wule committed 2026-11-02 18:10:32 +08:00
commit f9a1af6073
8 files changed
+884 -18

No files matched your search

+4 -1
View File
@@ -10,7 +10,7 @@
<ErrorReportUrlHistory />
<FallbackCulture>zh-CN</FallbackCulture>
<VerifyUploadedFiles>false</VerifyUploadedFiles>
<ActiveDebugProfile>Production</ActiveDebugProfile>
<ActiveDebugProfile>Development</ActiveDebugProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
@@ -182,6 +182,9 @@
<Compile Update="WXForms\ProductForms\WXFinderSquareForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="WXForms\ProductForms\WXViolationLogsForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="WXForms\ShopForms\WXOemPointsTopUpListForm.cs">
<SubType>Form</SubType>
</Compile>
@@ -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;
@@ -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
}
}
/// <summary>
/// 图片宽高均达到指定尺寸时返回 true;尺寸不足或无效输入返回 false。
/// </summary>
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;
@@ -35,9 +35,11 @@ namespace TemuKJD.WXForms.ProductForms
public Lazy<WXProdNoCheckEditAddressForm> prodNoCheckEditForm = new Lazy<WXProdNoCheckEditAddressForm>();
public Lazy<CloudGoodsForm> cloudgoodsForm = new Lazy<CloudGoodsForm>();
public Lazy<CloudGoodsFavoriteForm> cloudgoodsFavoriteForm = new Lazy<CloudGoodsFavoriteForm>();
private readonly Lazy<WXViolationLogsForm> violationLogsForm = new Lazy<WXViolationLogsForm>();
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;
@@ -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;
}
}
@@ -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<GoodsViolationLogEntity> rows = new BindingList<GoodsViolationLogEntity>();
private Dictionary<int, string> reasons;
private Dictionary<string, string> shops;
private const int PageSize = 50;
private readonly List<long> pageCursors = new List<long> { 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<string, string>("", "全部店铺") }.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<int, string>(0, "全部类型") }
.Concat(types.Where(type => type.IsViolation).Select(type => new KeyValuePair<int, string>(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<GoodsViolationLogEntity>();
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;
}
}
}
}
}
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+175 -11
View File
@@ -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<ReportGoodsViolationResult>() : await ReportViolations(reports);
var results = new List<ReportGoodsViolationResult>();
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<ReportGoodsViolationItem> batch)
{
if (!GlobalConfig.GlobalSettings.AutoReportViolationProducts) return;
Dictionary<string, int> 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<RecordViolationLogRequest>(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<long>(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("批量查询商品违规状态返回数据为空。");
}
/// <summary>逐原因记录店铺动作。日志请求失败不能改变剔除决定或中断后续下架。</summary>
public static Task RecordShopActionLogs(string shopId, GoodsViolationStatus status, ViolationLogAction action)
=> RecordShopActionLogs(shopId, new[] { status }, action);
private static async Task RecordShopActionLogs(string shopId, IEnumerable<GoodsViolationStatus> statuses, ViolationLogAction action)
{
if (!(action == ViolationLogAction.AutoListingExcluded && GlobalConfig.GlobalSettings.AutoExcludeViolationProductsOnUpload
|| action == ViolationLogAction.AutoOffShelf && GlobalConfig.GlobalSettings.AutoDelistViolationProducts))
return;
var logs = new List<RecordViolationLogRequest>();
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<RecordViolationLogRequest> 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}");
}
}
/// <summary>记录店铺已完成的违规上报、上货剔除或下架动作。</summary>
public static Task RecordViolationLog(RecordViolationLogRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
return RecordViolationLogs(new[] { request });
}
public static async Task RecordViolationLogs(IReadOnlyList<RecordViolationLogRequest> 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<Result>(html) ?? throw new Exception("记录违规日志返回为空。");
if (ret.Code != 200)
throw new Exception(ret.Msg);
}
/// <summary>读取日志筛选和显示使用的违规原因字典。</summary>
public static async Task<List<ViolationReason>> GetViolationReasons()
{
using (var web = new System.Net.WebClient { Encoding = Encoding.UTF8 })
{
var html = await web.DownloadStringTaskAsync(ServerUrl + "/api/CloudGoods/GetViolationReasons");
var ret = JsonConvert.DeserializeObject<Result<List<ViolationReason>>>(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; }
}
/// <summary>按日志编号倒序查询,下一页传入上一页最后一条日志的 Id。</summary>
public static async Task<List<GoodsViolationLogEntity>> 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<Result<List<GoodsViolationLogEntity>>>(html)
?? throw new Exception("查询违规日志返回为空。");
if (ret.Code != 200)
throw new Exception(ret.Msg);
return ret.Data ?? throw new Exception("查询违规日志返回数据为空。");
}
#region user
/// <summary>
/// 搜索收藏
@@ -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<string>();
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; }
}
/// <summary>
/// 违规商品上报项。
/// </summary>
@@ -437,6 +598,9 @@ namespace Work.Services
/// </summary>
public class GoodsViolationStatus
{
[JsonProperty("reasonIds")]
public int[] ReasonIds { get; set; }
[JsonProperty("goodsId")]
public long GoodsId { get; set; }