Files
KJD/TEMUKJD/BLL/ImportConverters/ConverterWXToPddV2.cs
T
2026-08-27 15:27:42 +08:00

510 lines
14 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;
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;
}
}
}