修改 平台类型 存放位置
This commit is contained in:
1 parent
b18aad1f85
commit
9bc35bb2d6
11 files changed
+961
-39
No files matched your search
@@ -7,132 +7,133 @@ using System.Text;
|
|||||||
namespace Store.Users.Contracts.Dtos
|
namespace Store.Users.Contracts.Dtos
|
||||||
{
|
{
|
||||||
public class UserInfo
|
public class UserInfo
|
||||||
{/// <summary>
|
{
|
||||||
/// ID
|
/// <summary>
|
||||||
/// </summary>
|
/// ID
|
||||||
public int Id { get; set; }
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户昵称
|
/// 用户昵称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string NickName { get; set; }
|
public string NickName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户名称(登录账户)
|
/// 用户名称(登录账户)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 登录密码
|
/// 登录密码
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.Text.Json.Serialization.JsonIgnore]
|
[System.Text.Json.Serialization.JsonIgnore]
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// card_id
|
/// card_id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CardId { get; set; }
|
public string CardId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户分组,参见 <see cref="UserGroup"/>。
|
/// 用户分组,参见 <see cref="UserGroup"/>。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UserGroup GroupId { get; set; }
|
public UserGroup GroupId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 父账户用户名
|
/// 父账户用户名
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ParentUserName { get; set; }
|
public string ParentUserName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// oem编号,仅oem用户有值
|
/// oem编号,仅oem用户有值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string OemId { get; set; }
|
public string OemId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 手机号
|
/// 手机号
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Mobile { get; set; }
|
public string Mobile { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 添加时间
|
/// 添加时间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? AddTime { get; set; }
|
public DateTime? AddTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 修改时间
|
/// 修改时间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? MdyTime { get; set; }
|
public DateTime? MdyTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 公司名称
|
/// 公司名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CompanyName { get; set; }
|
public string CompanyName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 备注
|
/// 备注
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Notes { get; set; }
|
public string Notes { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 超管等级,参见 <see cref="UserAdminLevel"/>。
|
/// 超管等级,参见 <see cref="UserAdminLevel"/>。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UserAdminLevel? SaLevel { get; set; }
|
public UserAdminLevel? SaLevel { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 账户权限
|
/// 账户权限
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Authority { get; set; }
|
public string Authority { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 子账号被禁止展示的功能页面
|
/// 子账号被禁止展示的功能页面
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ForbiddenPage { get; set; }
|
public string ForbiddenPage { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 账户积分,1积分=1元
|
/// 账户积分,1积分=1元
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal Points { get; set; }
|
public decimal Points { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 积分历史累计充值金额
|
/// 积分历史累计充值金额
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? RechargePointsTotal { get; set; }
|
public decimal? RechargePointsTotal { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 店铺每续费一个月需要的积分,单位元
|
/// 店铺每续费一个月需要的积分,单位元
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ShopPointsPerMonth { get; set; }
|
public int ShopPointsPerMonth { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 可用余额
|
/// 可用余额
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? Balance { get; set; }
|
public decimal? Balance { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 累计充值金额
|
/// 累计充值金额
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? RechargeAmountTotal { get; set; }
|
public decimal? RechargeAmountTotal { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 每单中转仓快递费用,单位元,两位小数
|
/// 每单中转仓快递费用,单位元,两位小数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? PostagePerOrder { get; set; }
|
public decimal? PostagePerOrder { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 每单中转仓退货快递费用,单位元,两位小数
|
/// 每单中转仓退货快递费用,单位元,两位小数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? RefundPostagePerOrder { get; set; }
|
public decimal? RefundPostagePerOrder { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 礼物每单额外收取费用,单位元,两位小数
|
/// 礼物每单额外收取费用,单位元,两位小数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? GiftOemAmountPerOrder { get; set; }
|
public decimal? GiftOemAmountPerOrder { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 同步物流费用,单位元,两位小数
|
/// 同步物流费用,单位元,两位小数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? SyncLogisticsPerOrder { get; set; }
|
public decimal? SyncLogisticsPerOrder { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 是否主账户
|
/// 是否主账户
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public class UserSearchRequest
|
|||||||
{
|
{
|
||||||
[Range(1, 1000000)]
|
[Range(1, 1000000)]
|
||||||
public int Page { get; set; } = 1;
|
public int Page { get; set; } = 1;
|
||||||
[Range(1, 200)]
|
[Range(1, 2000)]
|
||||||
public int PageSize { get; set; } = 50;
|
public int PageSize { get; set; } = 50;
|
||||||
[StringLength(100)]
|
[StringLength(100)]
|
||||||
public string? Keyword { get; set; }
|
public string? Keyword { get; set; }
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using Store.Modules.CloudProduct.Entities;
|
|||||||
using Store.Modules.CloudProduct.ImportConverters;
|
using Store.Modules.CloudProduct.ImportConverters;
|
||||||
using Store.Modules.CloudProduct.Services;
|
using Store.Modules.CloudProduct.Services;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Store.Modules.CloudProduct.Controllers
|
namespace Store.Modules.CloudProduct.Controllers
|
||||||
{
|
{
|
||||||
@@ -23,10 +24,12 @@ namespace Store.Modules.CloudProduct.Controllers
|
|||||||
private readonly IRawDataStorage _rawDataStorage;
|
private readonly IRawDataStorage _rawDataStorage;
|
||||||
private readonly GoodsIndexJob _goodsIndexJob;
|
private readonly GoodsIndexJob _goodsIndexJob;
|
||||||
private readonly GoodsViolationService _violations;
|
private readonly GoodsViolationService _violations;
|
||||||
|
private readonly ILogger<GoodsController> _logger;
|
||||||
|
|
||||||
public GoodsController(IGoodsService goodsService, IOpenSearchService searchService, IRawDataStorage rawDataStorage, GoodsIndexJob goodsIndexJob, GoodsViolationService violations)
|
public GoodsController(IGoodsService goodsService, IOpenSearchService searchService, IRawDataStorage rawDataStorage, GoodsIndexJob goodsIndexJob, GoodsViolationService violations, ILogger<GoodsController> logger)
|
||||||
{
|
{
|
||||||
_violations = violations;
|
_violations = violations;
|
||||||
|
_logger = logger;
|
||||||
_goodsService = goodsService;
|
_goodsService = goodsService;
|
||||||
_searchService = searchService;
|
_searchService = searchService;
|
||||||
_rawDataStorage = rawDataStorage;
|
_rawDataStorage = rawDataStorage;
|
||||||
@@ -96,7 +99,7 @@ namespace Store.Modules.CloudProduct.Controllers
|
|||||||
int batchCount;
|
int batchCount;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
batchCount = await _goodsService.IndexPendingGoodsAsync(1000);
|
batchCount = await _goodsService.IndexPendingGoodsAsync(20000);
|
||||||
count += batchCount;
|
count += batchCount;
|
||||||
} while (batchCount > 0);
|
} while (batchCount > 0);
|
||||||
|
|
||||||
@@ -260,6 +263,35 @@ namespace Store.Modules.CloudProduct.Controllers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("/api/wx/data/import")]
|
||||||
|
[EndpointSummary("导入商品数据")]
|
||||||
|
public async Task<GoodsImportResult> ImportWXData([FromBody] string data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(data))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
}
|
||||||
|
var content = data;
|
||||||
|
var goods = GoodsConverterFactory.ConvertContent(content);
|
||||||
|
if (goods == null)
|
||||||
|
{
|
||||||
|
throw new AppException("未找到匹配的转换器,无法解析测试数据");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _goodsService.ImportGoodsAsync(new[]
|
||||||
|
{
|
||||||
|
new GoodsImportItem { Goods = goods, RawData = content }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "导入微信数据:");
|
||||||
|
throw new AppException("导入微信数据失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 导入接口:接收客户端批量上传的 JSON 文件,解析后入库并写入分词索引。
|
/// 导入接口:接收客户端批量上传的 JSON 文件,解析后入库并写入分词索引。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -398,7 +430,7 @@ namespace Store.Modules.CloudProduct.Controllers
|
|||||||
var node = JsonNode.Parse(raw);
|
var node = JsonNode.Parse(raw);
|
||||||
return node ?? throw new AppException("商品详情解析失败", ResultCode.BusinessError);
|
return node ?? throw new AppException("商品详情解析失败", ResultCode.BusinessError);
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new AppException(ex.Message);
|
throw new AppException(ex.Message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
using Store.Framework.Models;
|
||||||
using Store.Modules.CloudProduct.Entities;
|
using Store.Modules.CloudProduct.Entities;
|
||||||
using Store.Modules.CloudProduct.Enums;
|
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
namespace Store.Modules.CloudProduct.ImportConverters
|
namespace Store.Modules.CloudProduct.ImportConverters
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Store.CloudProduct.ImportConverters;
|
||||||
using Store.Modules.CloudProduct.Entities;
|
using Store.Modules.CloudProduct.Entities;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -12,6 +13,7 @@ namespace Store.Modules.CloudProduct.ImportConverters
|
|||||||
private static readonly List<IGoodsConverter> _converters = new List<IGoodsConverter>
|
private static readonly List<IGoodsConverter> _converters = new List<IGoodsConverter>
|
||||||
{
|
{
|
||||||
new PddGoodsConverter(),
|
new PddGoodsConverter(),
|
||||||
|
new WXGoodsConverter(),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
using Store.Framework.Models;
|
||||||
using Store.Modules.CloudProduct.Entities;
|
using Store.Modules.CloudProduct.Entities;
|
||||||
using Store.Modules.CloudProduct.Enums;
|
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
namespace Store.Modules.CloudProduct.ImportConverters
|
namespace Store.Modules.CloudProduct.ImportConverters
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
using MAX.Core.Extensions;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Org.BouncyCastle.Asn1.Cms;
|
||||||
|
using Store.Framework.Models;
|
||||||
|
using Store.Modules.CloudProduct.Entities;
|
||||||
|
using Store.Modules.CloudProduct.ImportConverters;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.ServerSentEvents;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace Store.CloudProduct.ImportConverters
|
||||||
|
{
|
||||||
|
internal class WXGoodsConverter : GoodsConverter
|
||||||
|
{
|
||||||
|
public override string ConverterName => nameof(WXGoodsConverter);
|
||||||
|
|
||||||
|
public override PlatformType Platform => PlatformType.WX;
|
||||||
|
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
|
||||||
|
|
||||||
|
protected override bool IsMatchCore()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_jsonRoot = JsonConvert.DeserializeObject<JObject>(RawData);
|
||||||
|
if (_jsonRoot == null) return false;
|
||||||
|
|
||||||
|
var detail = _jsonRoot.SelectToken("$.detail") as JObject;
|
||||||
|
if (detail == null) return false;
|
||||||
|
|
||||||
|
var product = detail.SelectToken("$.product") as JObject;
|
||||||
|
if (product == null) return false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
var info = product.SelectToken("$.info") as JObject;
|
||||||
|
if (info == null) return false;
|
||||||
|
|
||||||
|
var title = info.SelectToken("$.title")?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(title)) return false;
|
||||||
|
|
||||||
|
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 GoodsEntity ConvertCore()
|
||||||
|
{
|
||||||
|
var product = ConverterProvider();
|
||||||
|
var item = new
|
||||||
|
GoodsEntity
|
||||||
|
{
|
||||||
|
Platform = (byte)PlatformType.WX,
|
||||||
|
GoodsId = product.ProductId.ToLong(0),
|
||||||
|
Title = product.Title ?? string.Empty,
|
||||||
|
Description = product.Description,
|
||||||
|
ThumbUrl = product.ThumbUrl,
|
||||||
|
SoldQuantity = GetTotalSold(),
|
||||||
|
Status = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
item.MinPrice = ((long)product.Skus.Min(a=>a.Price));
|
||||||
|
item.MaxPrice = ((long)product.Skus.Max(a => a.Price));
|
||||||
|
item.CatId1 = product.CategoryIds.Count > 0 ? product.CategoryIds[0] : (int?)null;
|
||||||
|
item.CatId2 = product.CategoryIds.Count > 1 ? product.CategoryIds[1] : (int?)null;
|
||||||
|
item.CatId3 = product.CategoryIds.Count > 2 ? product.CategoryIds[2] : (int?)null;
|
||||||
|
item.CatId4 = product.CategoryIds.Count > 3 ? product.CategoryIds[3] : (int?)null;
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
private Product ConverterProvider()
|
||||||
|
{
|
||||||
|
var product = new Product
|
||||||
|
{
|
||||||
|
Platform = Platform,
|
||||||
|
ProductId = 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. 品牌(新模型无 BrandId,只有 Brand 名称)
|
||||||
|
var brandId = GetLongValue(_infoNode, "brand_id");
|
||||||
|
product.Brand = brandId > 0 ? brandId.ToString() : string.Empty;
|
||||||
|
|
||||||
|
// 3. 类目
|
||||||
|
product.CategoryId = GetCategoryId();
|
||||||
|
product.CategoryIds = GetCategoryPath();
|
||||||
|
|
||||||
|
// 4. 主图
|
||||||
|
product.MainImages = GetMainImages();
|
||||||
|
|
||||||
|
// 5. 详情图
|
||||||
|
product.DetailImages = GetDetailImages();
|
||||||
|
|
||||||
|
// 6. 属性
|
||||||
|
product.Attributes = GetProperties();
|
||||||
|
|
||||||
|
// 7. 规格 + SKU
|
||||||
|
var skuResult = GetSkusAndSpecs();
|
||||||
|
product.Skus = skuResult.Item1;
|
||||||
|
product.Specs = skuResult.Item2;
|
||||||
|
|
||||||
|
// 8. 媒体与视频(新模型只有单个 Video)
|
||||||
|
product.ThumbUrl = product.MainImages.FirstOrDefault() ?? string.Empty;
|
||||||
|
product.Video = GetVideoUrls().FirstOrDefault() ?? string.Empty;
|
||||||
|
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 核心字段解析
|
||||||
|
|
||||||
|
private string ParseGoodsId()
|
||||||
|
{
|
||||||
|
var productId = _productNode.SelectToken("$.product_key.product_id")?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(productId))
|
||||||
|
return productId;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetMainImages()
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
/// 属性转换为新模型结构:多个同名key的value合并到 Values 列表
|
||||||
|
/// </summary>
|
||||||
|
private List<ProductAttribute> GetProperties()
|
||||||
|
{
|
||||||
|
var result = new List<ProductAttribute>();
|
||||||
|
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)) continue;
|
||||||
|
|
||||||
|
var existing = result.FirstOrDefault(p => p.Name == key);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
result.Add(new ProductAttribute
|
||||||
|
{
|
||||||
|
Name = key,
|
||||||
|
Values = new List<string> { value }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (!existing.Values.Contains(value))
|
||||||
|
{
|
||||||
|
existing.Values.Add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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和规格解析
|
||||||
|
|
||||||
|
private Tuple<List<ProductSku>, List<ProductSpec>> GetSkusAndSpecs()
|
||||||
|
{
|
||||||
|
var skuList = new List<ProductSku>();
|
||||||
|
var specList = new List<ProductSpec>();
|
||||||
|
|
||||||
|
var priceFallbackDict = BuildPriceFallbackDict();
|
||||||
|
var attrFallbackDict = BuildAttrFallbackDict();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildSpecList(specList, skuList);
|
||||||
|
|
||||||
|
return new Tuple<List<ProductSku>, List<ProductSpec>>(skuList, specList);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
var estimatePrice = skuObj["estimate_price"]?.Value<decimal>() ?? 0;
|
||||||
|
var price = skuObj["price"]?.Value<decimal>() ?? 0;
|
||||||
|
dict[skuId] = estimatePrice > 0 ? estimatePrice : price;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// skuId -> 结构化的规格列表
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<string, List<ProductSkuSpec>> BuildAttrFallbackDict()
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, List<ProductSkuSpec>>();
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
foreach (var skuIdToken in skuIds)
|
||||||
|
{
|
||||||
|
var id = skuIdToken?.Value<string>();
|
||||||
|
if (string.IsNullOrEmpty(id)) continue;
|
||||||
|
|
||||||
|
if (!dict.ContainsKey(id))
|
||||||
|
dict[id] = new List<ProductSkuSpec>();
|
||||||
|
|
||||||
|
dict[id].Add(new ProductSkuSpec { Name = key, Value = value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProductSku ParseProductSku(
|
||||||
|
JObject skuObj,
|
||||||
|
Dictionary<string, decimal> priceFallbackDict,
|
||||||
|
Dictionary<string, List<ProductSkuSpec>> 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 skuSpecs = ParseSkuSpecs(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 ProductSku
|
||||||
|
{
|
||||||
|
SkuId = !string.IsNullOrEmpty(outSkuId) ? outSkuId : skuId,
|
||||||
|
Price = salePrice / 100,
|
||||||
|
Stock = stockNum,
|
||||||
|
ImageUrl = thumbImg,
|
||||||
|
Specs = skuSpecs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProductSku ParseSkuInfoOnly(
|
||||||
|
JObject skuObj,
|
||||||
|
Dictionary<string, List<ProductSkuSpec>> attrFallbackDict)
|
||||||
|
{
|
||||||
|
var skuId = skuObj["sku_id"]?.Value<string>() ?? string.Empty;
|
||||||
|
if (string.IsNullOrEmpty(skuId)) return null;
|
||||||
|
|
||||||
|
var skuSpecs = attrFallbackDict.TryGetValue(skuId, out var specs)
|
||||||
|
? specs
|
||||||
|
: new List<ProductSkuSpec>();
|
||||||
|
|
||||||
|
var estimatePrice = skuObj["estimate_price"]?.Value<decimal>() ?? 0;
|
||||||
|
var price = skuObj["price"]?.Value<decimal>() ?? 0;
|
||||||
|
var actualPrice = estimatePrice > 0 ? estimatePrice : price;
|
||||||
|
|
||||||
|
return new ProductSku
|
||||||
|
{
|
||||||
|
SkuId = skuId,
|
||||||
|
Price = actualPrice / 100,
|
||||||
|
Stock = 0,
|
||||||
|
ImageUrl = string.Empty,
|
||||||
|
Specs = skuSpecs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析SKU的结构化规格列表
|
||||||
|
/// </summary>
|
||||||
|
private List<ProductSkuSpec> ParseSkuSpecs(
|
||||||
|
JObject skuInfo,
|
||||||
|
string skuId,
|
||||||
|
Dictionary<string, List<ProductSkuSpec>> attrFallbackDict)
|
||||||
|
{
|
||||||
|
var specs = new List<ProductSkuSpec>();
|
||||||
|
var saleParams = skuInfo["sale_param"] as JArray;
|
||||||
|
|
||||||
|
if (saleParams != null)
|
||||||
|
{
|
||||||
|
foreach (var param in saleParams)
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
specs.Add(new ProductSkuSpec { Name = attrName, Value = attrValue });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
specs.Add(new ProductSkuSpec { Name = attrName, Value = attrValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (specs.Count == 0 && attrFallbackDict.TryGetValue(skuId, out var fallbackSpecs))
|
||||||
|
{
|
||||||
|
specs.AddRange(fallbackSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
/// 构建聚合后的规格定义列表
|
||||||
|
/// </summary>
|
||||||
|
private void BuildSpecList(List<ProductSpec> specList, List<ProductSku> 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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退:从SKU的结构化规格解析
|
||||||
|
if (specDict.Count == 0 && skus.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var sku in skus)
|
||||||
|
{
|
||||||
|
foreach (var s in sku.Specs)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(s.Name)) continue;
|
||||||
|
if (!specDict.ContainsKey(s.Name))
|
||||||
|
specDict[s.Name] = new HashSet<string>();
|
||||||
|
specDict[s.Name].Add(s.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var spec in specDict)
|
||||||
|
{
|
||||||
|
specList.Add(new ProductSpec
|
||||||
|
{
|
||||||
|
Name = spec.Key,
|
||||||
|
Values = spec.Value.ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private int GetTotalSold()
|
||||||
|
{
|
||||||
|
var stockInfo = _infoNode["total_sold_num"] as JToken;
|
||||||
|
if (stockInfo == null) return 0;
|
||||||
|
return stockInfo.Value<int>();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 辅助方法
|
||||||
|
|
||||||
|
private string GetStringValue(JObject obj, string path)
|
||||||
|
=> 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -238,7 +238,7 @@ namespace Store.Modules.CloudProduct.Services
|
|||||||
var pending = await _repository.Select.As("g")
|
var pending = await _repository.Select.As("g")
|
||||||
.Where(x => x.IsSegmented == 0 && x.Status == 0)
|
.Where(x => x.IsSegmented == 0 && x.Status == 0)
|
||||||
.Where("NOT EXISTS (SELECT 1 FROM goods_violation v WHERE v.goods_id = g.goods_id)")
|
.Where("NOT EXISTS (SELECT 1 FROM goods_violation v WHERE v.goods_id = g.goods_id)")
|
||||||
.OrderBy(x => x.GoodsId)
|
//.OrderBy(x => x.GoodsId)
|
||||||
.Limit(batchSize)
|
.Limit(batchSize)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"CloudProductConnection": "Server=192.168.0.200;Port=3306;Database=goods_center;Uid=root;Pwd=123456;Allow User Variables=true",
|
"CloudProductConnection": "Server=192.168.0.200;Port=3306;Database=goods_center;Uid=root;Pwd=123456;Allow User Variables=true",
|
||||||
|
|
||||||
|
//"CloudProductConnection": "Server=103.90.155.78;Port=1567;Database=goods_center;Uid=goods_center;Pwd=GnSafXLMcaCTG6N7;Allow User Variables=true",
|
||||||
"Search": {
|
"Search": {
|
||||||
"Url": "http://192.168.0.200:9200",
|
"Url": "http://103.90.155.78:9200",
|
||||||
"IndexName": "goods_search",
|
"IndexName": "goods_search",
|
||||||
"FavoritesIndexName": "user_goods_favorites_search"
|
"FavoritesIndexName": "user_goods_favorites_search"
|
||||||
},
|
},
|
||||||
@@ -24,6 +26,6 @@
|
|||||||
"Endpoint": "https://llm-lq2jys2ky6yxo7rh.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions",
|
"Endpoint": "https://llm-lq2jys2ky6yxo7rh.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||||
"Model": "qwen3.7-flash",
|
"Model": "qwen3.7-flash",
|
||||||
"ApiKey": "sk-fa7fbaa3bb4047928f30bf05ff52c03a",
|
"ApiKey": "sk-fa7fbaa3bb4047928f30bf05ff52c03a",
|
||||||
"MaxConcurrentAnalysis": 5,
|
"MaxConcurrentAnalysis": 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Store.Modules.CloudProduct.Enums
|
namespace Store.Framework.Models
|
||||||
{
|
{
|
||||||
public enum PlatformType
|
public enum PlatformType
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Store.Framework.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通用商品Model
|
||||||
|
/// </summary>
|
||||||
|
public class Product
|
||||||
|
{
|
||||||
|
public Product()
|
||||||
|
{
|
||||||
|
CategoryIds = new List<int>();
|
||||||
|
Attributes = new List<ProductAttribute>();
|
||||||
|
MainImages = new List<string>();
|
||||||
|
DetailImages = new List<string>();
|
||||||
|
Specs = new List<ProductSpec>();
|
||||||
|
Skus = new List<ProductSku>();
|
||||||
|
Extensions = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 来源
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源平台
|
||||||
|
/// </summary>
|
||||||
|
public PlatformType Platform { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源商品链接
|
||||||
|
/// </summary>
|
||||||
|
public string SourceUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源店铺编号
|
||||||
|
/// </summary>
|
||||||
|
public string ShopId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源店铺名称
|
||||||
|
/// </summary>
|
||||||
|
public string ShopName { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 基本信息
|
||||||
|
/// <summary>
|
||||||
|
/// 商家编号
|
||||||
|
/// </summary>
|
||||||
|
public string ProductId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 标题
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 品牌
|
||||||
|
/// </summary>
|
||||||
|
public string Brand { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 类目
|
||||||
|
/// </summary>
|
||||||
|
public int CategoryId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 类目编号路径,从一级类目到叶子类目,叶子类目为最后一项
|
||||||
|
/// </summary>
|
||||||
|
public List<int> CategoryIds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品属性(非销售维度),如材质、产地
|
||||||
|
/// </summary>
|
||||||
|
public List<ProductAttribute> Attributes { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 媒体
|
||||||
|
/// <summary>
|
||||||
|
/// 缩略图
|
||||||
|
/// </summary>
|
||||||
|
public string ThumbUrl { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 主图
|
||||||
|
/// </summary>
|
||||||
|
public List<string> MainImages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 详情图
|
||||||
|
/// </summary>
|
||||||
|
public List<string> DetailImages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 视频地址,可为空
|
||||||
|
/// </summary>
|
||||||
|
public string Video { get; set; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 销售
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规格
|
||||||
|
/// </summary>
|
||||||
|
public List<ProductSpec> Specs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU
|
||||||
|
/// </summary>
|
||||||
|
public List<ProductSku> Skus { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 扩展数据
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> Extensions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 划线价/市场价(商品级)
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MarketPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 销量
|
||||||
|
/// </summary>
|
||||||
|
public long? SalesCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源平台展示的销量文案,如“已拼10万+件”
|
||||||
|
/// </summary>
|
||||||
|
public string SalesText { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源平台上架时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ListingTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU最低售价
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public decimal MinPrice
|
||||||
|
{
|
||||||
|
get { return Skus.Any() ? Skus.Min(a => a.Price) : 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU最高售价
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public decimal MaxPrice
|
||||||
|
{
|
||||||
|
get { return Skus.Any() ? Skus.Max(a => a.Price) : 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所有SKU库存总和
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public int TotalStock
|
||||||
|
{
|
||||||
|
get { return Skus.Sum(a => a.Stock); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验商品数据,返回错误信息,null 表示通过。
|
||||||
|
/// </summary>
|
||||||
|
public string Validate()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ProductId)) return "缺少商品编号";
|
||||||
|
if (MainImages.Count == 0) return "缺少主图";
|
||||||
|
if (Skus.Count == 0) return "缺少SKU";
|
||||||
|
|
||||||
|
if (Specs.Any(s => string.IsNullOrEmpty(s.Name) || s.Values.Count == 0))
|
||||||
|
return "规格名称为空或规格值为空";
|
||||||
|
if (Specs.GroupBy(s => s.Name).Any(g => g.Count() > 1))
|
||||||
|
return "规格名称重复";
|
||||||
|
if (Specs.Any(s => s.Values.Distinct().Count() != s.Values.Count))
|
||||||
|
return "同一规格下规格值重复";
|
||||||
|
|
||||||
|
foreach (var sku in Skus)
|
||||||
|
{
|
||||||
|
// 每个 SKU 在每个规格维度上恰好选一个值
|
||||||
|
if (sku.Specs.Count != Specs.Count)
|
||||||
|
return "SKU规格数量与商品规格不一致:" + sku.SkuId;
|
||||||
|
foreach (var s in sku.Specs)
|
||||||
|
{
|
||||||
|
var spec = Specs.FirstOrDefault(a => a.Name == s.Name);
|
||||||
|
if (spec == null)
|
||||||
|
return "SKU使用了不存在的规格:" + s.Name;
|
||||||
|
if (!spec.Values.Contains(s.Value))
|
||||||
|
return "SKU使用了不存在的规格值:" + s.Name + ":" + s.Value;
|
||||||
|
}
|
||||||
|
if (sku.Specs.GroupBy(a => a.Name).Any(g => g.Count() > 1))
|
||||||
|
return "SKU同一规格重复选择:" + sku.SkuId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品规格定义,如 颜色:红色、蓝色
|
||||||
|
/// </summary>
|
||||||
|
public class ProductSpec
|
||||||
|
{
|
||||||
|
public ProductSpec()
|
||||||
|
{
|
||||||
|
Values = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规格名称,如颜色、尺码,商品内唯一
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规格可选值,如红色、XL,规格内唯一
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品属性(非销售维度)
|
||||||
|
/// </summary>
|
||||||
|
public class ProductAttribute
|
||||||
|
{
|
||||||
|
public ProductAttribute()
|
||||||
|
{
|
||||||
|
Values = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 属性名称,如材质
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 属性值列表
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品销售单元
|
||||||
|
/// </summary>
|
||||||
|
public class ProductSku
|
||||||
|
{
|
||||||
|
public ProductSku()
|
||||||
|
{
|
||||||
|
Specs = new List<ProductSkuSpec>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU编号
|
||||||
|
/// </summary>
|
||||||
|
public string SkuId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前售价(元)
|
||||||
|
/// </summary>
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可售库存
|
||||||
|
/// </summary>
|
||||||
|
public int Stock { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU图片
|
||||||
|
/// </summary>
|
||||||
|
public string ImageUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU选中的规格值,Name/Value 必须存在于 Product.Specs
|
||||||
|
/// </summary>
|
||||||
|
public List<ProductSkuSpec> Specs { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SKU选中的一个规格值
|
||||||
|
/// </summary>
|
||||||
|
public class ProductSkuSpec
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 规格名称,对应 ProductSpec.Name
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规格值,对应 ProductSpec.Values 中的一项
|
||||||
|
/// </summary>
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
Reference in new issue
Block a user