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);