using AllBase;
using AllBase.Helper;
using AllBase.Model.Result;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Work.Api.Wx;
namespace Work.Services
{
///
/// 云商品数据服务
///
public class CloudGoodsService
{
private static string ServerUrl = "http://103.90.155.78:9001";
static CloudGoodsService()
{
if (EnvironmentHelper.IsDevelopment)
{
ServerUrl = "http://localhost:14312";
}
}
public static async Task> GetGoods(GoodsSearchRequest request)
{
var html = await GetHtml.PostJsonAsync(ServerUrl + "/api/CloudGoods/SearchGoods", request);
var ret = JsonConvert.DeserializeObject>>(html);
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data;
}
public static string GetGoodsDetail(long goodsId)
{
var html = GetHtml.PostJson(ServerUrl + $"/api/CloudGoods/GetGoodsRawData?goodsId={goodsId}", "");
var ret = JsonConvert.DeserializeObject>(html);
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data;
}
///
/// 在后台解析微信处罚 JSON、匹配店铺商品编码并上报,异常在内部记录。
///
public static void ReportViolationsFromJson(string json, ShopLoginInfo shop)
{
_ = Task.Run(async () =>
{
try
{
await ReportViolationsFromJsonCore(json, shop).ConfigureAwait(false);
}
catch (Exception ex)
{
try
{
LogSave.AddFile("解析并上报违规商品失败:" + ex.Message);
}
catch
{
// 日志失败也不能影响调用方。
}
}
});
}
private static async Task> ReportViolationsFromJsonCore(string json, ShopLoginInfo shop)
{
if (string.IsNullOrWhiteSpace(json))
{
throw new ArgumentException("处罚 JSON 文本不能为空。", nameof(json));
}
JObject root;
try
{
root = JObject.Parse(json);
}
catch (JsonException ex)
{
throw new ArgumentException("处罚 JSON 格式不正确。", nameof(json), ex);
}
var record = root["record"] as JObject;
if (root["code"]?.ToString() != "0" || record?["code"]?.ToString() != "0")
{
throw new ArgumentException("处罚数据的 code 或 record.code 不为 0,或缺少 record。", nameof(json));
}
if (!(record["items"] is JArray records))
{
throw new ArgumentException("处罚数据缺少 record.items 数组。", nameof(json));
}
if (records.Count == 0)
return new List();
var items = new List();
foreach (var token in records)
{
if (!(token is JObject punishment))
{
throw new ArgumentException("record.items 中的处罚记录必须是对象。", nameof(json));
}
var objects = punishment["punishObject"];
if (objects == null || objects.Type == JTokenType.Null)
{
continue;
}
if (!(objects is JArray products))
{
throw new ArgumentException("punishObject 必须是数组。", nameof(json));
}
foreach (var product in products)
{
if (!(product is JObject target))
{
throw new ArgumentException("punishObject 中的处罚对象必须是对象。", nameof(json));
}
var id = target["productId"];
// 订单等非商品处罚对象不包含 productId。
if (id == null)
{
continue;
}
if ((id.Type != JTokenType.String && id.Type != JTokenType.Integer)
|| !long.TryParse(id.ToString(), out var goodsId) || goodsId <= 0)
{
throw new ArgumentException("商品 productId 必须是有效的正整数编号。", nameof(json));
}
var reason = punishment["punishReason"];
if (reason?.Type != JTokenType.String || string.IsNullOrWhiteSpace((string)reason))
{
throw new ArgumentException($"商品 {goodsId} 缺少有效的 punishReason。", nameof(json));
}
items.Add(new ReportGoodsViolationItem { GoodsId = goodsId, Reasons = ((string)reason).Trim() });
}
}
if (items.Count == 0)
{
return new List();
}
var productCodes = GetWxProductCodes(shop, items.Select(item => item.GoodsId));
var mappedItems = new List();
foreach (var item in items)
{
if (!productCodes.TryGetValue(item.GoodsId, out var code) || string.IsNullOrWhiteSpace(code))
{
LogSave.AddFile($"跳过违规商品 {item.GoodsId}:未找到对应的 spuCode。");
continue;
}
if (!TryGetGoodsId(code, out var goodsId))
{
LogSave.AddFile($"跳过违规商品 {item.GoodsId}:spuCode 无法转换为有效商品编号。");
continue;
}
mappedItems.Add(new ReportGoodsViolationItem { GoodsId = goodsId, Reasons = item.Reasons });
}
var reports = mappedItems.GroupBy(item => item.GoodsId).Select(group => new ReportGoodsViolationItem
{
GoodsId = group.Key,
Reasons = string.Join(";", group.Select(item => item.Reasons).Distinct())
}).ToList();
return reports.Count == 0
? new List() : await ReportViolations(reports);
}
private static bool TryGetGoodsId(string code, out long goodsId)
{
code = code?.Trim() ?? string.Empty;
if (code.StartsWith("P1-", StringComparison.Ordinal))
{
code = code.Substring(3);
}
else if (code.StartsWith("EC1", StringComparison.Ordinal) && code.EndsWith("A", StringComparison.Ordinal))
{
code = code.Substring(3, code.Length - 4);
}
return long.TryParse(code, out goodsId) && goodsId > 0;
}
///
/// 后台检查当前店铺所有在售商品,云端确认违规后立即下架;异常仅记录日志。
///
public static void CheckAndOffShelfViolations(ShopLoginInfo shop)
{
_ = Task.Run(async () =>
{
try
{
await CheckAndOffShelfViolationsCore(shop).ConfigureAwait(false);
}
catch (Exception ex)
{
try
{
LogSave.AddFile($"店铺 {shop?.ShopId} 在售商品违规检查失败:{ex.Message}");
}
catch { }
}
});
}
private static async Task CheckAndOffShelfViolationsCore(ShopLoginInfo shop)
{
// 先读取完整列表,避免下架改变分页位置而漏查后面的商品。
var products = GetWxProductCodes(shop, status: 5);
var mappings = new Dictionary();
foreach (var product in products)
{
if (TryGetGoodsId(product.Value, out var goodsId))
{
mappings.Add(product.Key, goodsId);
}
else
{
LogSave.AddFile($"店铺 {shop.ShopId} 跳过商品 {product.Key}:spuCode 无法转换为有效商品编号。");
}
}
var goodsIds = mappings.Values.Distinct().ToList();
for (var offset = 0; offset < goodsIds.Count; offset += 50)
{
var batch = goodsIds.Skip(offset).Take(50).ToList();
var statuses = 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();
// 同一云商品可能对应多个微信商品,每次下架最多 50 个。
for (var index = 0; index < productIds.Count; index += 50)
{
var ids = string.Join(",", productIds.Skip(index).Take(50));
var response = JObject.Parse(WxProductApi.DownProd(shop, ids));
if (response["code"]?.ToString() != "0")
{
throw new Exception($"微信商品 {ids} 下架失败:{response}");
}
LogSave.AddFile($"店铺 {shop.ShopId} 云端检测违规,已下架微信商品:{ids}");
}
}
}
private static Dictionary GetWxProductCodes(ShopLoginInfo shop, IEnumerable productIds = null, int status = -1)
{
if (shop == null)
{
throw new ArgumentNullException(nameof(shop));
}
var codes = new Dictionary();
var prodIds = productIds == null ? null : string.Join(",", productIds.Distinct());
long received = 0;
for (var page = 1; ; page++)
{
var response = JObject.Parse(WxProductApi.SearchProduct(shop, page, pageSize: 50, prodIds: prodIds, status: status));
if (response["ret"]?.ToString() != "0" || response["success"]?.Type != JTokenType.Boolean
|| !(bool)response["success"] || !(response["productList"] is JArray products)
|| !long.TryParse(response["totalNum"]?.ToString(), out var total) || total < 0)
{
throw new Exception($"获取微信商品列表第 {page} 页失败。");
}
var previousCount = codes.Count;
foreach (var product in products)
{
if (!(product is JObject item) || !long.TryParse(item["productId"]?.ToString(), out var id) || id <= 0)
{
throw new Exception("微信商品列表包含无效的 productId。");
}
codes[id] = item["spuCode"]?.Type == JTokenType.String ? (string)item["spuCode"] : null;
}
received += products.Count;
if (received >= total)
{
return codes;
}
if (codes.Count == previousCount)
{
throw new Exception("微信商品列表分页未返回新商品,无法获取全部搜索结果。");
}
}
}
///
/// 批量上报违规商品。
///
public static async Task> ReportViolations(List items)
{
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
if (items.Any(item => item == null))
{
throw new ArgumentException("违规商品列表不能包含空项。", nameof(items));
}
var html = await GetHtml.PostJsonAsync(ServerUrl + "/api/CloudGoods/ReportViolations", items, timeOut: 5);
var ret = JsonConvert.DeserializeObject>>(html);
if (ret == null)
{
throw new Exception("批量上报违规商品返回为空。");
}
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data ?? throw new Exception("批量上报违规商品返回数据为空。");
}
///
/// 批量查询商品违规状态。
///
public static List GetViolationStatus(List goodsIds)
{
if (goodsIds == null)
{
throw new ArgumentNullException(nameof(goodsIds));
}
var html = GetHtml.PostJson(ServerUrl + "/api/CloudGoods/GetViolationStatus", goodsIds, timeOut: 5);
var ret = JsonConvert.DeserializeObject>>(html);
if (ret == null)
{
throw new Exception("批量查询商品违规状态返回为空。");
}
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data ?? throw new Exception("批量查询商品违规状态返回数据为空。");
}
#region user
///
/// 搜索收藏
///
///
///
///
public static async Task> SearchFavorites(UserFavoritesGoodsSearchRequest request)
{
request.UserName = GetUserName();
var html = await GetHtml.PostJsonAsync(ServerUrl + "/api/CloudGoods/SearchFavorites", request);
var ret = JsonConvert.DeserializeObject>>(html);
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data;
}
///
/// 收藏
///
///
///
///
public static async Task Favorite(List goodsIds)
{
var html = await GetHtml.PostJsonAsync(ServerUrl + $"/api/CloudGoods/AddFavorite", new
{
UserName = GetUserName(),
GoodsIds = goodsIds,
});
var ret = JsonConvert.DeserializeObject>(html);
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data;
}
///
/// 取消收藏
///
///
///
///
public static async Task RemoveFavorites(List goodsIds)
{
var html = await GetHtml.PostJsonAsync(ServerUrl + $"/api/CloudGoods/RemoveFavorites", new
{
UserName = GetUserName(),
GoodsIds = goodsIds,
});
var ret = JsonConvert.DeserializeObject>(html);
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data;
}
#endregion
public static string GetUserName()
{
return string.IsNullOrEmpty(BaseClass.ParentUserName) ? BaseClass.UserName : BaseClass.ParentUserName;
}
}
///
/// 违规商品上报项。
///
public class ReportGoodsViolationItem
{
[JsonProperty("goodsId")]
public long GoodsId { get; set; }
[JsonProperty("reasons")]
public string Reasons { get; set; } = string.Empty;
}
///
/// 单个商品的违规上报结果。
///
public class ReportGoodsViolationResult
{
[JsonProperty("goodsId")]
public long GoodsId { get; set; }
[JsonProperty("isOffShelf")]
public bool IsOffShelf { get; set; }
}
///
/// 商品违规状态。
///
public class GoodsViolationStatus
{
[JsonProperty("goodsId")]
public long GoodsId { get; set; }
[JsonProperty("isViolation")]
public bool IsViolation { get; set; }
}
///
/// 商品搜索请求
///
public class GoodsSearchRequest
{
///
/// 标题(模糊搜索)
///
public string Title { get; set; } = string.Empty;
///
/// 最低价格(元)
///
public long? MinPrice { get; set; }
///
/// 最高价格(元)
///
public long? MaxPrice { get; set; }
///
/// 一级类目ID
///
public int? CatId1 { get; set; }
///
/// 二级类目ID
///
public int? CatId2 { get; set; }
///
/// 三级类目ID
///
public int? CatId3 { get; set; }
///
/// 四级类目ID
///
public int? CatId4 { get; set; }
///
/// 最低销量
///
public int? MinSoldQuantity { get; set; }
///
/// 最高销量
///
public int? MaxSoldQuantity { get; set; }
///
/// 平台类型
///
public PlatformType Platform { get; set; }
///
/// 页码(默认1)
///
public int PageIndex { get; set; } = 1;
///
/// 每页数量(默认20)
///
public int PageSize { get; set; } = 20;
}
public class UserFavoritesGoodsSearchRequest : GoodsSearchRequest
{
public string UserName { get; set; }
}
///
/// 商品搜索返回结果
///
public class GoodsSearchResponse
{
///
/// 商品列表
///
[JsonProperty("list")]
public List List { get; set; } = new List();
///
/// 总记录数
///
[JsonProperty("total")]
public int Total { get; set; }
///
/// 当前页码
///
[JsonProperty("pageIndex")]
public int PageIndex { get; set; }
///
/// 每页数量
///
[JsonProperty("pageSize")]
public int PageSize { get; set; }
///
/// 总页数
///
[JsonProperty("totalPages")]
public int TotalPages { get; set; }
}
public class CloudGoods : BindableBase
{
public bool IsSelected
{
get => GetValue();
set => SetValue(value);
}
///
/// 自增ID
///
public long Id { get; set; }
///
/// 平台类型
///
public byte Platform { get; set; }
///
/// 商品编号
///
public long GoodsId { get; set; }
///
/// 标题
///
public string Title { get; set; } = string.Empty;
///
/// 描述
///
public string? Description { get; set; }
///
/// 缩略图
///
public string? ThumbUrl { get; set; }
///
/// 一级类目ID
///
public int? CatId1 { get; set; }
///
/// 二级类目ID
///
public int? CatId2 { get; set; }
///
/// 三级类目ID
///
public int? CatId3 { get; set; }
///
/// 四级类目ID
///
public int? CatId4 { get; set; }
///
/// 最低价快照(元),导入时计算
///
public decimal MinPrice { get; set; }
///
/// 最高价快照(元),导入时计算
///
public decimal MaxPrice { get; set; }
public string Price => $"{MinPrice / 100m:N2}";
///
/// 销量
///
public int SoldQuantity { get; set; }
///
/// 状态:0正常 1已删除
///
public byte Status { get; set; }
///
/// 创建时间
///
public DateTime CreatedTime { get; set; }
///
/// 更新时间(自动更新)
///
public DateTime UpdatedTime { get; set; }
///
/// 索引同步状态:0未同步 1已同步
///
public byte IndexStatus { get; set; }
}
}