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