Files
KJD/Work/Services/CloudGoodsService.cs
T
2026-10-30 18:44:39 +08:00

646 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 云商品数据服务
/// </summary>
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<PageData<CloudGoods>> GetGoods(GoodsSearchRequest request)
{
var html = await GetHtml.PostJsonAsync(ServerUrl + "/api/CloudGoods/SearchGoods", request);
var ret = JsonConvert.DeserializeObject<Result<PageData<CloudGoods>>>(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<Result<string>>(html);
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data;
}
/// <summary>
/// 在后台解析微信处罚 JSON、匹配店铺商品编码并上报,异常在内部记录。
/// </summary>
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<List<ReportGoodsViolationResult>> 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<ReportGoodsViolationResult>();
var items = new List<ReportGoodsViolationItem>();
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<ReportGoodsViolationResult>();
}
var productCodes = GetWxProductCodes(shop, items.Select(item => item.GoodsId));
var mappedItems = new List<ReportGoodsViolationItem>();
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<ReportGoodsViolationResult>() : 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;
}
/// <summary>
/// 后台检查当前店铺所有在售商品,云端确认违规后立即下架;异常仅记录日志。
/// </summary>
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<long, long>();
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<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();
// 同一云商品可能对应多个微信商品,每次下架最多 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<long, string> GetWxProductCodes(ShopLoginInfo shop, IEnumerable<long> productIds = null, int status = -1)
{
if (shop == null)
{
throw new ArgumentNullException(nameof(shop));
}
var codes = new Dictionary<long, string>();
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("微信商品列表分页未返回新商品,无法获取全部搜索结果。");
}
}
}
/// <summary>
/// 批量上报违规商品。
/// </summary>
public static async Task<List<ReportGoodsViolationResult>> ReportViolations(List<ReportGoodsViolationItem> 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<Result<List<ReportGoodsViolationResult>>>(html);
if (ret == null)
{
throw new Exception("批量上报违规商品返回为空。");
}
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data ?? throw new Exception("批量上报违规商品返回数据为空。");
}
/// <summary>
/// 批量查询商品违规状态。
/// </summary>
public static List<GoodsViolationStatus> GetViolationStatus(List<long> goodsIds)
{
if (goodsIds == null)
{
throw new ArgumentNullException(nameof(goodsIds));
}
var html = GetHtml.PostJson(ServerUrl + "/api/CloudGoods/GetViolationStatus", goodsIds, timeOut: 5);
var ret = JsonConvert.DeserializeObject<Result<List<GoodsViolationStatus>>>(html);
if (ret == null)
{
throw new Exception("批量查询商品违规状态返回为空。");
}
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data ?? throw new Exception("批量查询商品违规状态返回数据为空。");
}
#region user
/// <summary>
/// 搜索收藏
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<PageData<CloudGoods>> SearchFavorites(UserFavoritesGoodsSearchRequest request)
{
request.UserName = GetUserName();
var html = await GetHtml.PostJsonAsync(ServerUrl + "/api/CloudGoods/SearchFavorites", request);
var ret = JsonConvert.DeserializeObject<Result<PageData<CloudGoods>>>(html);
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data;
}
/// <summary>
/// 收藏
/// </summary>
/// <param name="goodsIds"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<int> Favorite(List<long> goodsIds)
{
var html = await GetHtml.PostJsonAsync(ServerUrl + $"/api/CloudGoods/AddFavorite", new
{
UserName = GetUserName(),
GoodsIds = goodsIds,
});
var ret = JsonConvert.DeserializeObject<Result<int>>(html);
if (ret.Code != 200)
{
throw new Exception(ret.Msg);
}
return ret.Data;
}
/// <summary>
/// 取消收藏
/// </summary>
/// <param name="goodsIds"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<int> RemoveFavorites(List<long> goodsIds)
{
var html = await GetHtml.PostJsonAsync(ServerUrl + $"/api/CloudGoods/RemoveFavorites", new
{
UserName = GetUserName(),
GoodsIds = goodsIds,
});
var ret = JsonConvert.DeserializeObject<Result<int>>(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;
}
}
/// <summary>
/// 违规商品上报项。
/// </summary>
public class ReportGoodsViolationItem
{
[JsonProperty("goodsId")]
public long GoodsId { get; set; }
[JsonProperty("reasons")]
public string Reasons { get; set; } = string.Empty;
}
/// <summary>
/// 单个商品的违规上报结果。
/// </summary>
public class ReportGoodsViolationResult
{
[JsonProperty("goodsId")]
public long GoodsId { get; set; }
[JsonProperty("isOffShelf")]
public bool IsOffShelf { get; set; }
}
/// <summary>
/// 商品违规状态。
/// </summary>
public class GoodsViolationStatus
{
[JsonProperty("goodsId")]
public long GoodsId { get; set; }
[JsonProperty("isViolation")]
public bool IsViolation { get; set; }
}
/// <summary>
/// 商品搜索请求
/// </summary>
public class GoodsSearchRequest
{
/// <summary>
/// 标题(模糊搜索)
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 最低价格(元)
/// </summary>
public long? MinPrice { get; set; }
/// <summary>
/// 最高价格(元)
/// </summary>
public long? MaxPrice { get; set; }
/// <summary>
/// 一级类目ID
/// </summary>
public int? CatId1 { get; set; }
/// <summary>
/// 二级类目ID
/// </summary>
public int? CatId2 { get; set; }
/// <summary>
/// 三级类目ID
/// </summary>
public int? CatId3 { get; set; }
/// <summary>
/// 四级类目ID
/// </summary>
public int? CatId4 { get; set; }
/// <summary>
/// 最低销量
/// </summary>
public int? MinSoldQuantity { get; set; }
/// <summary>
/// 最高销量
/// </summary>
public int? MaxSoldQuantity { get; set; }
/// <summary>
/// 平台类型
/// </summary>
public PlatformType Platform { get; set; }
/// <summary>
/// 页码(默认1)
/// </summary>
public int PageIndex { get; set; } = 1;
/// <summary>
/// 每页数量(默认20)
/// </summary>
public int PageSize { get; set; } = 20;
}
public class UserFavoritesGoodsSearchRequest : GoodsSearchRequest
{
public string UserName { get; set; }
}
/// <summary>
/// 商品搜索返回结果
/// </summary>
public class GoodsSearchResponse
{
/// <summary>
/// 商品列表
/// </summary>
[JsonProperty("list")]
public List<CloudGoods> List { get; set; } = new List<CloudGoods>();
/// <summary>
/// 总记录数
/// </summary>
[JsonProperty("total")]
public int Total { get; set; }
/// <summary>
/// 当前页码
/// </summary>
[JsonProperty("pageIndex")]
public int PageIndex { get; set; }
/// <summary>
/// 每页数量
/// </summary>
[JsonProperty("pageSize")]
public int PageSize { get; set; }
/// <summary>
/// 总页数
/// </summary>
[JsonProperty("totalPages")]
public int TotalPages { get; set; }
}
public class CloudGoods : BindableBase
{
public bool IsSelected
{
get => GetValue<bool>();
set => SetValue(value);
}
/// <summary>
/// 自增ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 平台类型
/// </summary>
public byte Platform { get; set; }
/// <summary>
/// 商品编号
/// </summary>
public long GoodsId { get; set; }
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 缩略图
/// </summary>
public string? ThumbUrl { get; set; }
/// <summary>
/// 一级类目ID
/// </summary>
public int? CatId1 { get; set; }
/// <summary>
/// 二级类目ID
/// </summary>
public int? CatId2 { get; set; }
/// <summary>
/// 三级类目ID
/// </summary>
public int? CatId3 { get; set; }
/// <summary>
/// 四级类目ID
/// </summary>
public int? CatId4 { get; set; }
/// <summary>
/// 最低价快照(元),导入时计算
/// </summary>
public decimal MinPrice { get; set; }
/// <summary>
/// 最高价快照(元),导入时计算
/// </summary>
public decimal MaxPrice { get; set; }
public string Price => $"{MinPrice / 100m:N2}";
/// <summary>
/// 销量
/// </summary>
public int SoldQuantity { get; set; }
/// <summary>
/// 状态:0正常 1已删除
/// </summary>
public byte Status { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedTime { get; set; }
/// <summary>
/// 更新时间(自动更新)
/// </summary>
public DateTime UpdatedTime { get; set; }
/// <summary>
/// 索引同步状态:0未同步 1已同步
/// </summary>
public byte IndexStatus { get; set; }
}
}