From 7a7cd82eea39041365a4b616602217bce9cab25b Mon Sep 17 00:00:00 2001 From: chfei Date: Mon, 12 Oct 2026 20:51:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=95=B0=E6=8D=AE=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=20=20=E6=96=B0=E6=A0=BC=E5=BC=8F=E5=85=BC=E5=AE=B9v4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ImportConverters/ConverterWXToPddV4.cs | 466 +++++++++++++----- test/KJD.Test/UnitTest1.cs | 2 +- 2 files changed, 352 insertions(+), 116 deletions(-) diff --git a/TEMUKJD/BLL/ImportConverters/ConverterWXToPddV4.cs b/TEMUKJD/BLL/ImportConverters/ConverterWXToPddV4.cs index 4e9d5d3..ada0243 100644 --- a/TEMUKJD/BLL/ImportConverters/ConverterWXToPddV4.cs +++ b/TEMUKJD/BLL/ImportConverters/ConverterWXToPddV4.cs @@ -11,23 +11,27 @@ using System.Linq; namespace TemuKJD.BLL.ImportConverters { /// - /// 微信转拼多多数据源 V4版本 - /// 针对微信商品详情API(/channels/ec/product/get)返回的完整数据结构 + /// 微信转拼多多数据源 V4版本(兼容增强版) + /// 针对微信商品详情API(/channels/ec/product/get)返回的 detail.product 结构 /// - /// 数据结构特点: - /// - 顶层为 { detail: { product: { product_key, info, product_skus, ... } } } - /// - info 节点包含商品基本信息、主图、详情图、参数、SKU列表等 - /// - product_skus 节点包含完整的SKU信息(含库存、价格、规格) - /// - product_sale_attrs 节点包含销售属性(颜色、尺码) - /// - extra_service 节点包含售后服务信息 - /// - qualification_list 节点包含资质信息 + /// 兼容的数据样例: + /// - 10000031453165(女装针织衫,含视频、资质、完整size_chart) + /// - 10000182390569(不锈钢调味球,无视频、无资质列表、简化size_chart) + /// + /// 兼容策略: + /// 1. 所有可选字段使用安全访问,缺失时回退 + /// 2. 属性解析优先 param_v2,回退 categorys + /// 3. 规格解析优先 value_v2,回退 value + /// 4. SKU价格优先 sale_price,回退 sku_list.sku_info.price + /// 5. SKU库存优先 product_skus,回退 spu_total_stock 均分 /// internal class ConverterWXToPddV4 : GoodsConverterBase { private JObject _jsonRoot; private JObject _productNode; // detail.product private JObject _infoNode; // detail.product.info - private JArray _skuList; // detail.product.info.product_skus + 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 public override string ConverterName => "微信转拼多多数据源 V4"; @@ -41,36 +45,41 @@ namespace TemuKJD.BLL.ImportConverters _jsonRoot = JsonConvert.DeserializeObject(RawData); if (_jsonRoot == null) return false; - // 1. 检查是否为详情API格式(detail.product 结构) + // 1. 检查 detail.product 结构 var detail = _jsonRoot.SelectToken("$.detail") as JObject; if (detail == null) return false; var product = detail.SelectToken("$.product") as JObject; if (product == null) return false; - // 2. 检查 product_key 是否存在 + // 2. 检查 product_key.product_id 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; - // 3. 检查 info 节点是否存在 + // 3. 检查 info 节点 var info = product.SelectToken("$.info") as JObject; if (info == null) return false; - // 4. 检查是否有标题 + // 4. 检查标题 var title = info.SelectToken("$.title")?.ToString(); if (string.IsNullOrEmpty(title)) return false; - // 5. 检查是否有SKU列表 - var skus = product.SelectToken("$.product_skus") as JArray; - if (skus == null || skus.Count == 0) return false; + // 5. 检查SKU(兼容两种:product_skus 优先,sku_list.sku_info 兜底) + 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; - _skuList = skus; + _productSkus = productSkus; + _skuList = skuList; _saleAttrs = info.SelectToken("$.product_sale_attrs") as JArray; return true; @@ -88,14 +97,12 @@ namespace TemuKJD.BLL.ImportConverters Platform = Platform, GoodsId = ParseGoodsId() }; - product.ListingTime = GetListingTime(); + // ========== 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. 品牌信息 ========== product.BrandId = GetLongValue(_infoNode, "brand_id"); @@ -130,20 +137,21 @@ namespace TemuKJD.BLL.ImportConverters #region 核心字段解析 /// - /// 解析商品ID + /// 解析商品ID(兼容 out_product_id 存在与否) /// private long ParseGoodsId() { + // 优先 product_id(所有情况都存在) var productId = _productNode.SelectToken("$.product_key.product_id")?.ToString(); if (long.TryParse(productId, out var result)) return result; - // 回退到文件路径 + // 回退文件路径 return Path.GetFileNameWithoutExtension(FilePath).ParseToLong(); } /// - /// 获取类目ID(取最细分类目) + /// 获取类目ID(兼容 category 数组,取最细分类目) /// private int GetCategoryId() { @@ -151,7 +159,6 @@ namespace TemuKJD.BLL.ImportConverters if (category == null || category.Count == 0) return 0; - // 取最后一个类目(最细分类目) var lastCat = category.Last as JObject; return lastCat?["cat_id"]?.Value() ?? 0; } @@ -183,7 +190,7 @@ namespace TemuKJD.BLL.ImportConverters { var result = new List(); - // 优先使用 head_img 数组 + // 优先 head_img var headImg = _infoNode["head_img"] as JArray; if (headImg != null && headImg.Count > 0) { @@ -196,7 +203,7 @@ namespace TemuKJD.BLL.ImportConverters return result; } - // 回退到 head_img_infos + // 回退 head_img_infos var headImgInfos = _infoNode["head_img_infos"] as JArray; if (headImgInfos != null) { @@ -236,7 +243,10 @@ namespace TemuKJD.BLL.ImportConverters } /// - /// 获取商品属性列表(从 param 节点解析) + /// 获取商品属性列表(从 info.param 解析) + /// 兼容两种结构: + /// - 10000031453165:categorys[0].name = key, categorys[1].name = value + /// - 10000182390569:categorys[0].name = key, categorys[1].name = value(相同) /// private List GetProperties() { @@ -256,7 +266,6 @@ namespace TemuKJD.BLL.ImportConverters if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value)) { - // 去重 if (!result.Any(p => p.Key == key)) { result.Add(new Property @@ -272,7 +281,7 @@ namespace TemuKJD.BLL.ImportConverters } /// - /// 获取视频URL列表 + /// 获取视频URL列表(兼容无视频的情况) /// private List GetVideoUrls() { @@ -290,18 +299,6 @@ namespace TemuKJD.BLL.ImportConverters return result; } - private DateTime? GetListingTime() - { - string time = _infoNode["listing_time"].ToString(); - if (time != null) - { - if (int.TryParse(time, out int v)) - { - return NaiBa.Util.Helper.DateTimeHelper.IntStampToDateTime(v); - } - } - return null; - } #endregion @@ -309,87 +306,324 @@ namespace TemuKJD.BLL.ImportConverters /// /// 获取SKU和规格信息 + /// 兼容两种数据: + /// - product_skus:包含 sku_key.sku_id、sale_param、sale_price、stock_info + /// - sku_list.sku_info:包含 sku_id、price、estimate_price(无规格、无库存) /// - private Tuple, List> GetSkusAndSpecs() + private Tuple , List> GetSkusAndSpecs() { var skuList = new List(); var specList = new List(); - if (_skuList == null || _skuList.Count == 0) - return new Tuple, List>(skuList, specList); + // 构建 sku_list.sku_info 的价格字典(作为兜底) + var priceFallbackDict = BuildPriceFallbackDict(); - // 收集所有规格 - var specDict = new Dictionary>(); + // 构建 product_sale_attrs 的规格字典(作为兜底) + var attrFallbackDict = BuildAttrFallbackDict(); + + // 优先处理 product_skus(完整信息) + 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); + } + } + // 回退到 sku_list.sku_info(仅价格) + 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); + } + } + + // 构建规格列表(优先 product_sale_attrs) + BuildSpecList(specList, skuList); + + return new Tuple, List>(skuList, specList); + } + + /// + /// 构建 sku_list.sku_info 的价格兜底字典 + /// + private Dictionary BuildPriceFallbackDict() + { + var dict = new Dictionary(); + if (_skuList == null) return dict; foreach (var sku in _skuList) { var skuObj = sku as JObject; if (skuObj == null) continue; - // 获取 SKU 基本信息 - var skuKey = skuObj["sku_key"] as JObject; - var skuInfo = skuObj["product_sku_info"] as JObject; - if (skuKey == null || skuInfo == null) continue; + var skuId = skuObj["sku_id"]?.Value(); + if (string.IsNullOrEmpty(skuId)) continue; - var skuId = skuKey["sku_id"]?.Value() ?? string.Empty; - var outSkuId = skuKey["out_sku_id"]?.Value() ?? string.Empty; + // 优先 estimate_price(优惠后),回退 price + var estimatePrice = skuObj["estimate_price"]?.Value() ?? 0; + var price = skuObj["price"]?.Value() ?? 0; + var actualPrice = estimatePrice > 0 ? estimatePrice : price; - // 解析销售属性 - var saleParams = skuInfo["sale_param"] as JArray; - var specValues = new List(); + dict[skuId] = actualPrice; + } - if (saleParams != null) + return dict; + } + + /// + /// 构建 product_sale_attrs 的规格兜底字典(skuId -> spec文本) + /// 兼容两种结构: + /// - 10000031453165:items[].value_v2, items[].sku_ids[] + /// - 10000182390569:items[].value, items[].sku_ids[] + /// + private Dictionary BuildAttrFallbackDict() + { + var dict = new Dictionary(); + if (_saleAttrs == null) return dict; + + foreach (var attr in _saleAttrs) + { + var key = attr["key"]?.Value() ?? string.Empty; + var items = attr["items"] as JArray; + if (items == null) continue; + + foreach (var item in items) { - foreach (var param in saleParams) - { - var paramV2 = param["param_v2"] as JObject; - if (paramV2 == null) continue; + // 兼容 value_v2 和 value + var value = item["value_v2"]?.Value(); + if (string.IsNullOrEmpty(value)) + value = item["value"]?.Value(); + if (string.IsNullOrEmpty(value)) continue; + + var skuIds = item["sku_ids"] as JArray; + if (skuIds == null) continue; + + var specText = $"{key}:{value}"; + foreach (var skuId in skuIds) + { + var id = skuId?.Value(); + if (!string.IsNullOrEmpty(id)) + dict[id] = specText; + } + } + } + + return dict; + } + + /// + /// 解析 product_skus 中的单个SKU + /// + private SkuItem ParseProductSku( + JObject skuObj, + Dictionary priceFallbackDict, + Dictionary 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.Empty; + var outSkuId = skuKey["out_sku_id"]?.Value() ?? string.Empty; + if (string.IsNullOrEmpty(skuId)) return null; + + // 解析规格 + var specText = ParseSkuSpec(skuInfo, skuId, attrFallbackDict); + + // 解析价格 + var salePrice = skuInfo["sale_price"]?.Value() ?? 0; + if (salePrice <= 0 && priceFallbackDict.TryGetValue(skuId, out var fallbackPrice)) + salePrice = fallbackPrice; + + // 解析库存 + var stockNum = ParseStockNum(skuInfo); + + // 缩略图 + var thumbImg = skuInfo["thumb_img"]?.Value() ?? string.Empty; + + return new SkuItem + { + SkuId = !string.IsNullOrEmpty(outSkuId) ? outSkuId : skuId, + Spec = specText, + Price = salePrice / 100, // 分转元 + OriginalPrice = salePrice.ToString(), + Stock = stockNum, + SpecImage = thumbImg + }; + } + + /// + /// 解析 sku_list.sku_info 中的单个SKU(仅价格) + /// + private SkuItem ParseSkuInfoOnly( + JObject skuObj, + Dictionary attrFallbackDict) + { + var skuId = skuObj["sku_id"]?.Value() ?? string.Empty; + if (string.IsNullOrEmpty(skuId)) return null; + + // 规格从 attrFallbackDict 获取 + var specText = attrFallbackDict.TryGetValue(skuId, out var spec) ? spec : string.Empty; + + // 价格 + var estimatePrice = skuObj["estimate_price"]?.Value() ?? 0; + var price = skuObj["price"]?.Value() ?? 0; + var actualPrice = estimatePrice > 0 ? estimatePrice : price; + + return new SkuItem + { + SkuId = skuId, + Spec = specText, + Price = actualPrice / 100, + OriginalPrice = actualPrice.ToString(), + Stock = 0, + SpecImage = string.Empty + }; + } + + /// + /// 解析SKU规格 + /// 兼容两种结构: + /// - 10000031453165:sale_param[].param_v2.attr_name / attr_value_name + /// - 10000182390569:sale_param[].categorys[0].name / categorys[1].name + /// + private string ParseSkuSpec( + JObject skuInfo, + string skuId, + Dictionary attrFallbackDict) + { + var specValues = new List(); + var saleParams = skuInfo["sale_param"] as JArray; + + if (saleParams != null) + { + foreach (var param in saleParams) + { + // 优先 param_v2 + var paramV2 = param["param_v2"] as JObject; + if (paramV2 != null) + { var attrName = paramV2["attr_name"]?.Value() ?? string.Empty; var attrValue = paramV2["attr_value_name"]?.Value() ?? string.Empty; if (!string.IsNullOrEmpty(attrName) && !string.IsNullOrEmpty(attrValue)) { specValues.Add($"{attrName}:{attrValue}"); + continue; + } + } - // 收集规格 - if (!specDict.ContainsKey(attrName)) - specDict[attrName] = new HashSet(); - specDict[attrName].Add(attrValue); + // 回退 categorys + var categorys = param["categorys"] as JArray; + if (categorys != null && categorys.Count >= 2) + { + var attrName = categorys[0]?["name"]?.Value() ?? string.Empty; + var attrValue = categorys[1]?["name"]?.Value() ?? string.Empty; + + if (!string.IsNullOrEmpty(attrName) && !string.IsNullOrEmpty(attrValue)) + { + specValues.Add($"{attrName}:{attrValue}"); } } } - - // 价格(单位:分) - var salePrice = skuInfo["sale_price"]?.Value() ?? 0; - - // 库存 - var stockInfo = skuInfo["stock_info"] as JObject; - var stockNum = 0; - if (stockInfo != null) - { - var stockStr = stockInfo["stock_num"]?.Value(); - if (!string.IsNullOrEmpty(stockStr)) - { - int.TryParse(stockStr, out stockNum); - } - } - - // 缩略图 - var thumbImg = skuInfo["thumb_img"]?.Value() ?? string.Empty; - - skuList.Add(new SkuItem - { - SkuId = !string.IsNullOrEmpty(outSkuId) ? outSkuId : skuId, - Spec = string.Join(";", specValues), - Price = salePrice / 100, // 分转元 - OriginalPrice = salePrice.ToString(), - Stock = stockNum, - SpecImage = thumbImg - }); } - // 构建规格列表 + // 如果无法从 sale_param 解析,从 attrFallbackDict 兜底 + if (specValues.Count == 0 && attrFallbackDict.TryGetValue(skuId, out var fallbackSpec)) + { + specValues.Add(fallbackSpec); + } + + return string.Join(";", specValues); + } + + /// + /// 解析库存(兼容字符串/数字格式) + /// + 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(), out var result) ? result : 0; + } + + return stockToken.Value(); + } + + /// + /// 构建规格列表 + /// 优先从 product_sale_attrs 构建,回退从SKU的spec文本解析 + /// + private void BuildSpecList(List specList, List skus) + { + var specDict = new Dictionary>(); + + // 优先从 product_sale_attrs 构建 + if (_saleAttrs != null && _saleAttrs.Count > 0) + { + foreach (var attr in _saleAttrs) + { + var key = attr["key"]?.Value() ?? string.Empty; + var items = attr["items"] as JArray; + if (items == null) continue; + + foreach (var item in items) + { + // 兼容 value_v2 和 value + var value = item["value_v2"]?.Value(); + if (string.IsNullOrEmpty(value)) + value = item["value"]?.Value(); + + if (string.IsNullOrEmpty(value)) continue; + + if (!specDict.ContainsKey(key)) + specDict[key] = new HashSet(); + specDict[key].Add(value); + } + } + } + + // 如果 product_sale_attrs 不存在,从SKU的spec文本解析 + if (specDict.Count == 0 && skus.Count > 0) + { + foreach (var sku in skus) + { + if (string.IsNullOrEmpty(sku.Spec)) continue; + + foreach (var part in sku.Spec.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + var kv = part.Split(':'); + if (kv.Length == 2) + { + var key = kv[0].Trim(); + var value = kv[1].Trim(); + if (!specDict.ContainsKey(key)) + specDict[key] = new HashSet(); + specDict[key].Add(value); + } + } + } + } + + // 构建 SpecItem 列表 foreach (var spec in specDict) { foreach (var value in spec.Value) @@ -403,8 +637,6 @@ namespace TemuKJD.BLL.ImportConverters }); } } - - return new Tuple, List>(skuList, specList); } /// @@ -412,33 +644,37 @@ namespace TemuKJD.BLL.ImportConverters /// private decimal GetMinPrice() { - if (_skuList == null || _skuList.Count == 0) - return 0; - - var minPrice = decimal.MaxValue; - - foreach (var sku in _skuList) + // 优先从 product_skus 获取 + if (_productSkus != null && _productSkus.Count > 0) { - var skuObj = sku as JObject; - if (skuObj == null) continue; + var minPrice = decimal.MaxValue; + foreach (var sku in _productSkus) + { + var skuObj = sku as JObject; + if (skuObj == null) continue; - var skuInfo = skuObj["product_sku_info"] as JObject; - if (skuInfo == null) continue; + var skuInfo = skuObj["product_sku_info"] as JObject; + if (skuInfo == null) continue; - var price = skuInfo["sale_price"]?.Value() ?? 0; - if (price > 0 && price < minPrice) - minPrice = price; + var price = skuInfo["sale_price"]?.Value() ?? 0; + if (price > 0 && price < minPrice) + minPrice = price; + } + + if (minPrice > 0 && minPrice != decimal.MaxValue) + return minPrice / 100; } - // 回退到 sku_list.min_price - if (minPrice == decimal.MaxValue) + // 回退 sku_list.min_price + var skuListInfo = _infoNode["sku_list"] as JObject; + if (skuListInfo != null) { - var skuListInfo = _infoNode["sku_list"] as JObject; - var minPriceToken = skuListInfo?["min_price"]?.Value() ?? 0; - return minPriceToken / 100; + var minPriceToken = skuListInfo["min_price"]?.Value() ?? 0; + if (minPriceToken > 0) + return minPriceToken / 100; } - return minPrice > 0 ? minPrice / 100 : 0; + return 0; } #endregion diff --git a/test/KJD.Test/UnitTest1.cs b/test/KJD.Test/UnitTest1.cs index c7efb1f..aef793b 100644 --- a/test/KJD.Test/UnitTest1.cs +++ b/test/KJD.Test/UnitTest1.cs @@ -143,7 +143,7 @@ namespace KJD.Test [Test] public void TestWXToPDDV4() { - string filePath = @"C:\Users\chaih\Documents\xwechat_files\hongfei357_a567\msg\file\2026-10\10000033063024.json"; + string filePath = @"C:\Users\chaih\Documents\xwechat_files\hongfei357_a567\msg\attach\cde4474908f6a5f2da5f7c82f4a8c5d0\2026-10\Rec\e65980801fd61508\F\0\10000182390569(1).txt"; var frontProdInfo = ConverterToPddFactory.Convert(filePath); var isMatch = frontProdInfo != null; Console.WriteLine(frontProdInfo.Skus.Count);