Files
KJD/TEMUKJD/BLL/ImportConverters/ConverterWXToPddV4.cs
T

702 lines
19 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.Model.Result;
using AllBase.Model.Result.PDDResult;
using NaiBa.Util.Extension;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace TemuKJD.BLL.ImportConverters
{
/// <summary>
/// 微信转拼多多数据源 V4版本(兼容增强版)
/// 针对微信商品详情API(/channels/ec/product/get)返回的 detail.product 结构
///
/// 兼容的数据样例:
/// - 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 均分
/// </summary>
internal class ConverterWXToPddV4 : GoodsConverterBase
{
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
public override string ConverterName => "微信转拼多多数据源 V4";
public override PlatformType Platform => PlatformType.WX;
protected override bool IsMatchProvider()
{
try
{
_jsonRoot = JsonConvert.DeserializeObject<JObject>(RawData);
if (_jsonRoot == null) return false;
// 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.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 节点
var info = product.SelectToken("$.info") as JObject;
if (info == null) return false;
// 4. 检查标题
var title = info.SelectToken("$.title")?.ToString();
if (string.IsNullOrEmpty(title)) 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;
_productSkus = productSkus;
_skuList = skuList;
_saleAttrs = info.SelectToken("$.product_sale_attrs") as JArray;
return true;
}
catch
{
return false;
}
}
protected override PDDFrontProductInfo5 ConverterProvider()
{
var product = new PDDFrontProductInfo5
{
Platform = Platform,
GoodsId = 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. 品牌信息 ==========
product.BrandId = GetLongValue(_infoNode, "brand_id");
// ========== 3. 类目信息 ==========
product.CategoryId = GetCategoryId();
product.CategoryPath = GetCategoryPath();
// ========== 4. 主图 ==========
product.MainImages = GetMainImages();
// ========== 5. 详情图 ==========
product.DetailImages = GetDetailImages();
// ========== 6. 商品属性 ==========
product.Propertys = GetProperties();
// ========== 7. SKU和规格信息 ==========
var skuResult = GetSkusAndSpecs();
product.Skus = skuResult.Item1;
product.Specs = skuResult.Item2;
// ========== 8. 其他信息 ==========
product.ThumbUrl = product.MainImages.FirstOrDefault() ?? string.Empty;
product.MallId = 0;
product.Price = GetMinPrice();
product.Videos = GetVideoUrls();
return product;
}
#region 核心字段解析
/// <summary>
/// 解析商品ID(兼容 out_product_id 存在与否)
/// </summary>
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();
}
/// <summary>
/// 获取类目ID(兼容 category 数组,取最细分类目)
/// </summary>
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;
}
/// <summary>
/// 获取类目路径
/// </summary>
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;
}
/// <summary>
/// 获取主图列表
/// </summary>
private List<string> GetMainImages()
{
var result = new List<string>();
// 优先 head_img
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;
}
// 回退 head_img_infos
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;
}
/// <summary>
/// 获取详情图列表
/// </summary>
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>
/// 获取商品属性列表(从 info.param 解析)
/// 兼容两种结构:
/// - 10000031453165:categorys[0].name = key, categorys[1].name = value
/// - 10000182390569:categorys[0].name = key, categorys[1].name = value(相同)
/// </summary>
private List<Property> GetProperties()
{
var result = new List<Property>();
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))
{
if (!result.Any(p => p.Key == key))
{
result.Add(new Property
{
Key = key,
Value = value
});
}
}
}
return result;
}
/// <summary>
/// 获取视频URL列表(兼容无视频的情况)
/// </summary>
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和规格解析
/// <summary>
/// 获取SKU和规格信息
/// 兼容两种数据:
/// - product_skus:包含 sku_key.sku_id、sale_param、sale_price、stock_info
/// - sku_list.sku_info:包含 sku_id、price、estimate_price(无规格、无库存)
/// </summary>
private Tuple<List<SkuItem> , List<SpecItem>> GetSkusAndSpecs()
{
var skuList = new List<SkuItem>();
var specList = new List<SpecItem>();
// 构建 sku_list.sku_info 的价格字典(作为兜底)
var priceFallbackDict = BuildPriceFallbackDict();
// 构建 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<SkuItem>, List<SpecItem>>(skuList, specList);
}
/// <summary>
/// 构建 sku_list.sku_info 的价格兜底字典
/// </summary>
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;
// 优先 estimate_price(优惠后),回退 price
var estimatePrice = skuObj["estimate_price"]?.Value<decimal>() ?? 0;
var price = skuObj["price"]?.Value<decimal>() ?? 0;
var actualPrice = estimatePrice > 0 ? estimatePrice : price;
dict[skuId] = actualPrice;
}
return dict;
}
/// <summary>
/// 构建 product_sale_attrs 的规格兜底字典(skuId -> spec文本)
/// 兼容两种结构:
/// - 10000031453165:items[].value_v2, items[].sku_ids[]
/// - 10000182390569:items[].value, items[].sku_ids[]
/// </summary>
private Dictionary<string, string> BuildAttrFallbackDict()
{
var dict = new Dictionary<string, string>();
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)
{
// 兼容 value_v2 和 value
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;
var specText = $"{key}:{value}";
foreach (var skuId in skuIds)
{
var id = skuId?.Value<string>();
if (!string.IsNullOrEmpty(id))
dict[id] = specText;
}
}
}
return dict;
}
/// <summary>
/// 解析 product_skus 中的单个SKU
/// </summary>
private SkuItem ParseProductSku(
JObject skuObj,
Dictionary<string, decimal> priceFallbackDict,
Dictionary<string, string> 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 specText = ParseSkuSpec(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 SkuItem
{
SkuId = !string.IsNullOrEmpty(outSkuId) ? outSkuId : skuId,
Spec = specText,
Price = salePrice / 100, // 分转元
OriginalPrice = salePrice.ToString(),
Stock = stockNum,
SpecImage = thumbImg
};
}
/// <summary>
/// 解析 sku_list.sku_info 中的单个SKU(仅价格)
/// </summary>
private SkuItem ParseSkuInfoOnly(
JObject skuObj,
Dictionary<string, string> attrFallbackDict)
{
var skuId = skuObj["sku_id"]?.Value<string>() ?? 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<decimal>() ?? 0;
var price = skuObj["price"]?.Value<decimal>() ?? 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
};
}
/// <summary>
/// 解析SKU规格
/// 兼容两种结构:
/// - 10000031453165:sale_param[].param_v2.attr_name / attr_value_name
/// - 10000182390569:sale_param[].categorys[0].name / categorys[1].name
/// </summary>
private string ParseSkuSpec(
JObject skuInfo,
string skuId,
Dictionary<string, string> attrFallbackDict)
{
var specValues = new List<string>();
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>() ?? string.Empty;
var attrValue = paramV2["attr_value_name"]?.Value<string>() ?? string.Empty;
if (!string.IsNullOrEmpty(attrName) && !string.IsNullOrEmpty(attrValue))
{
specValues.Add($"{attrName}:{attrValue}");
continue;
}
}
// 回退 categorys
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))
{
specValues.Add($"{attrName}:{attrValue}");
}
}
}
}
// 如果无法从 sale_param 解析,从 attrFallbackDict 兜底
if (specValues.Count == 0 && attrFallbackDict.TryGetValue(skuId, out var fallbackSpec))
{
specValues.Add(fallbackSpec);
}
return string.Join(";", specValues);
}
/// <summary>
/// 解析库存(兼容字符串/数字格式)
/// </summary>
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>
/// 构建规格列表
/// 优先从 product_sale_attrs 构建,回退从SKU的spec文本解析
/// </summary>
private void BuildSpecList(List<SpecItem> specList, List<SkuItem> 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)
{
// 兼容 value_v2 和 value
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);
}
}
}
// 如果 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<string>();
specDict[key].Add(value);
}
}
}
}
// 构建 SpecItem 列表
foreach (var spec in specDict)
{
foreach (var value in spec.Value)
{
specList.Add(new SpecItem
{
SpecKey = spec.Key,
SpecValue = value,
SpecKeyId = 0,
SpecValueId = 0
});
}
}
}
/// <summary>
/// 获取最低价格(元)
/// </summary>
private decimal GetMinPrice()
{
// 优先从 product_skus 获取
if (_productSkus != null && _productSkus.Count > 0)
{
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 price = skuInfo["sale_price"]?.Value<decimal>() ?? 0;
if (price > 0 && price < minPrice)
minPrice = price;
}
if (minPrice > 0 && minPrice != decimal.MaxValue)
return minPrice / 100;
}
// 回退 sku_list.min_price
var skuListInfo = _infoNode["sku_list"] as JObject;
if (skuListInfo != null)
{
var minPriceToken = skuListInfo["min_price"]?.Value<decimal>() ?? 0;
if (minPriceToken > 0)
return minPriceToken / 100;
}
return 0;
}
#endregion
#region 辅助方法
private string GetStringValue(JObject obj, string path)
{
return 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
}
}