新增 WX to PDD V3
This commit is contained in:
1 parent
610e75dfce
commit
9e71c54414
4 files changed
+376
No files matched your search
@@ -15,6 +15,7 @@ namespace TemuKJD.BLL.ImportConverts
|
||||
new ConverterToPddV5(),
|
||||
new ConverterWXToPddV1(),
|
||||
new ConverterWXToPddV2(),
|
||||
new ConverterWXToPddV3(),
|
||||
};
|
||||
public static PDDFrontProductInfo5 Convert(string filePath)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
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
|
||||
{
|
||||
internal class ConverterWXToPddV3 : GoodsConverterBase
|
||||
{
|
||||
private JObject jsonRoot;
|
||||
|
||||
public override string ConverterName => "微信转拼多多数据源V3";
|
||||
|
||||
public override PlatformType Platform => PlatformType.WX;
|
||||
|
||||
|
||||
protected override bool IsMatchProvider()
|
||||
{
|
||||
try
|
||||
{
|
||||
jsonRoot = JsonConvert.DeserializeObject<JObject>(RawData);
|
||||
if (jsonRoot == null) return false;
|
||||
|
||||
// V3 特征:真实商品数据被包裹在 body 节点下
|
||||
var body = jsonRoot.SelectToken("$.body");
|
||||
if (body == null || body.Type != JTokenType.Object) return false;
|
||||
|
||||
// 1. 检查 body.extra_service 是否存在且为对象类型
|
||||
var extraService = body.SelectToken("$.extra_service");
|
||||
if (extraService == null || extraService.Type != JTokenType.Object) return false;
|
||||
|
||||
// 2. 检查 body.out_product_id 是否存在且为字符串类型(非空)
|
||||
var outProductId = body.SelectToken("$.out_product_id")?.ToString();
|
||||
if (string.IsNullOrEmpty(outProductId)) return false;
|
||||
|
||||
// 3. 检查 body.skus 是否存在且为数组,且第一个元素包含 out_sku_id
|
||||
var skus = body.SelectToken("$.skus");
|
||||
if (skus == null) return true;
|
||||
if (skus.Type != JTokenType.Array) return false;
|
||||
var skusArray = skus as JArray;
|
||||
if (skusArray.Count == 0) return true;
|
||||
|
||||
var outSkuId = skusArray[0]?.SelectToken("$.out_sku_id")?.ToString();
|
||||
if (string.IsNullOrEmpty(outSkuId)) return false;
|
||||
|
||||
// 四个条件全部通过(比 V1 多了 body 包装校验)
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override PDDFrontProductInfo5 ConverterProvider()
|
||||
{
|
||||
var product = new PDDFrontProductInfo5();
|
||||
|
||||
product.Platform = Platform;
|
||||
|
||||
// 解析JSON数据
|
||||
var jsonData = JsonConvert.DeserializeObject<JObject>(RawData);
|
||||
var body = jsonData["body"] as JObject ?? new JObject();
|
||||
var notes = jsonData["notes"] as JObject;
|
||||
var source = notes?["source"] as JObject;
|
||||
|
||||
// 商品ID:优先用文件名解析,解析不到再退化使用 body.out_product_id / notes.source.product_id
|
||||
product.GoodsId = Path.GetFileNameWithoutExtension(FilePath).ParseToLong();
|
||||
if (product.GoodsId <= 0)
|
||||
{
|
||||
var fallbackId = body["out_product_id"]?.Value<string>() ?? source?["product_id"]?.Value<string>();
|
||||
product.GoodsId = fallbackId.ParseToLong();
|
||||
}
|
||||
|
||||
// 商品基本信息
|
||||
product.Title = body["title"]?.Value<string>() ?? source?["title"]?.Value<string>() ?? "";
|
||||
// 新格式没有 short_title,退化取 desc_info.desc
|
||||
product.Description = body["short_title"]?.Value<string>()
|
||||
?? body["desc_info"]?["desc"]?.Value<string>()
|
||||
?? "";
|
||||
|
||||
// 品牌ID - 转换为long
|
||||
product.BrandId = body["brand_id"]?.Value<long>() ?? 0;
|
||||
|
||||
// 类目信息
|
||||
product.CategoryId = GetCategoryIdFromCats(body);
|
||||
product.CategoryPath = GetCategoryPathFromCats(body);
|
||||
|
||||
// 主图
|
||||
product.MainImages = GetMainImagesFromNewFormat(body);
|
||||
|
||||
// 详情图
|
||||
product.DetailImages = GetDetailImagesFromNewFormat(body);
|
||||
|
||||
// 商品属性
|
||||
product.Propertys = GetPropertiesFromNewFormat(body);
|
||||
|
||||
// SKU和规格信息(SKU 会结合 notes.sku_price / stock_num 做补全)
|
||||
product.Skus = GetSkusFromNewFormat(body, notes);
|
||||
product.Specs = GetSpecsFromNewFormat(body);
|
||||
|
||||
// 其他默认值
|
||||
product.ThumbUrl = body["head_imgs"]?[0]?.Value<string>() ?? "";
|
||||
product.MallId = 0; // body 中没有 mallID
|
||||
product.Price = GetMinPrice(body); // 从SKU中获取最低价
|
||||
product.Videos = new List<string>(); // body 中没有视频信息
|
||||
|
||||
return product;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 从cats_v2中获取类目ID
|
||||
/// </summary>
|
||||
private int GetCategoryIdFromCats(JObject body)
|
||||
{
|
||||
var cats = body["cats_v2"] as JArray;
|
||||
if (cats == null || cats.Count == 0)
|
||||
return 0;
|
||||
|
||||
// 获取最后一个类目(最细分类目)
|
||||
var lastCat = cats.Last;
|
||||
return lastCat?["cat_id"]?.Value<int>() ?? 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从cats_v2中获取类目路径
|
||||
/// </summary>
|
||||
private List<int> GetCategoryPathFromCats(JObject body)
|
||||
{
|
||||
var result = new List<int>();
|
||||
var cats = body["cats_v2"] as JArray;
|
||||
|
||||
if (cats == null)
|
||||
return result;
|
||||
|
||||
foreach (var cat in cats)
|
||||
{
|
||||
var catId = cat["cat_id"]?.Value<int>();
|
||||
if (catId.HasValue && catId.Value > 0)
|
||||
result.Add(catId.Value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取主图
|
||||
/// </summary>
|
||||
private List<string> GetMainImagesFromNewFormat(JObject body)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var headImgs = body["head_imgs"] as JArray;
|
||||
|
||||
if (headImgs == null)
|
||||
return result;
|
||||
|
||||
foreach (var img in headImgs)
|
||||
{
|
||||
var url = img?.Value<string>();
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
result.Add(url);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取详情图
|
||||
/// </summary>
|
||||
private List<string> GetDetailImagesFromNewFormat(JObject body)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var descInfo = body["desc_info"];
|
||||
|
||||
if (descInfo == null)
|
||||
return result;
|
||||
|
||||
var imgs = descInfo["imgs"] 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>
|
||||
/// 获取商品属性
|
||||
/// </summary>
|
||||
private List<Property> GetPropertiesFromNewFormat(JObject body)
|
||||
{
|
||||
var result = new List<Property>();
|
||||
var attrs = body["attrs"] as JArray;
|
||||
|
||||
if (attrs == null)
|
||||
return result;
|
||||
|
||||
foreach (var attr in attrs)
|
||||
{
|
||||
var key = attr["attr_key"]?.Value<string>();
|
||||
var value = attr["attr_value"]?.Value<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value))
|
||||
{
|
||||
result.Add(new Property
|
||||
{
|
||||
Key = key,
|
||||
Value = value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取SKU。
|
||||
/// 相比 V1:
|
||||
/// 1. 库存优先取 body.skus[].stock_num(新格式已带该字段),取不到才用默认值 999;
|
||||
/// 2. 划线价/原价优先取 notes.sku_price 中按 out_sku_id 匹配到的 origin 字段(单位:分),
|
||||
/// 取不到则退化为 sale_price 本身。
|
||||
/// </summary>
|
||||
private List<SkuItem> GetSkusFromNewFormat(JObject body, JObject notes)
|
||||
{
|
||||
var result = new List<SkuItem>();
|
||||
var skus = body["skus"] as JArray;
|
||||
|
||||
if (skus == null)
|
||||
return result;
|
||||
|
||||
// 按 out_sku_id 建索引,方便用 notes.sku_price 补全划线价
|
||||
var skuPriceMap = new Dictionary<string, JObject>();
|
||||
var skuPriceList = notes?["sku_price"] as JArray;
|
||||
if (skuPriceList != null)
|
||||
{
|
||||
foreach (var item in skuPriceList)
|
||||
{
|
||||
if (item is JObject jo)
|
||||
{
|
||||
var outSkuId = jo["out_sku_id"]?.Value<string>();
|
||||
if (!string.IsNullOrEmpty(outSkuId))
|
||||
skuPriceMap[outSkuId] = jo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sku in skus)
|
||||
{
|
||||
var skuAttrs = sku["sku_attrs"] as JArray;
|
||||
var specValues = new List<string>();
|
||||
|
||||
if (skuAttrs != null)
|
||||
{
|
||||
foreach (var attr in skuAttrs)
|
||||
{
|
||||
var key = attr["attr_key"]?.Value<string>();
|
||||
var value = attr["attr_value"]?.Value<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value))
|
||||
specValues.Add($"{key}:{value}");
|
||||
}
|
||||
}
|
||||
|
||||
var outSkuId = sku["out_sku_id"]?.Value<string>() ?? "";
|
||||
|
||||
// sale_price 是分,需要转换为元
|
||||
var salePriceInCents = sku["sale_price"]?.Value<decimal>() ?? 0;
|
||||
var salePriceInYuan = salePriceInCents / 100;
|
||||
|
||||
// 划线价:优先用 notes.sku_price[].origin(分),取不到就退化用 sale_price
|
||||
var originPriceInCents = salePriceInCents;
|
||||
if (skuPriceMap.TryGetValue(outSkuId, out var priceInfo))
|
||||
{
|
||||
originPriceInCents = priceInfo["origin"]?.Value<decimal>() ?? salePriceInCents;
|
||||
}
|
||||
|
||||
// 库存:新格式 body.skus 已自带 stock_num,取不到才用默认值
|
||||
var stock = sku["stock_num"]?.Value<int>() ?? 999;
|
||||
|
||||
result.Add(new SkuItem
|
||||
{
|
||||
SkuId = outSkuId,
|
||||
Spec = string.Join(";", specValues),
|
||||
Price = salePriceInYuan,
|
||||
OriginalPrice = originPriceInCents.ToString(), // 保留划线价(分)
|
||||
Stock = stock,
|
||||
SpecImage = sku["thumb_img"]?.Value<string>() ?? ""
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取规格
|
||||
/// </summary>
|
||||
private List<SpecItem> GetSpecsFromNewFormat(JObject body)
|
||||
{
|
||||
var result = new List<SpecItem>();
|
||||
var skus = body["skus"] as JArray;
|
||||
|
||||
if (skus == null)
|
||||
return result;
|
||||
|
||||
foreach (var sku in skus)
|
||||
{
|
||||
var skuAttrs = sku["sku_attrs"] as JArray;
|
||||
if (skuAttrs == null)
|
||||
continue;
|
||||
|
||||
foreach (var attr in skuAttrs)
|
||||
{
|
||||
var specKey = attr["attr_key"]?.Value<string>() ?? "";
|
||||
var specValue = attr["attr_value"]?.Value<string>() ?? "";
|
||||
|
||||
// 去重
|
||||
if (result.Any(x => x.SpecKey == specKey && x.SpecValue == specValue))
|
||||
continue;
|
||||
|
||||
result.Add(new SpecItem
|
||||
{
|
||||
SpecKey = specKey,
|
||||
SpecValue = specValue,
|
||||
SpecKeyId = 0, // 新格式中没有ID
|
||||
SpecValueId = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取最低价格
|
||||
/// </summary>
|
||||
private decimal GetMinPrice(JObject body)
|
||||
{
|
||||
var skus = body["skus"] as JArray;
|
||||
if (skus == null || skus.Count == 0)
|
||||
return 0;
|
||||
|
||||
var minPrice = decimal.MaxValue;
|
||||
foreach (var sku in skus)
|
||||
{
|
||||
var price = sku["sale_price"]?.Value<decimal>() ?? 0;
|
||||
if (price > 0 && price < minPrice)
|
||||
minPrice = price;
|
||||
}
|
||||
|
||||
return minPrice > 0 ? minPrice / 100 : 0; // 转换为元
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,7 @@
|
||||
<Compile Include="BLL\HttpResponseWxAiTalkBLL.cs" />
|
||||
<Compile Include="BLL\ImportConverters\ConverterWXToPddV1.cs" />
|
||||
<Compile Include="BLL\ImportConverters\ConverterWXToPddV2.cs" />
|
||||
<Compile Include="BLL\ImportConverters\ConverterWXToPddV3.cs" />
|
||||
<Compile Include="BLL\ImportConverters\ConvertToPddFactory.cs" />
|
||||
<Compile Include="BLL\ImportConverters\ConverterToPddV5.cs" />
|
||||
<Compile Include="BLL\ImportConverters\IGoodsConverter.cs" />
|
||||
|
||||
@@ -100,6 +100,14 @@ namespace KJD.Test
|
||||
var frontProdInfo = ConverterToPddFactory.Convert(filePath);
|
||||
var isMatch = frontProdInfo != null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWXToPDDV3()
|
||||
{
|
||||
string filePath = @"C:\Users\chaih\Documents\xwechat_files\hongfei357_a567\msg\file\2026-08\10000528322732(1).json";
|
||||
var frontProdInfo = ConverterToPddFactory.Convert(filePath);
|
||||
var isMatch = frontProdInfo != null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user