diff --git a/Modules/Store.Users.Contracts/Dtos/UserInfo.cs b/Modules/Store.Users.Contracts/Dtos/UserInfo.cs
index f11fd63..01d3693 100644
--- a/Modules/Store.Users.Contracts/Dtos/UserInfo.cs
+++ b/Modules/Store.Users.Contracts/Dtos/UserInfo.cs
@@ -7,132 +7,133 @@ using System.Text;
namespace Store.Users.Contracts.Dtos
{
public class UserInfo
- {///
- /// ID
- ///
- public int Id { get; set; }
+ {
+ ///
+ /// ID
+ ///
+ public int Id { get; set; }
///
/// 用户昵称
///
- public string NickName { get; set; }
+ public string NickName { get; set; }
///
/// 用户名称(登录账户)
///
- public string UserName { get; set; }
+ public string UserName { get; set; }
///
/// 登录密码
///
- [System.Text.Json.Serialization.JsonIgnore]
- public string Password { get; set; }
+ [System.Text.Json.Serialization.JsonIgnore]
+ public string Password { get; set; }
///
/// card_id
///
- public string CardId { get; set; }
+ public string CardId { get; set; }
///
/// 用户分组,参见 。
///
- public UserGroup GroupId { get; set; }
+ public UserGroup GroupId { get; set; }
///
/// 父账户用户名
///
- public string ParentUserName { get; set; }
+ public string ParentUserName { get; set; }
///
/// oem编号,仅oem用户有值
///
- public string OemId { get; set; }
+ public string OemId { get; set; }
///
/// 手机号
///
- public string Mobile { get; set; }
+ public string Mobile { get; set; }
///
/// 添加时间
///
- public DateTime? AddTime { get; set; }
+ public DateTime? AddTime { get; set; }
///
/// 修改时间
///
- public DateTime? MdyTime { get; set; }
+ public DateTime? MdyTime { get; set; }
///
/// 公司名称
///
- public string CompanyName { get; set; }
+ public string CompanyName { get; set; }
///
/// 备注
///
- public string Notes { get; set; }
+ public string Notes { get; set; }
///
/// 超管等级,参见 。
///
- public UserAdminLevel? SaLevel { get; set; }
+ public UserAdminLevel? SaLevel { get; set; }
///
/// 账户权限
///
- public string Authority { get; set; }
+ public string Authority { get; set; }
///
/// 子账号被禁止展示的功能页面
///
- public string ForbiddenPage { get; set; }
+ public string ForbiddenPage { get; set; }
///
/// 账户积分,1积分=1元
///
- public decimal Points { get; set; }
+ public decimal Points { get; set; }
///
/// 积分历史累计充值金额
///
- public decimal? RechargePointsTotal { get; set; }
+ public decimal? RechargePointsTotal { get; set; }
///
/// 店铺每续费一个月需要的积分,单位元
///
- public int ShopPointsPerMonth { get; set; }
+ public int ShopPointsPerMonth { get; set; }
///
/// 可用余额
///
- public decimal? Balance { get; set; }
+ public decimal? Balance { get; set; }
///
/// 累计充值金额
///
- public decimal? RechargeAmountTotal { get; set; }
+ public decimal? RechargeAmountTotal { get; set; }
///
/// 每单中转仓快递费用,单位元,两位小数
///
- public decimal? PostagePerOrder { get; set; }
+ public decimal? PostagePerOrder { get; set; }
///
/// 每单中转仓退货快递费用,单位元,两位小数
///
- public decimal? RefundPostagePerOrder { get; set; }
+ public decimal? RefundPostagePerOrder { get; set; }
///
/// 礼物每单额外收取费用,单位元,两位小数
///
- public decimal? GiftOemAmountPerOrder { get; set; }
+ public decimal? GiftOemAmountPerOrder { get; set; }
///
/// 同步物流费用,单位元,两位小数
///
- public decimal? SyncLogisticsPerOrder { get; set; }
+ public decimal? SyncLogisticsPerOrder { get; set; }
///
/// 是否主账户
diff --git a/Modules/Store.Users.Contracts/Dtos/UserManagementRequests.cs b/Modules/Store.Users.Contracts/Dtos/UserManagementRequests.cs
index 3668db4..7b8276c 100644
--- a/Modules/Store.Users.Contracts/Dtos/UserManagementRequests.cs
+++ b/Modules/Store.Users.Contracts/Dtos/UserManagementRequests.cs
@@ -77,7 +77,7 @@ public class UserSearchRequest
{
[Range(1, 1000000)]
public int Page { get; set; } = 1;
- [Range(1, 200)]
+ [Range(1, 2000)]
public int PageSize { get; set; } = 50;
[StringLength(100)]
public string? Keyword { get; set; }
diff --git a/Plugins/CloudProduct/Controllers/GoodsController.cs b/Plugins/CloudProduct/Controllers/GoodsController.cs
index d9ce091..f03feac 100644
--- a/Plugins/CloudProduct/Controllers/GoodsController.cs
+++ b/Plugins/CloudProduct/Controllers/GoodsController.cs
@@ -10,6 +10,7 @@ using Store.Modules.CloudProduct.Entities;
using Store.Modules.CloudProduct.ImportConverters;
using Store.Modules.CloudProduct.Services;
using System.Text.Json.Nodes;
+using Microsoft.Extensions.Logging;
namespace Store.Modules.CloudProduct.Controllers
{
@@ -23,10 +24,12 @@ namespace Store.Modules.CloudProduct.Controllers
private readonly IRawDataStorage _rawDataStorage;
private readonly GoodsIndexJob _goodsIndexJob;
private readonly GoodsViolationService _violations;
+ private readonly ILogger _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 logger)
{
_violations = violations;
+ _logger = logger;
_goodsService = goodsService;
_searchService = searchService;
_rawDataStorage = rawDataStorage;
@@ -96,7 +99,7 @@ namespace Store.Modules.CloudProduct.Controllers
int batchCount;
do
{
- batchCount = await _goodsService.IndexPendingGoodsAsync(1000);
+ batchCount = await _goodsService.IndexPendingGoodsAsync(20000);
count += batchCount;
} while (batchCount > 0);
@@ -260,6 +263,35 @@ namespace Store.Modules.CloudProduct.Controllers
});
}
+ [HttpPost]
+ [Route("/api/wx/data/import")]
+ [EndpointSummary("导入商品数据")]
+ public async Task 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("导入微信数据失败");
+ }
+ }
///
/// 导入接口:接收客户端批量上传的 JSON 文件,解析后入库并写入分词索引。
///
@@ -398,7 +430,7 @@ namespace Store.Modules.CloudProduct.Controllers
var node = JsonNode.Parse(raw);
return node ?? throw new AppException("商品详情解析失败", ResultCode.BusinessError);
}
- catch(Exception ex)
+ catch (Exception ex)
{
throw new AppException(ex.Message);
}
diff --git a/Plugins/CloudProduct/ImportConverters/GoodsConverter.cs b/Plugins/CloudProduct/ImportConverters/GoodsConverter.cs
index c74c0e3..b6604dc 100644
--- a/Plugins/CloudProduct/ImportConverters/GoodsConverter.cs
+++ b/Plugins/CloudProduct/ImportConverters/GoodsConverter.cs
@@ -1,5 +1,5 @@
+using Store.Framework.Models;
using Store.Modules.CloudProduct.Entities;
-using Store.Modules.CloudProduct.Enums;
using System.Text.Json.Nodes;
namespace Store.Modules.CloudProduct.ImportConverters
diff --git a/Plugins/CloudProduct/ImportConverters/GoodsConverterFactory.cs b/Plugins/CloudProduct/ImportConverters/GoodsConverterFactory.cs
index e4bc497..46b7562 100644
--- a/Plugins/CloudProduct/ImportConverters/GoodsConverterFactory.cs
+++ b/Plugins/CloudProduct/ImportConverters/GoodsConverterFactory.cs
@@ -1,3 +1,4 @@
+using Store.CloudProduct.ImportConverters;
using Store.Modules.CloudProduct.Entities;
using System.Collections.Generic;
using System.IO;
@@ -12,6 +13,7 @@ namespace Store.Modules.CloudProduct.ImportConverters
private static readonly List _converters = new List
{
new PddGoodsConverter(),
+ new WXGoodsConverter(),
};
///
diff --git a/Plugins/CloudProduct/ImportConverters/PddGoodsConverter.cs b/Plugins/CloudProduct/ImportConverters/PddGoodsConverter.cs
index 52c5972..400da2f 100644
--- a/Plugins/CloudProduct/ImportConverters/PddGoodsConverter.cs
+++ b/Plugins/CloudProduct/ImportConverters/PddGoodsConverter.cs
@@ -1,5 +1,5 @@
+using Store.Framework.Models;
using Store.Modules.CloudProduct.Entities;
-using Store.Modules.CloudProduct.Enums;
using System.Text.Json.Nodes;
namespace Store.Modules.CloudProduct.ImportConverters
diff --git a/Plugins/CloudProduct/ImportConverters/WXGoodsConverter.cs b/Plugins/CloudProduct/ImportConverters/WXGoodsConverter.cs
new file mode 100644
index 0000000..875d82e
--- /dev/null
+++ b/Plugins/CloudProduct/ImportConverters/WXGoodsConverter.cs
@@ -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(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() ?? 0;
+ }
+
+ private List GetCategoryPath()
+ {
+ var result = new List();
+ var category = _infoNode["category"] as JArray;
+ if (category == null) return result;
+
+ foreach (var cat in category)
+ {
+ var catId = cat["cat_id"]?.Value();
+ if (catId.HasValue && catId.Value > 0)
+ result.Add(catId.Value);
+ }
+
+ return result;
+ }
+
+ private List GetMainImages()
+ {
+ var result = new List();
+
+ var headImg = _infoNode["head_img"] as JArray;
+ if (headImg != null && headImg.Count > 0)
+ {
+ foreach (var img in headImg)
+ {
+ var url = img?.Value();
+ 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();
+ if (!string.IsNullOrEmpty(url))
+ result.Add(url);
+ }
+ }
+
+ return result.Distinct().ToList();
+ }
+
+ private List GetDetailImages()
+ {
+ var result = new List();
+ 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();
+ if (!string.IsNullOrEmpty(url))
+ result.Add(url);
+ }
+
+ return result;
+ }
+
+ ///
+ /// 属性转换为新模型结构:多个同名key的value合并到 Values 列表
+ ///
+ private List GetProperties()
+ {
+ var result = new List();
+ 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();
+ var value = categorys[1]?["name"]?.Value();
+
+ 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 { value }
+ });
+ }
+ else if (!existing.Values.Contains(value))
+ {
+ existing.Values.Add(value);
+ }
+ }
+
+ return result;
+ }
+
+ private List GetVideoUrls()
+ {
+ var result = new List();
+ var headVideoInfo = _infoNode["head_video_info"] as JArray;
+ if (headVideoInfo == null) return result;
+
+ foreach (var video in headVideoInfo)
+ {
+ var url = video["video_url"]?.Value();
+ if (!string.IsNullOrEmpty(url))
+ result.Add(url);
+ }
+
+ return result;
+ }
+
+ #endregion
+
+ #region SKU和规格解析
+
+ private Tuple, List> GetSkusAndSpecs()
+ {
+ var skuList = new List();
+ var specList = new List();
+
+ 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>(skuList, specList);
+ }
+
+ private Dictionary BuildPriceFallbackDict()
+ {
+ var dict = new Dictionary();
+ 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();
+ if (string.IsNullOrEmpty(skuId)) continue;
+
+ var estimatePrice = skuObj["estimate_price"]?.Value() ?? 0;
+ var price = skuObj["price"]?.Value() ?? 0;
+ dict[skuId] = estimatePrice > 0 ? estimatePrice : price;
+ }
+
+ return dict;
+ }
+
+ ///
+ /// skuId -> 结构化的规格列表
+ ///
+ private Dictionary> BuildAttrFallbackDict()
+ {
+ var dict = new Dictionary>();
+ if (_saleAttrs == null) return dict;
+
+ foreach (var attr in _saleAttrs)
+ {
+ var key = attr["key"]?.Value() ?? string.Empty;
+ var items = attr["items"] as JArray;
+ if (items == null) continue;
+
+ foreach (var item in items)
+ {
+ var value = item["value_v2"]?.Value();
+ if (string.IsNullOrEmpty(value))
+ value = item["value"]?.Value();
+ 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();
+ if (string.IsNullOrEmpty(id)) continue;
+
+ if (!dict.ContainsKey(id))
+ dict[id] = new List();
+
+ dict[id].Add(new ProductSkuSpec { Name = key, Value = value });
+ }
+ }
+ }
+
+ return dict;
+ }
+
+ private ProductSku ParseProductSku(
+ JObject skuObj,
+ Dictionary priceFallbackDict,
+ Dictionary> 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.Empty;
+ var outSkuId = skuKey["out_sku_id"]?.Value() ?? string.Empty;
+ if (string.IsNullOrEmpty(skuId)) return null;
+
+ var skuSpecs = ParseSkuSpecs(skuInfo, skuId, attrFallbackDict);
+
+ var salePrice = skuInfo["sale_price"]?.Value() ?? 0;
+ if (salePrice <= 0 && priceFallbackDict.TryGetValue(skuId, out var fallbackPrice))
+ salePrice = fallbackPrice;
+
+ var stockNum = ParseStockNum(skuInfo);
+ var thumbImg = skuInfo["thumb_img"]?.Value() ?? 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> attrFallbackDict)
+ {
+ var skuId = skuObj["sku_id"]?.Value() ?? string.Empty;
+ if (string.IsNullOrEmpty(skuId)) return null;
+
+ var skuSpecs = attrFallbackDict.TryGetValue(skuId, out var specs)
+ ? specs
+ : new List();
+
+ var estimatePrice = skuObj["estimate_price"]?.Value() ?? 0;
+ var price = skuObj["price"]?.Value() ?? 0;
+ var actualPrice = estimatePrice > 0 ? estimatePrice : price;
+
+ return new ProductSku
+ {
+ SkuId = skuId,
+ Price = actualPrice / 100,
+ Stock = 0,
+ ImageUrl = string.Empty,
+ Specs = skuSpecs
+ };
+ }
+
+ ///
+ /// 解析SKU的结构化规格列表
+ ///
+ private List ParseSkuSpecs(
+ JObject skuInfo,
+ string skuId,
+ Dictionary> attrFallbackDict)
+ {
+ var specs = new List();
+ 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.Empty;
+ var attrValue = paramV2["attr_value_name"]?.Value() ?? 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.Empty;
+ var attrValue = categorys[1]?["name"]?.Value() ?? 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(), out var result) ? result : 0;
+
+ return stockToken.Value();
+ }
+
+ ///
+ /// 构建聚合后的规格定义列表
+ ///
+ private void BuildSpecList(List specList, List skus)
+ {
+ var specDict = new Dictionary>();
+
+ // 优先从 product_sale_attrs
+ if (_saleAttrs != null && _saleAttrs.Count > 0)
+ {
+ foreach (var attr in _saleAttrs)
+ {
+ var key = attr["key"]?.Value() ?? string.Empty;
+ var items = attr["items"] as JArray;
+ if (items == null) continue;
+
+ foreach (var item in items)
+ {
+ var value = item["value_v2"]?.Value();
+ if (string.IsNullOrEmpty(value))
+ value = item["value"]?.Value();
+ if (string.IsNullOrEmpty(value)) continue;
+
+ if (!specDict.ContainsKey(key))
+ specDict[key] = new HashSet();
+ 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();
+ 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();
+ }
+ #endregion
+
+ #region 辅助方法
+
+ private string GetStringValue(JObject obj, string path)
+ => obj?.SelectToken(path)?.Value() ?? 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(), out var result) ? result : 0;
+
+ return token.Value();
+ }
+
+ #endregion
+
+
+
+ }
+}
diff --git a/Plugins/CloudProduct/Services/GoodsService.cs b/Plugins/CloudProduct/Services/GoodsService.cs
index 569c4fe..9659193 100644
--- a/Plugins/CloudProduct/Services/GoodsService.cs
+++ b/Plugins/CloudProduct/Services/GoodsService.cs
@@ -238,7 +238,7 @@ namespace Store.Modules.CloudProduct.Services
var pending = await _repository.Select.As("g")
.Where(x => x.IsSegmented == 0 && x.Status == 0)
.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)
.ToListAsync();
diff --git a/Plugins/CloudProduct/appsettings.json b/Plugins/CloudProduct/appsettings.json
index f9dcc9a..b69d9e3 100644
--- a/Plugins/CloudProduct/appsettings.json
+++ b/Plugins/CloudProduct/appsettings.json
@@ -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=103.90.155.78;Port=1567;Database=goods_center;Uid=goods_center;Pwd=GnSafXLMcaCTG6N7;Allow User Variables=true",
"Search": {
- "Url": "http://192.168.0.200:9200",
+ "Url": "http://103.90.155.78:9200",
"IndexName": "goods_search",
"FavoritesIndexName": "user_goods_favorites_search"
},
@@ -24,6 +26,6 @@
"Endpoint": "https://llm-lq2jys2ky6yxo7rh.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions",
"Model": "qwen3.7-flash",
"ApiKey": "sk-fa7fbaa3bb4047928f30bf05ff52c03a",
- "MaxConcurrentAnalysis": 5,
+ "MaxConcurrentAnalysis": 5
}
}
diff --git a/Plugins/CloudProduct/Enums/PlatformType.cs b/Store.Framework/Models/PlatformType.cs
similarity index 60%
rename from Plugins/CloudProduct/Enums/PlatformType.cs
rename to Store.Framework/Models/PlatformType.cs
index 0a11b30..cc6473a 100644
--- a/Plugins/CloudProduct/Enums/PlatformType.cs
+++ b/Store.Framework/Models/PlatformType.cs
@@ -1,4 +1,4 @@
-namespace Store.Modules.CloudProduct.Enums
+namespace Store.Framework.Models
{
public enum PlatformType
{
diff --git a/Store.Framework/Models/Product.cs b/Store.Framework/Models/Product.cs
new file mode 100644
index 0000000..85073c8
--- /dev/null
+++ b/Store.Framework/Models/Product.cs
@@ -0,0 +1,300 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+namespace Store.Framework.Models;
+
+///
+/// 通用商品Model
+///
+public class Product
+{
+ public Product()
+ {
+ CategoryIds = new List();
+ Attributes = new List();
+ MainImages = new List();
+ DetailImages = new List();
+ Specs = new List();
+ Skus = new List();
+ Extensions = new Dictionary();
+ }
+
+ #region 来源
+
+ ///
+ /// 来源平台
+ ///
+ public PlatformType Platform { get; set; }
+
+ ///
+ /// 来源商品链接
+ ///
+ public string SourceUrl { get; set; }
+
+ ///
+ /// 来源店铺编号
+ ///
+ public string ShopId { get; set; }
+
+ ///
+ /// 来源店铺名称
+ ///
+ public string ShopName { get; set; }
+
+ #endregion
+
+ #region 基本信息
+ ///
+ /// 商家编号
+ ///
+ public string ProductId { get; set; }
+ ///
+ /// 标题
+ ///
+ public string Title { get; set; }
+
+ ///
+ /// 描述
+ ///
+ public string Description { get; set; }
+ ///
+ /// 品牌
+ ///
+ public string Brand { get; set; }
+
+ ///
+ /// 类目
+ ///
+ public int CategoryId { get; set; }
+ ///
+ /// 类目编号路径,从一级类目到叶子类目,叶子类目为最后一项
+ ///
+ public List CategoryIds { get; set; }
+
+ ///
+ /// 商品属性(非销售维度),如材质、产地
+ ///
+ public List Attributes { get; set; }
+
+ #endregion
+
+ #region 媒体
+ ///
+ /// 缩略图
+ ///
+ public string ThumbUrl { get; set; }
+ ///
+ /// 主图
+ ///
+ public List MainImages { get; set; }
+
+ ///
+ /// 详情图
+ ///
+ public List DetailImages { get; set; }
+
+ ///
+ /// 视频地址,可为空
+ ///
+ public string Video { get; set; }
+
+ #endregion
+
+ #region 销售
+
+ ///
+ /// 规格
+ ///
+ public List Specs { get; set; }
+
+ ///
+ /// SKU
+ ///
+ public List Skus { get; set; }
+ ///
+ /// 扩展数据
+ ///
+ public Dictionary Extensions { get; set; }
+
+ ///
+ /// 划线价/市场价(商品级)
+ ///
+ public decimal? MarketPrice { get; set; }
+
+ ///
+ /// 销量
+ ///
+ public long? SalesCount { get; set; }
+
+ ///
+ /// 来源平台展示的销量文案,如“已拼10万+件”
+ ///
+ public string SalesText { get; set; }
+
+ ///
+ /// 来源平台上架时间
+ ///
+ public DateTime? ListingTime { get; set; }
+
+ ///
+ /// SKU最低售价
+ ///
+ [JsonIgnore]
+ public decimal MinPrice
+ {
+ get { return Skus.Any() ? Skus.Min(a => a.Price) : 0; }
+ }
+
+ ///
+ /// SKU最高售价
+ ///
+ [JsonIgnore]
+ public decimal MaxPrice
+ {
+ get { return Skus.Any() ? Skus.Max(a => a.Price) : 0; }
+ }
+
+ ///
+ /// 所有SKU库存总和
+ ///
+ [JsonIgnore]
+ public int TotalStock
+ {
+ get { return Skus.Sum(a => a.Stock); }
+ }
+
+ #endregion
+
+
+ ///
+ /// 校验商品数据,返回错误信息,null 表示通过。
+ ///
+ 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;
+ }
+}
+
+///
+/// 商品规格定义,如 颜色:红色、蓝色
+///
+public class ProductSpec
+{
+ public ProductSpec()
+ {
+ Values = new List();
+ }
+
+ ///
+ /// 规格名称,如颜色、尺码,商品内唯一
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 规格可选值,如红色、XL,规格内唯一
+ ///
+ public List Values { get; set; }
+}
+
+///
+/// 商品属性(非销售维度)
+///
+public class ProductAttribute
+{
+ public ProductAttribute()
+ {
+ Values = new List();
+ }
+
+ ///
+ /// 属性名称,如材质
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 属性值列表
+ ///
+ public List Values { get; set; }
+}
+
+///
+/// 商品销售单元
+///
+public class ProductSku
+{
+ public ProductSku()
+ {
+ Specs = new List();
+ }
+
+ ///
+ /// SKU编号
+ ///
+ public string SkuId { get; set; }
+
+ ///
+ /// 当前售价(元)
+ ///
+ public decimal Price { get; set; }
+
+ ///
+ /// 可售库存
+ ///
+ public int Stock { get; set; }
+
+ ///
+ /// SKU图片
+ ///
+ public string ImageUrl { get; set; }
+
+ ///
+ /// SKU选中的规格值,Name/Value 必须存在于 Product.Specs
+ ///
+ public List Specs { get; set; }
+}
+
+///
+/// SKU选中的一个规格值
+///
+public class ProductSkuSpec
+{
+ ///
+ /// 规格名称,对应 ProductSpec.Name
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 规格值,对应 ProductSpec.Values 中的一项
+ ///
+ public string Value { get; set; }
+}
\ No newline at end of file