541 lines
14 KiB
C#
541 lines
14 KiB
C#
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>
|
|
/// 微信转拼多多数据源 V5版本
|
|
/// 针对扁平化结构(含 _raw 节点 + 顶层冗余字段)
|
|
///
|
|
/// 数据结构特点:
|
|
/// - 顶层包含 _raw、_src、appid、brandId、catId 等元信息
|
|
/// - 核心商品数据在 _raw 节点内(驼峰命名:headImg、skuList、detail等)
|
|
/// - 顶层同时冗余了部分字段(headImg、headImgs、saleAttrs、skuSpecs等)
|
|
/// - SKU价格使用 isDiscounted + estimatePrice 计算优惠后价格
|
|
/// - 类目路径为字符串 "1208/1242/1244" 格式
|
|
/// </summary>
|
|
internal class ConverterWXToPddV5 : GoodsConverterBase
|
|
{
|
|
private JObject _jsonRoot;
|
|
private JObject _rawNode; // _raw 节点
|
|
private JArray _skuInfoList; // _raw.skuList.skuInfo
|
|
private JArray _saleAttrs; // 顶层 saleAttrs 或 _raw.saleAttrs
|
|
private JArray _skuSpecs; // 顶层 skuSpecs
|
|
|
|
public override string ConverterName => "微信转拼多多数据源 V5";
|
|
|
|
public override PlatformType Platform => PlatformType.WX;
|
|
|
|
protected override bool IsMatchProvider()
|
|
{
|
|
try
|
|
{
|
|
_jsonRoot = JsonConvert.DeserializeObject<JObject>(RawData);
|
|
if (_jsonRoot == null) return false;
|
|
|
|
// 1. 检查 _raw 节点是否存在
|
|
var raw = _jsonRoot.SelectToken("$._raw") as JObject;
|
|
if (raw == null) return false;
|
|
|
|
// 2. 检查标题是否存在
|
|
var title = raw.SelectToken("$.title")?.ToString();
|
|
if (string.IsNullOrEmpty(title)) return false;
|
|
|
|
// 3. 检查 headImg 是否存在且为数组
|
|
var headImg = raw.SelectToken("$.headImg") as JArray;
|
|
if (headImg == null || headImg.Count == 0) return false;
|
|
|
|
// 4. 检查 skuList.skuInfo 是否存在且为数组
|
|
var skuList = raw.SelectToken("$.skuList") as JObject;
|
|
if (skuList == null) return false;
|
|
|
|
var skuInfo = skuList.SelectToken("$.skuInfo") as JArray;
|
|
if (skuInfo == null || skuInfo.Count == 0) return false;
|
|
|
|
// 5. 检查第一个SKU是否有skuId
|
|
var firstSku = skuInfo.FirstOrDefault() as JObject;
|
|
if (firstSku == null) return false;
|
|
|
|
var skuId = firstSku.SelectToken("$.skuId")?.ToString();
|
|
if (string.IsNullOrEmpty(skuId)) return false;
|
|
|
|
// 缓存节点
|
|
_rawNode = raw;
|
|
_skuInfoList = skuInfo;
|
|
_saleAttrs = _jsonRoot.SelectToken("$.saleAttrs") as JArray
|
|
?? raw.SelectToken("$.saleAttrs") as JArray;
|
|
_skuSpecs = _jsonRoot.SelectToken("$.skuSpecs") as JArray;
|
|
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected override PDDFrontProductInfo5 ConverterProvider()
|
|
{
|
|
var product = new PDDFrontProductInfo5
|
|
{
|
|
Platform = Platform,
|
|
GoodsId = ParseGoodsId()
|
|
};
|
|
product.ListingTime = GetListingTime();
|
|
// ========== 1. 商品基本信息 ==========
|
|
product.Title = GetStringValue(_rawNode, "title");
|
|
product.Description = GetStringValue(_rawNode, "subTitle");
|
|
if (string.IsNullOrEmpty(product.Description))
|
|
{
|
|
product.Description = GetStringValue(_jsonRoot, "subTitle");
|
|
}
|
|
|
|
// ========== 2. 品牌信息 ==========
|
|
product.BrandId = GetLongValue(_rawNode, "brandId");
|
|
|
|
// ========== 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 = new List<string>();
|
|
|
|
return product;
|
|
}
|
|
|
|
#region 核心字段解析
|
|
|
|
/// <summary>
|
|
/// 解析商品ID
|
|
/// </summary>
|
|
private long ParseGoodsId()
|
|
{
|
|
// 优先从 productId / itemId 解析
|
|
var productId = GetStringValue(_jsonRoot, "productId");
|
|
if (string.IsNullOrEmpty(productId))
|
|
productId = GetStringValue(_jsonRoot, "itemId");
|
|
|
|
if (long.TryParse(productId, out var result))
|
|
return result;
|
|
|
|
// 回退到 expressInfo.productId
|
|
var expressProductId = _rawNode.SelectToken("$.expressInfo.productId")?.ToString();
|
|
if (long.TryParse(expressProductId, out var result2))
|
|
return result2;
|
|
|
|
// 最后回退到文件路径
|
|
return Path.GetFileNameWithoutExtension(FilePath).ParseToLong();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取类目ID(从 catId 字符串 "1208/1242/1244" 解析)
|
|
/// </summary>
|
|
private int GetCategoryId()
|
|
{
|
|
// 优先从顶层 catId 解析
|
|
var catIdStr = GetStringValue(_jsonRoot, "catId");
|
|
var catId = ParseLastCatId(catIdStr);
|
|
if (catId > 0) return catId;
|
|
|
|
// 回退到 _raw.category 数组
|
|
var category = _rawNode["category"] as JArray;
|
|
if (category != null && category.Count > 0)
|
|
{
|
|
var lastCat = category.Last as JObject;
|
|
var lastCatId = lastCat?["catId"]?.Value<string>();
|
|
if (int.TryParse(lastCatId, out var result))
|
|
return result;
|
|
}
|
|
|
|
// 回退到 nodeCatId
|
|
var nodeCatId = GetStringValue(_rawNode, "nodeCatId");
|
|
if (int.TryParse(nodeCatId, out var nodeResult))
|
|
return nodeResult;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取类目路径
|
|
/// </summary>
|
|
private List<int> GetCategoryPath()
|
|
{
|
|
var result = new List<int>();
|
|
|
|
// 优先从 _raw.category 数组解析
|
|
var category = _rawNode["category"] as JArray;
|
|
if (category != null && category.Count > 0)
|
|
{
|
|
foreach (var cat in category)
|
|
{
|
|
var _catIdStr = cat["catId"]?.Value<string>();
|
|
if (int.TryParse(_catIdStr, out var catId) && catId > 0)
|
|
result.Add(catId);
|
|
}
|
|
if (result.Count > 0) return result;
|
|
}
|
|
|
|
// 回退到顶层 catId 字符串
|
|
var catIdStr = GetStringValue(_jsonRoot, "catId");
|
|
if (!string.IsNullOrEmpty(catIdStr))
|
|
{
|
|
foreach (var part in catIdStr.Split('/'))
|
|
{
|
|
if (int.TryParse(part.Trim(), out var catId) && catId > 0)
|
|
result.Add(catId);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从 "1208/1242/1244" 解析最后一个类目ID
|
|
/// </summary>
|
|
private int ParseLastCatId(string catIdStr)
|
|
{
|
|
if (string.IsNullOrEmpty(catIdStr))
|
|
return 0;
|
|
|
|
var parts = catIdStr.Split('/');
|
|
if (parts.Length == 0) return 0;
|
|
|
|
var lastPart = parts[parts.Length - 1].Trim();
|
|
return int.TryParse(lastPart, out var result) ? result : 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取主图列表
|
|
/// </summary>
|
|
private List<string> GetMainImages()
|
|
{
|
|
var result = new List<string>();
|
|
|
|
// 优先从 _raw.headImg 解析
|
|
var headImg = _rawNode["headImg"] 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;
|
|
}
|
|
|
|
// 回退到顶层 headImg / headImgs
|
|
var topHeadImg = _jsonRoot.SelectToken("$.headImg") as JArray
|
|
?? _jsonRoot.SelectToken("$.headImgs") as JArray;
|
|
if (topHeadImg != null)
|
|
{
|
|
foreach (var img in topHeadImg)
|
|
{
|
|
var url = img?.Value<string>();
|
|
if (!string.IsNullOrEmpty(url))
|
|
result.Add(url);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取详情图列表
|
|
/// </summary>
|
|
private List<string> GetDetailImages()
|
|
{
|
|
var result = new List<string>();
|
|
|
|
// 从 _raw.detail.detailImg 解析
|
|
var detail = _rawNode["detail"] as JObject;
|
|
if (detail == null)
|
|
return result;
|
|
|
|
var detailImg = detail["detailImg"] 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>
|
|
/// 获取商品属性列表(从 _raw.param 解析)
|
|
/// </summary>
|
|
private List<Property> GetProperties()
|
|
{
|
|
var result = new List<Property>();
|
|
var param = _rawNode["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;
|
|
}
|
|
private DateTime? GetListingTime()
|
|
{
|
|
string time = _rawNode["listingTime"].ToString();
|
|
if (time != null)
|
|
{
|
|
if (int.TryParse(time, out int v))
|
|
{
|
|
return NaiBa.Util.Helper.DateTimeHelper.IntStampToDateTime(v);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
#endregion
|
|
|
|
#region SKU和规格解析
|
|
|
|
/// <summary>
|
|
/// 获取SKU和规格信息
|
|
/// </summary>
|
|
private Tuple<List<SkuItem>, List<SpecItem>> GetSkusAndSpecs()
|
|
{
|
|
var skuList = new List<SkuItem>();
|
|
var specList = new List<SpecItem>();
|
|
|
|
if (_skuInfoList == null || _skuInfoList.Count == 0)
|
|
return new Tuple<List<SkuItem>, List<SpecItem>>(skuList, specList);
|
|
|
|
// 构建 skuSpecs 映射(skuId -> spec文本)
|
|
var skuSpecDict = new Dictionary<string, string>();
|
|
if (_skuSpecs != null)
|
|
{
|
|
foreach (var spec in _skuSpecs)
|
|
{
|
|
var skuId = spec["skuId"]?.Value<string>();
|
|
var specText = spec["spec"]?.Value<string>();
|
|
specText = specText.Replace('+', ';');
|
|
if (!string.IsNullOrEmpty(skuId) && !string.IsNullOrEmpty(specText))
|
|
{
|
|
skuSpecDict[skuId] = specText;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 收集所有规格
|
|
var specDict = new Dictionary<string, HashSet<string>>();
|
|
|
|
foreach (var sku in _skuInfoList)
|
|
{
|
|
var skuObj = sku as JObject;
|
|
if (skuObj == null) continue;
|
|
|
|
var skuId = skuObj["skuId"]?.Value<string>() ?? string.Empty;
|
|
if (string.IsNullOrEmpty(skuId)) continue;
|
|
|
|
// 解析规格
|
|
string specText = string.Empty;
|
|
var specValues = new List<string>();
|
|
|
|
if (skuSpecDict.TryGetValue(skuId, out var cachedSpec))
|
|
{
|
|
specText = cachedSpec;
|
|
// 解析 "颜色分类:牛油果/尺码:S" 格式
|
|
foreach (var part in cachedSpec.Split(new char[] { '/', '+', ';' }, StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var kv = part.Split(':');
|
|
if (kv.Length == 2)
|
|
{
|
|
var key = kv[0].Trim();
|
|
var value = kv[1].Trim();
|
|
specValues.Add($"{key}:{value}");
|
|
|
|
if (!specDict.ContainsKey(key))
|
|
specDict[key] = new HashSet<string>();
|
|
specDict[key].Add(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 价格解析:优先使用 estimatePrice(优惠后),否则用 originPrice
|
|
var originPrice = skuObj["originPrice"]?.Value<decimal>() ?? 0;
|
|
var estimatePrice = skuObj["estimatePrice"]?.Value<decimal>() ?? 0;
|
|
var price = skuObj["price"]?.Value<decimal>() ?? 0;
|
|
|
|
// 实际售价:优先 estimatePrice,其次 price,最后 originPrice
|
|
var actualPrice = estimatePrice > 0 ? estimatePrice
|
|
: price > 0 ? price
|
|
: originPrice;
|
|
|
|
// 库存:从 spuTotalStock 均分(SKU级别无库存字段)
|
|
var totalStock = GetTotalStock();
|
|
var skuCount = _skuInfoList.Count;
|
|
var stockPerSku = skuCount > 0 ? totalStock / skuCount : 0;
|
|
|
|
skuList.Add(new SkuItem
|
|
{
|
|
SkuId = skuId,
|
|
Spec = specText,
|
|
Price = actualPrice / 100, // 分转元
|
|
OriginalPrice = originPrice.ToString(),
|
|
Stock = stockPerSku,
|
|
SpecImage = string.Empty // 该结构无SKU图片
|
|
});
|
|
}
|
|
|
|
// 构建规格列表(优先从 saleAttrs 构建)
|
|
if (_saleAttrs != null && _saleAttrs.Count > 0)
|
|
{
|
|
foreach (var attr in _saleAttrs)
|
|
{
|
|
var key = attr["key"]?.Value<string>() ?? string.Empty;
|
|
var values = attr["values"] as JArray;
|
|
if (values == null) continue;
|
|
|
|
foreach (var value in values)
|
|
{
|
|
var valueStr = value?.Value<string>() ?? string.Empty;
|
|
if (!string.IsNullOrEmpty(valueStr))
|
|
{
|
|
specList.Add(new SpecItem
|
|
{
|
|
SpecKey = key,
|
|
SpecValue = valueStr,
|
|
SpecKeyId = 0,
|
|
SpecValueId = 0
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 回退到从SKU中收集的规格
|
|
foreach (var spec in specDict)
|
|
{
|
|
foreach (var value in spec.Value)
|
|
{
|
|
specList.Add(new SpecItem
|
|
{
|
|
SpecKey = spec.Key,
|
|
SpecValue = value,
|
|
SpecKeyId = 0,
|
|
SpecValueId = 0
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Tuple<List<SkuItem>, List<SpecItem>>(skuList, specList);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取总库存
|
|
/// </summary>
|
|
private int GetTotalStock()
|
|
{
|
|
var spuTotalStock = _rawNode["spuTotalStock"] as JObject;
|
|
if (spuTotalStock == null) return 0;
|
|
|
|
var stockNumStr = spuTotalStock["stockNum"]?.Value<string>();
|
|
if (int.TryParse(stockNumStr, out var result))
|
|
return result;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取最低价格(元)
|
|
/// </summary>
|
|
private decimal GetMinPrice()
|
|
{
|
|
if (_skuInfoList == null || _skuInfoList.Count == 0)
|
|
return 0;
|
|
|
|
var minPrice = decimal.MaxValue;
|
|
|
|
foreach (var sku in _skuInfoList)
|
|
{
|
|
var skuObj = sku as JObject;
|
|
if (skuObj == null) continue;
|
|
|
|
var estimatePrice = skuObj["estimatePrice"]?.Value<decimal>() ?? 0;
|
|
var price = skuObj["price"]?.Value<decimal>() ?? 0;
|
|
var actualPrice = estimatePrice > 0 ? estimatePrice : price;
|
|
|
|
if (actualPrice > 0 && actualPrice < minPrice)
|
|
minPrice = actualPrice;
|
|
}
|
|
|
|
return minPrice > 0 && minPrice != decimal.MaxValue ? minPrice / 100 : 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
|
|
}
|
|
} |