Files
StoreWeb/Plugins/CloudProduct/ImportConverters/WXGoodsConverter.cs
T

586 lines
16 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 MAX.Core.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Org.BouncyCastle.Asn1.Cms;
using Store.Framework.Models;
using Store.Modules.CloudProduct.Entities;
using Store.Modules.CloudProduct.ImportConverters;
using System;
using System.Collections.Generic;
using System.Net.ServerSentEvents;
using System.Text;
using System.Text.Json.Nodes;
namespace Store.CloudProduct.ImportConverters
{
internal class WXGoodsConverter : GoodsConverter
{
public override string ConverterName => nameof(WXGoodsConverter);
public override PlatformType Platform => PlatformType.WX;
private JObject _jsonRoot;
private JObject _productNode; // detail.product
private JObject _infoNode; // detail.product.info
private JArray _skuList; // detail.product.info.sku_list.sku_info(价格兜底)
private JArray _productSkus; // detail.product.product_skus(完整SKU)
private JArray _saleAttrs; // detail.product.info.product_sale_attrs
protected override bool IsMatchCore()
{
try
{
_jsonRoot = JsonConvert.DeserializeObject<JObject>(RawData);
if (_jsonRoot == null) return false;
var detail = _jsonRoot.SelectToken("$.detail") as JObject;
if (detail == null) return false;
var product = detail.SelectToken("$.product") as JObject;
if (product == null) return false;
var productKey = product.SelectToken("$.product_key") as JObject;
if (productKey == null) return false;
var productId = productKey.SelectToken("$.product_id")?.ToString();
if (string.IsNullOrEmpty(productId)) return false;
var info = product.SelectToken("$.info") as JObject;
if (info == null) return false;
var title = info.SelectToken("$.title")?.ToString();
if (string.IsNullOrEmpty(title)) return false;
var productSkus = product.SelectToken("$.product_skus") as JArray;
var skuList = info.SelectToken("$.sku_list.sku_info") as JArray;
if ((productSkus == null || productSkus.Count == 0) &&
(skuList == null || skuList.Count == 0))
return false;
_productNode = product;
_infoNode = info;
_productSkus = productSkus;
_skuList = skuList;
_saleAttrs = info.SelectToken("$.product_sale_attrs") as JArray;
return true;
}
catch
{
return false;
}
}
protected override GoodsEntity ConvertCore()
{
var product = ConverterProvider();
var item = new
GoodsEntity
{
Platform = (byte)PlatformType.WX,
GoodsId = product.ProductId.ToLong(0),
Title = product.Title ?? string.Empty,
Description = product.Description,
ThumbUrl = product.ThumbUrl,
SoldQuantity = GetTotalSold(),
Status = 0,
};
item.MinPrice = ((long)product.Skus.Min(a=>a.Price));
item.MaxPrice = ((long)product.Skus.Max(a => a.Price));
item.CatId1 = product.CategoryIds.Count > 0 ? product.CategoryIds[0] : (int?)null;
item.CatId2 = product.CategoryIds.Count > 1 ? product.CategoryIds[1] : (int?)null;
item.CatId3 = product.CategoryIds.Count > 2 ? product.CategoryIds[2] : (int?)null;
item.CatId4 = product.CategoryIds.Count > 3 ? product.CategoryIds[3] : (int?)null;
return item;
}
private Product ConverterProvider()
{
var product = new Product
{
Platform = Platform,
ProductId = ParseGoodsId()
};
// 1. 基本信息
product.Title = GetStringValue(_infoNode, "title");
product.Description = GetStringValue(_infoNode, "short_title");
if (string.IsNullOrEmpty(product.Description))
product.Description = GetStringValue(_infoNode, "ai_gen_short_title");
// 2. 品牌(新模型无 BrandId,只有 Brand 名称)
var brandId = GetLongValue(_infoNode, "brand_id");
product.Brand = brandId > 0 ? brandId.ToString() : string.Empty;
// 3. 类目
product.CategoryId = GetCategoryId();
product.CategoryIds = GetCategoryPath();
// 4. 主图
product.MainImages = GetMainImages();
// 5. 详情图
product.DetailImages = GetDetailImages();
// 6. 属性
product.Attributes = GetProperties();
// 7. 规格 + SKU
var skuResult = GetSkusAndSpecs();
product.Skus = skuResult.Item1;
product.Specs = skuResult.Item2;
// 8. 媒体与视频(新模型只有单个 Video)
product.ThumbUrl = product.MainImages.FirstOrDefault() ?? string.Empty;
product.Video = GetVideoUrls().FirstOrDefault() ?? string.Empty;
return product;
}
#region 核心字段解析
private string ParseGoodsId()
{
var productId = _productNode.SelectToken("$.product_key.product_id")?.ToString();
if (!string.IsNullOrEmpty(productId))
return productId;
return "";
}
private int GetCategoryId()
{
var category = _infoNode["category"] as JArray;
if (category == null || category.Count == 0)
return 0;
var lastCat = category.Last as JObject;
return lastCat?["cat_id"]?.Value<int>() ?? 0;
}
private List<int> GetCategoryPath()
{
var result = new List<int>();
var category = _infoNode["category"] as JArray;
if (category == null) return result;
foreach (var cat in category)
{
var catId = cat["cat_id"]?.Value<int>();
if (catId.HasValue && catId.Value > 0)
result.Add(catId.Value);
}
return result;
}
private List<string> GetMainImages()
{
var result = new List<string>();
var headImg = _infoNode["head_img"] as JArray;
if (headImg != null && headImg.Count > 0)
{
foreach (var img in headImg)
{
var url = img?.Value<string>();
if (!string.IsNullOrEmpty(url))
result.Add(url);
}
return result;
}
var headImgInfos = _infoNode["head_img_infos"] as JArray;
if (headImgInfos != null)
{
foreach (var img in headImgInfos)
{
var url = img["img_url"]?.Value<string>();
if (!string.IsNullOrEmpty(url))
result.Add(url);
}
}
return result.Distinct().ToList();
}
private List<string> GetDetailImages()
{
var result = new List<string>();
var detail = _infoNode["detail"] as JObject;
if (detail == null) return result;
var detailImg = detail["detail_img"] as JArray;
if (detailImg == null) return result;
foreach (var img in detailImg)
{
var url = img?.Value<string>();
if (!string.IsNullOrEmpty(url))
result.Add(url);
}
return result;
}
/// <summary>
/// 属性转换为新模型结构:多个同名key的value合并到 Values 列表
/// </summary>
private List<ProductAttribute> GetProperties()
{
var result = new List<ProductAttribute>();
var param = _infoNode["param"] as JArray;
if (param == null) return result;
foreach (var item in param)
{
var categorys = item["categorys"] as JArray;
if (categorys == null || categorys.Count < 2) continue;
var key = categorys[0]?["name"]?.Value<string>();
var value = categorys[1]?["name"]?.Value<string>();
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) continue;
var existing = result.FirstOrDefault(p => p.Name == key);
if (existing == null)
{
result.Add(new ProductAttribute
{
Name = key,
Values = new List<string> { value }
});
}
else if (!existing.Values.Contains(value))
{
existing.Values.Add(value);
}
}
return result;
}
private List<string> GetVideoUrls()
{
var result = new List<string>();
var headVideoInfo = _infoNode["head_video_info"] as JArray;
if (headVideoInfo == null) return result;
foreach (var video in headVideoInfo)
{
var url = video["video_url"]?.Value<string>();
if (!string.IsNullOrEmpty(url))
result.Add(url);
}
return result;
}
#endregion
#region SKU和规格解析
private Tuple<List<ProductSku>, List<ProductSpec>> GetSkusAndSpecs()
{
var skuList = new List<ProductSku>();
var specList = new List<ProductSpec>();
var priceFallbackDict = BuildPriceFallbackDict();
var attrFallbackDict = BuildAttrFallbackDict();
if (_productSkus != null && _productSkus.Count > 0)
{
foreach (var sku in _productSkus)
{
var skuObj = sku as JObject;
if (skuObj == null) continue;
var skuItem = ParseProductSku(skuObj, priceFallbackDict, attrFallbackDict);
if (skuItem != null)
skuList.Add(skuItem);
}
}
else if (_skuList != null && _skuList.Count > 0)
{
foreach (var sku in _skuList)
{
var skuObj = sku as JObject;
if (skuObj == null) continue;
var skuItem = ParseSkuInfoOnly(skuObj, attrFallbackDict);
if (skuItem != null)
skuList.Add(skuItem);
}
}
BuildSpecList(specList, skuList);
return new Tuple<List<ProductSku>, List<ProductSpec>>(skuList, specList);
}
private Dictionary<string, decimal> BuildPriceFallbackDict()
{
var dict = new Dictionary<string, decimal>();
if (_skuList == null) return dict;
foreach (var sku in _skuList)
{
var skuObj = sku as JObject;
if (skuObj == null) continue;
var skuId = skuObj["sku_id"]?.Value<string>();
if (string.IsNullOrEmpty(skuId)) continue;
var estimatePrice = skuObj["estimate_price"]?.Value<decimal>() ?? 0;
var price = skuObj["price"]?.Value<decimal>() ?? 0;
dict[skuId] = estimatePrice > 0 ? estimatePrice : price;
}
return dict;
}
/// <summary>
/// skuId -> 结构化的规格列表
/// </summary>
private Dictionary<string, List<ProductSkuSpec>> BuildAttrFallbackDict()
{
var dict = new Dictionary<string, List<ProductSkuSpec>>();
if (_saleAttrs == null) return dict;
foreach (var attr in _saleAttrs)
{
var key = attr["key"]?.Value<string>() ?? string.Empty;
var items = attr["items"] as JArray;
if (items == null) continue;
foreach (var item in items)
{
var value = item["value_v2"]?.Value<string>();
if (string.IsNullOrEmpty(value))
value = item["value"]?.Value<string>();
if (string.IsNullOrEmpty(value)) continue;
var skuIds = item["sku_ids"] as JArray;
if (skuIds == null) continue;
foreach (var skuIdToken in skuIds)
{
var id = skuIdToken?.Value<string>();
if (string.IsNullOrEmpty(id)) continue;
if (!dict.ContainsKey(id))
dict[id] = new List<ProductSkuSpec>();
dict[id].Add(new ProductSkuSpec { Name = key, Value = value });
}
}
}
return dict;
}
private ProductSku ParseProductSku(
JObject skuObj,
Dictionary<string, decimal> priceFallbackDict,
Dictionary<string, List<ProductSkuSpec>> attrFallbackDict)
{
var skuKey = skuObj["sku_key"] as JObject;
var skuInfo = skuObj["product_sku_info"] as JObject;
if (skuKey == null || skuInfo == null) return null;
var skuId = skuKey["sku_id"]?.Value<string>() ?? string.Empty;
var outSkuId = skuKey["out_sku_id"]?.Value<string>() ?? string.Empty;
if (string.IsNullOrEmpty(skuId)) return null;
var skuSpecs = ParseSkuSpecs(skuInfo, skuId, attrFallbackDict);
var salePrice = skuInfo["sale_price"]?.Value<decimal>() ?? 0;
if (salePrice <= 0 && priceFallbackDict.TryGetValue(skuId, out var fallbackPrice))
salePrice = fallbackPrice;
var stockNum = ParseStockNum(skuInfo);
var thumbImg = skuInfo["thumb_img"]?.Value<string>() ?? string.Empty;
return new ProductSku
{
SkuId = !string.IsNullOrEmpty(outSkuId) ? outSkuId : skuId,
Price = salePrice / 100,
Stock = stockNum,
ImageUrl = thumbImg,
Specs = skuSpecs
};
}
private ProductSku ParseSkuInfoOnly(
JObject skuObj,
Dictionary<string, List<ProductSkuSpec>> attrFallbackDict)
{
var skuId = skuObj["sku_id"]?.Value<string>() ?? string.Empty;
if (string.IsNullOrEmpty(skuId)) return null;
var skuSpecs = attrFallbackDict.TryGetValue(skuId, out var specs)
? specs
: new List<ProductSkuSpec>();
var estimatePrice = skuObj["estimate_price"]?.Value<decimal>() ?? 0;
var price = skuObj["price"]?.Value<decimal>() ?? 0;
var actualPrice = estimatePrice > 0 ? estimatePrice : price;
return new ProductSku
{
SkuId = skuId,
Price = actualPrice / 100,
Stock = 0,
ImageUrl = string.Empty,
Specs = skuSpecs
};
}
/// <summary>
/// 解析SKU的结构化规格列表
/// </summary>
private List<ProductSkuSpec> ParseSkuSpecs(
JObject skuInfo,
string skuId,
Dictionary<string, List<ProductSkuSpec>> attrFallbackDict)
{
var specs = new List<ProductSkuSpec>();
var saleParams = skuInfo["sale_param"] as JArray;
if (saleParams != null)
{
foreach (var param in saleParams)
{
var paramV2 = param["param_v2"] as JObject;
if (paramV2 != null)
{
var attrName = paramV2["attr_name"]?.Value<string>() ?? string.Empty;
var attrValue = paramV2["attr_value_name"]?.Value<string>() ?? string.Empty;
if (!string.IsNullOrEmpty(attrName) && !string.IsNullOrEmpty(attrValue))
{
specs.Add(new ProductSkuSpec { Name = attrName, Value = attrValue });
continue;
}
}
var categorys = param["categorys"] as JArray;
if (categorys != null && categorys.Count >= 2)
{
var attrName = categorys[0]?["name"]?.Value<string>() ?? string.Empty;
var attrValue = categorys[1]?["name"]?.Value<string>() ?? string.Empty;
if (!string.IsNullOrEmpty(attrName) && !string.IsNullOrEmpty(attrValue))
{
specs.Add(new ProductSkuSpec { Name = attrName, Value = attrValue });
}
}
}
}
if (specs.Count == 0 && attrFallbackDict.TryGetValue(skuId, out var fallbackSpecs))
{
specs.AddRange(fallbackSpecs);
}
return specs;
}
private int ParseStockNum(JObject skuInfo)
{
var stockInfo = skuInfo["stock_info"] as JObject;
if (stockInfo == null) return 0;
var stockToken = stockInfo["stock_num"];
if (stockToken == null) return 0;
if (stockToken.Type == JTokenType.String)
return int.TryParse(stockToken.Value<string>(), out var result) ? result : 0;
return stockToken.Value<int>();
}
/// <summary>
/// 构建聚合后的规格定义列表
/// </summary>
private void BuildSpecList(List<ProductSpec> specList, List<ProductSku> skus)
{
var specDict = new Dictionary<string, HashSet<string>>();
// 优先从 product_sale_attrs
if (_saleAttrs != null && _saleAttrs.Count > 0)
{
foreach (var attr in _saleAttrs)
{
var key = attr["key"]?.Value<string>() ?? string.Empty;
var items = attr["items"] as JArray;
if (items == null) continue;
foreach (var item in items)
{
var value = item["value_v2"]?.Value<string>();
if (string.IsNullOrEmpty(value))
value = item["value"]?.Value<string>();
if (string.IsNullOrEmpty(value)) continue;
if (!specDict.ContainsKey(key))
specDict[key] = new HashSet<string>();
specDict[key].Add(value);
}
}
}
// 回退:从SKU的结构化规格解析
if (specDict.Count == 0 && skus.Count > 0)
{
foreach (var sku in skus)
{
foreach (var s in sku.Specs)
{
if (string.IsNullOrEmpty(s.Name)) continue;
if (!specDict.ContainsKey(s.Name))
specDict[s.Name] = new HashSet<string>();
specDict[s.Name].Add(s.Value);
}
}
}
foreach (var spec in specDict)
{
specList.Add(new ProductSpec
{
Name = spec.Key,
Values = spec.Value.ToList()
});
}
}
private int GetTotalSold()
{
var stockInfo = _infoNode["total_sold_num"] as JToken;
if (stockInfo == null) return 0;
return stockInfo.Value<int>();
}
#endregion
#region 辅助方法
private string GetStringValue(JObject obj, string path)
=> obj?.SelectToken(path)?.Value<string>() ?? string.Empty;
private long GetLongValue(JObject obj, string path)
{
var token = obj?.SelectToken(path);
if (token == null) return 0;
if (token.Type == JTokenType.String)
return long.TryParse(token.Value<string>(), out var result) ? result : 0;
return token.Value<long>();
}
#endregion
}
}