510 lines
14 KiB
C#
510 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;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace TemuKJD.BLL.ImportConverters
|
||
{
|
||
/// <summary>
|
||
/// 微信小商店(新版商品详情接口)转拼多多数据源
|
||
/// 对应示例:title / subTitle / headImg / category / param / brandId /
|
||
/// skuIdList / skuList.skuInfo / detail.detailImg / expressInfo 等字段
|
||
/// 与 V1(out_product_id / skus / cats_v2 / head_imgs / attrs / extra_service 等下划线字段)格式不同
|
||
/// </summary>
|
||
internal class ConverterWXToPddV2 : GoodsConverterBase
|
||
{
|
||
private JObject jsonRoot;
|
||
|
||
public override string ConverterName => "微信转拼多多数据源V2";
|
||
|
||
public override PlatformType Platform => PlatformType.WX;
|
||
|
||
|
||
protected override bool IsMatchProvider()
|
||
{
|
||
try
|
||
{
|
||
jsonRoot = JsonConvert.DeserializeObject<JObject>(RawData);
|
||
if (jsonRoot == null) return false;
|
||
|
||
// 1. 检查 extraService 是否存在且为对象类型(驼峰命名,区别于V1的 extra_service)
|
||
var extraService = jsonRoot.SelectToken("$.extraService");
|
||
if (extraService == null || extraService.Type != JTokenType.Object) return false;
|
||
|
||
// 2. 检查 skuList.skuInfo 是否存在且为数组(区别于V1的 skus 数组)
|
||
var skuInfo = jsonRoot.SelectToken("$.skuList.skuInfo");
|
||
if (skuInfo == null || skuInfo.Type != JTokenType.Array) return false;
|
||
|
||
var skuInfoArray = skuInfo as JArray;
|
||
if (skuInfoArray.Count == 0) return false;
|
||
|
||
// 3. 检查第一个SKU是否包含 skuId 字段(区别于V1的 out_sku_id)
|
||
var skuId = skuInfoArray[0]?.SelectToken("$.skuId")?.ToString();
|
||
if (string.IsNullOrEmpty(skuId)) return false;
|
||
|
||
// 4. 检查 skuIdList.skuIds 是否存在
|
||
var skuIds = jsonRoot.SelectToken("$.skuIdList.skuIds");
|
||
if (skuIds == null || skuIds.Type != JTokenType.Array) return false;
|
||
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
protected override PDDFrontProductInfo5 ConverterProvider()
|
||
{
|
||
var product = new PDDFrontProductInfo5();
|
||
|
||
product.Platform = Platform;
|
||
// 解析JSON数据
|
||
var jsonData = JsonConvert.DeserializeObject<JObject>(RawData);
|
||
|
||
// 商品ID:优先取 expressInfo.productId,取不到则从文件名解析
|
||
product.GoodsId = GetGoodsId(jsonData);
|
||
|
||
// 商品基本信息
|
||
product.Title = jsonData["title"]?.Value<string>() ?? "";
|
||
product.Description = jsonData["shortTitle"]?.Value<string>()
|
||
?? jsonData["subTitle"]?.Value<string>()
|
||
?? "";
|
||
|
||
// 品牌ID
|
||
product.BrandId = jsonData["brandId"]?.Value<long>() ?? 0;
|
||
|
||
// 类目信息
|
||
product.CategoryId = GetCategoryIdFromCats(jsonData);
|
||
product.CategoryPath = GetCategoryPathFromCats(jsonData);
|
||
|
||
// 主图
|
||
product.MainImages = GetMainImagesFromNewFormat(jsonData);
|
||
|
||
// 详情图
|
||
product.DetailImages = GetDetailImagesFromNewFormat(jsonData);
|
||
|
||
// 商品属性(来自 param 数组)
|
||
product.Propertys = GetPropertiesFromNewFormat(jsonData);
|
||
|
||
// SKU和规格信息
|
||
product.Skus = GetSkusFromNewFormat(jsonData);
|
||
product.Specs = GetSpecsFromNewFormat(jsonData);
|
||
|
||
// 其他默认值
|
||
product.ThumbUrl = jsonData["headImg"]?[0]?.Value<string>() ?? "";
|
||
product.MallId = 0; // 该格式中没有mallID
|
||
product.Price = GetMinPrice(jsonData); // 从SKU列表中获取最低价
|
||
product.Videos = new List<string>(); // 该格式中没有视频信息
|
||
|
||
return product;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取商品ID:优先 expressInfo.productId,其次文件名(若含下划线取最后一段数字)
|
||
/// </summary>
|
||
private long GetGoodsId(JObject jsonData)
|
||
{
|
||
var productId = jsonData.SelectToken("$.expressInfo.productId")?.ToString();
|
||
if (!string.IsNullOrEmpty(productId))
|
||
{
|
||
var idFromExpress = productId.ParseToLong();
|
||
if (idFromExpress > 0) return idFromExpress;
|
||
}
|
||
|
||
var fileName = Path.GetFileNameWithoutExtension(FilePath);
|
||
if (!string.IsNullOrEmpty(fileName) && fileName.Contains("_"))
|
||
{
|
||
var lastSegment = fileName.Split('_').Last();
|
||
var idFromFile = lastSegment.ParseToLong();
|
||
if (idFromFile > 0) return idFromFile;
|
||
}
|
||
|
||
return fileName.ParseToLong();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从category中获取类目ID(取最细分类目,即最后一个)
|
||
/// </summary>
|
||
private int GetCategoryIdFromCats(JObject jsonData)
|
||
{
|
||
var cats = jsonData["category"] as JArray;
|
||
if (cats == null || cats.Count == 0)
|
||
return 0;
|
||
|
||
var lastCat = cats.Last;
|
||
return lastCat?["catId"]?.Value<int>() ?? 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从category中获取类目路径
|
||
/// </summary>
|
||
private List<int> GetCategoryPathFromCats(JObject jsonData)
|
||
{
|
||
var result = new List<int>();
|
||
var cats = jsonData["category"] as JArray;
|
||
|
||
if (cats == null)
|
||
return result;
|
||
|
||
foreach (var cat in cats)
|
||
{
|
||
var catId = cat["catId"]?.Value<int>();
|
||
if (catId.HasValue && catId.Value > 0)
|
||
result.Add(catId.Value);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取主图
|
||
/// </summary>
|
||
private List<string> GetMainImagesFromNewFormat(JObject jsonData)
|
||
{
|
||
var result = new List<string>();
|
||
var headImg = jsonData["headImg"] as JArray;
|
||
|
||
if (headImg == null)
|
||
return result;
|
||
|
||
foreach (var img in headImg)
|
||
{
|
||
var url = img?.Value<string>();
|
||
if (!string.IsNullOrEmpty(url))
|
||
result.Add(url);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取详情图(detail.detailImg)
|
||
/// </summary>
|
||
private List<string> GetDetailImagesFromNewFormat(JObject jsonData)
|
||
{
|
||
var result = new List<string>();
|
||
var detail = jsonData["detail"];
|
||
|
||
if (detail == null)
|
||
return result;
|
||
|
||
var imgs = detail["detailImg"] as JArray;
|
||
if (imgs == null)
|
||
return result;
|
||
|
||
foreach (var img in imgs)
|
||
{
|
||
var url = img?.Value<string>();
|
||
if (!string.IsNullOrEmpty(url))
|
||
result.Add(url);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取商品属性(param 数组,每项 categorys[0]为属性名,categorys[1..]为属性值)
|
||
/// </summary>
|
||
private List<Property> GetPropertiesFromNewFormat(JObject jsonData)
|
||
{
|
||
var result = new List<Property>();
|
||
var paramList = jsonData["param"] as JArray;
|
||
|
||
if (paramList == null)
|
||
return result;
|
||
|
||
foreach (var param in paramList)
|
||
{
|
||
var categorys = param["categorys"] as JArray;
|
||
if (categorys == null || categorys.Count < 2)
|
||
continue;
|
||
|
||
var key = categorys[0]?["name"]?.Value<string>();
|
||
var value = string.Join(";", categorys
|
||
.Skip(1)
|
||
.Select(c => c["name"]?.Value<string>())
|
||
.Where(v => !string.IsNullOrEmpty(v)));
|
||
|
||
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value))
|
||
{
|
||
result.Add(new Property
|
||
{
|
||
Key = key,
|
||
Value = value
|
||
});
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从单个SKU节点中提取规格键值对(兼容极少数直接把属性挂在sku节点上的情况)
|
||
/// 正常情况下规格信息在顶层的 productSaleAttrs 中,见 BuildSkuIdToAttrsMap
|
||
/// </summary>
|
||
private List<KeyValuePair<string, string>> GetSkuAttrPairs(JToken sku)
|
||
{
|
||
var result = new List<KeyValuePair<string, string>>();
|
||
|
||
JArray attrArray = sku["skuAttrs"] as JArray
|
||
?? sku["attrs"] as JArray
|
||
?? sku["specAttrs"] as JArray
|
||
?? sku["saleAttrList"] as JArray
|
||
?? sku["attrList"] as JArray;
|
||
|
||
if (attrArray == null)
|
||
return result;
|
||
|
||
foreach (var attr in attrArray)
|
||
{
|
||
var key = attr["attrKey"]?.Value<string>() ?? attr["key"]?.Value<string>() ?? attr["name"]?.Value<string>();
|
||
var value = attr["attrValue"]?.Value<string>() ?? attr["value"]?.Value<string>();
|
||
|
||
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value))
|
||
result.Add(new KeyValuePair<string, string>(key, value));
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从顶层 productSaleAttrs 构建 skuId -> [(规格名,规格值)] 的映射
|
||
/// productSaleAttrs 结构:[{ "key":"型号", "items":[{ "value":"xxx", "imgUrl":"xxx", "skuIds":["xxx"] }] }]
|
||
/// </summary>
|
||
private Dictionary<string, List<KeyValuePair<string, string>>> BuildSkuIdToAttrsMap(JObject jsonData)
|
||
{
|
||
var map = new Dictionary<string, List<KeyValuePair<string, string>>>();
|
||
var saleAttrs = jsonData["productSaleAttrs"] as JArray;
|
||
if (saleAttrs == null)
|
||
return map;
|
||
|
||
foreach (var attr in saleAttrs)
|
||
{
|
||
var key = attr["key"]?.Value<string>();
|
||
var items = attr["items"] as JArray;
|
||
if (string.IsNullOrEmpty(key) || items == null)
|
||
continue;
|
||
|
||
foreach (var item in items)
|
||
{
|
||
var value = item["value"]?.Value<string>();
|
||
var skuIds = item["skuIds"] as JArray;
|
||
if (string.IsNullOrEmpty(value) || skuIds == null)
|
||
continue;
|
||
|
||
foreach (var skuIdToken in skuIds)
|
||
{
|
||
var skuId = skuIdToken?.Value<string>();
|
||
if (string.IsNullOrEmpty(skuId))
|
||
continue;
|
||
|
||
if (!map.TryGetValue(skuId, out var list))
|
||
{
|
||
list = new List<KeyValuePair<string, string>>();
|
||
map[skuId] = list;
|
||
}
|
||
list.Add(new KeyValuePair<string, string>(key, value));
|
||
}
|
||
}
|
||
}
|
||
|
||
return map;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从顶层 productSaleAttrs 构建 skuId -> 规格图片(imgUrl) 的映射
|
||
/// </summary>
|
||
private Dictionary<string, string> BuildSkuIdToSpecImageMap(JObject jsonData)
|
||
{
|
||
var map = new Dictionary<string, string>();
|
||
var saleAttrs = jsonData["productSaleAttrs"] as JArray;
|
||
if (saleAttrs == null)
|
||
return map;
|
||
|
||
foreach (var attr in saleAttrs)
|
||
{
|
||
var items = attr["items"] as JArray;
|
||
if (items == null)
|
||
continue;
|
||
|
||
foreach (var item in items)
|
||
{
|
||
var imgUrl = item["imgUrl"]?.Value<string>();
|
||
var skuIds = item["skuIds"] as JArray;
|
||
if (string.IsNullOrEmpty(imgUrl) || skuIds == null)
|
||
continue;
|
||
|
||
foreach (var skuIdToken in skuIds)
|
||
{
|
||
var skuId = skuIdToken?.Value<string>();
|
||
if (string.IsNullOrEmpty(skuId))
|
||
continue;
|
||
|
||
if (!map.ContainsKey(skuId))
|
||
map[skuId] = imgUrl;
|
||
}
|
||
}
|
||
}
|
||
|
||
return map;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取SKU(skuList.skuInfo),规格信息优先取自顶层 productSaleAttrs
|
||
/// </summary>
|
||
private List<SkuItem> GetSkusFromNewFormat(JObject jsonData)
|
||
{
|
||
var result = new List<SkuItem>();
|
||
var skus = jsonData.SelectToken("$.skuList.skuInfo") as JArray;
|
||
|
||
if (skus == null)
|
||
return result;
|
||
|
||
var skuIdToAttrsMap = BuildSkuIdToAttrsMap(jsonData);
|
||
var skuIdToSpecImageMap = BuildSkuIdToSpecImageMap(jsonData);
|
||
|
||
foreach (var sku in skus)
|
||
{
|
||
var skuId = sku["skuId"]?.Value<string>() ?? "";
|
||
|
||
// 优先使用 productSaleAttrs 中按 skuId 匹配到的规格,取不到时兼容旧字段
|
||
List<KeyValuePair<string, string>> attrPairs;
|
||
if (!string.IsNullOrEmpty(skuId) && skuIdToAttrsMap.TryGetValue(skuId, out var mappedPairs))
|
||
attrPairs = mappedPairs;
|
||
else
|
||
attrPairs = GetSkuAttrPairs(sku);
|
||
|
||
var specValues = attrPairs.Select(a => $"{a.Key}:{a.Value}").ToList();
|
||
|
||
// price 单位为分,需要转换为元
|
||
var priceInCents = sku["price"]?.Value<decimal>() ?? 0;
|
||
var priceInYuan = priceInCents / 100;
|
||
|
||
var originPriceInCents = sku["originPrice"]?.Value<decimal>() ?? priceInCents;
|
||
|
||
string specImage = null;
|
||
if (!string.IsNullOrEmpty(skuId))
|
||
skuIdToSpecImageMap.TryGetValue(skuId, out specImage);
|
||
|
||
result.Add(new SkuItem
|
||
{
|
||
SkuId = skuId,
|
||
Spec = string.Join(";", specValues),
|
||
Price = priceInYuan,
|
||
OriginalPrice = originPriceInCents.ToString(), // 保留原始价格(分)
|
||
Stock = sku["stockNum"]?.Value<int>() ?? 999, // 无库存字段时使用默认值
|
||
SpecImage = specImage
|
||
?? sku["thumbImg"]?.Value<string>()
|
||
?? sku["skuImg"]?.Value<string>()
|
||
?? ""
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取规格:优先从顶层 productSaleAttrs 汇总(key为规格名,items[].value为规格值),
|
||
/// 若不存在该字段则退回从每个SKU自身属性字段中提取(兼容旧/异形数据)
|
||
/// </summary>
|
||
private List<SpecItem> GetSpecsFromNewFormat(JObject jsonData)
|
||
{
|
||
var result = new List<SpecItem>();
|
||
var saleAttrs = jsonData["productSaleAttrs"] as JArray;
|
||
|
||
if (saleAttrs != null && saleAttrs.Count > 0)
|
||
{
|
||
foreach (var attr in saleAttrs)
|
||
{
|
||
var key = attr["key"]?.Value<string>();
|
||
var items = attr["items"] as JArray;
|
||
if (string.IsNullOrEmpty(key) || items == null)
|
||
continue;
|
||
|
||
foreach (var item in items)
|
||
{
|
||
var value = item["value"]?.Value<string>();
|
||
if (string.IsNullOrEmpty(value))
|
||
continue;
|
||
|
||
// 去重
|
||
if (result.Any(x => x.SpecKey == key && x.SpecValue == value))
|
||
continue;
|
||
|
||
result.Add(new SpecItem
|
||
{
|
||
SpecKey = key,
|
||
SpecValue = value,
|
||
SpecKeyId = 0, // 该格式中没有ID
|
||
SpecValueId = 0
|
||
});
|
||
}
|
||
}
|
||
|
||
if (result.Count > 0)
|
||
return result;
|
||
}
|
||
|
||
// 兼容处理:没有 productSaleAttrs 时,尝试从每个SKU自身属性字段中提取
|
||
var skus = jsonData.SelectToken("$.skuList.skuInfo") as JArray;
|
||
if (skus == null)
|
||
return result;
|
||
|
||
foreach (var sku in skus)
|
||
{
|
||
var attrPairs = GetSkuAttrPairs(sku);
|
||
|
||
foreach (var pair in attrPairs)
|
||
{
|
||
var key = pair.Key;
|
||
var value = pair.Value;
|
||
|
||
if (result.Any(x => x.SpecKey == key && x.SpecValue == value))
|
||
continue;
|
||
|
||
result.Add(new SpecItem
|
||
{
|
||
SpecKey = key,
|
||
SpecValue = value,
|
||
SpecKeyId = 0,
|
||
SpecValueId = 0
|
||
});
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取最低价格:优先使用 skuList.minPrice,否则从 skuInfo 中计算
|
||
/// </summary>
|
||
private decimal GetMinPrice(JObject jsonData)
|
||
{
|
||
var minPriceInCents = jsonData.SelectToken("$.skuList.minPrice")?.Value<decimal>();
|
||
if (minPriceInCents.HasValue && minPriceInCents.Value > 0)
|
||
return minPriceInCents.Value / 100;
|
||
|
||
var skus = jsonData.SelectToken("$.skuList.skuInfo") as JArray;
|
||
if (skus == null || skus.Count == 0)
|
||
return 0;
|
||
|
||
var minPrice = decimal.MaxValue;
|
||
foreach (var sku in skus)
|
||
{
|
||
var price = sku["price"]?.Value<decimal>() ?? 0;
|
||
if (price > 0 && price < minPrice)
|
||
minPrice = price;
|
||
}
|
||
|
||
return minPrice > 0 && minPrice != decimal.MaxValue ? minPrice / 100 : 0;
|
||
}
|
||
}
|
||
} |