diff --git a/Models/CloudProduct/Controllers/ProductController.cs b/Models/CloudProduct/Controllers/GoodsController.cs similarity index 77% rename from Models/CloudProduct/Controllers/ProductController.cs rename to Models/CloudProduct/Controllers/GoodsController.cs index 2182bc1..1e88bb6 100644 --- a/Models/CloudProduct/Controllers/ProductController.cs +++ b/Models/CloudProduct/Controllers/GoodsController.cs @@ -1,4 +1,4 @@ -using Asp.Versioning; +using Asp.Versioning; using MAX.Framework.Commons; using MAX.Framework.Dtos; using Microsoft.AspNetCore.Http; @@ -8,39 +8,38 @@ using Store.Modules.CloudProduct.Dtos; using Store.Modules.CloudProduct.Entities; using Store.Modules.CloudProduct.ImportConverters; using Store.Modules.CloudProduct.Services; -using System; -using System.Collections.Generic; -using System.Text; using System.Text.Json.Nodes; namespace Store.Modules.CloudProduct.Controllers { [ApiVersionNeutral] - [Route("/CloudProduct/[action]")] - public class ProductController : AppControllerBase + [Route("/CloudGoods/[action]")] + public class GoodsController : AppControllerBase { - private readonly IProductService _productService; - private readonly IOpenSearchServer _searchServer; - private readonly RawDataStorage _rawDataStorage; + private readonly IGoodsService _goodsService; + private readonly IOpenSearchService _searchService; + private readonly IRawDataStorage _rawDataStorage; - public ProductController(IProductService productService, IOpenSearchServer searchServer, RawDataStorage rawDataStorage) + public GoodsController(IGoodsService goodsService, IOpenSearchService searchService, IRawDataStorage rawDataStorage) { - _productService = productService; - _searchServer = searchServer; + _goodsService = goodsService; + _searchService = searchService; _rawDataStorage = rawDataStorage; } + [HttpGet] public async Task Time() { return DateTime.Now.ToLongDateString(); } + /// /// 搜索 /// - /// + /// /// [HttpPost] - public async Task> SearchGoods(ProductSearchRequest request) + public async Task> SearchGoods(GoodsSearchRequest request) { // 1. 参数基础校验 if (request == null) @@ -125,8 +124,8 @@ namespace Store.Modules.CloudProduct.Controllers throw new AppException("请至少提供一个搜索条件", ResultCode.ValidationError); } - var result = await _productService.SearchGoods(request); - return new PageData + var result = await _goodsService.SearchGoodsAsync(request); + return new PageData { List = result.List, Total = result.Total, @@ -135,29 +134,32 @@ namespace Store.Modules.CloudProduct.Controllers }; } - + /// + /// 分词搜索,直接返回索引中的商品摘要(路由保持 SearchGoodsToIds 不变) + /// [HttpGet] - public async Task> SearchGoodsToIds(string keyword, int pageindex = 1, int pagesize = 20) + [ActionName("SearchGoodsToIds")] + public async Task> SearchGoodsSummary(string keyword, int pageindex = 1, int pagesize = 20) { - if (string.IsNullOrWhiteSpace(keyword)) { throw new AppException("填写关键字", ResultCode.ValidationError); } - var ret = await _searchServer.SearchByTitleAsync(new ProductSearchRequest + + var result = await _searchService.SearchByTitleAsync(new GoodsSearchRequest { Title = keyword, PageIndex = pageindex, PageSize = pagesize, }); - return new PageData + + return new PageData { PageIndex = pageindex, PageSize = pagesize, - List = ret.list, - Total = ret.Total, + List = result.Documents, + Total = result.Total, }; - } /// @@ -174,15 +176,15 @@ namespace Store.Modules.CloudProduct.Controllers } var content = await System.IO.File.ReadAllTextAsync(filePath); - var goods = ConverterToGoodsFactory.ConvertContent(content); + var goods = GoodsConverterFactory.ConvertContent(content); if (goods == null) { throw new AppException("未找到匹配的转换器,无法解析测试数据"); } - return await _productService.ImportGoodsAsync(new[] + return await _goodsService.ImportGoodsAsync(new[] { - new GoodsImportItem { ProductEntity = goods, RawData = content } + new GoodsImportItem { Goods = goods, RawData = content } }); } @@ -212,14 +214,14 @@ namespace Store.Modules.CloudProduct.Controllers using var reader = new StreamReader(file.OpenReadStream()); var content = await reader.ReadToEndAsync(); - var goods = ConverterToGoodsFactory.ConvertContent(content); + var goods = GoodsConverterFactory.ConvertContent(content); if (goods == null) { parseErrors.Add($"{file.FileName}: 未找到匹配的转换器"); } else { - items.Add(new GoodsImportItem { ProductEntity = goods, RawData = content }); + items.Add(new GoodsImportItem { Goods = goods, RawData = content }); } } catch (Exception ex) @@ -228,8 +230,8 @@ namespace Store.Modules.CloudProduct.Controllers } } - var result = await _productService.ImportGoodsAsync(items); - result.Failed += parseErrors.Count; + var result = await _goodsService.ImportGoodsAsync(items); + result.FailedCount += parseErrors.Count; result.Errors.InsertRange(0, parseErrors); return result; @@ -241,7 +243,7 @@ namespace Store.Modules.CloudProduct.Controllers [HttpGet] public async Task GetOpenSearchStatus() { - return await _searchServer.GetStatusAsync(); + return await _searchService.GetStatusAsync(); } /// @@ -271,11 +273,11 @@ namespace Store.Modules.CloudProduct.Controllers [HttpPost] public async Task RebuildIndex([FromBody] long[]? ids) { - return await _productService.RebuildSearchIndexAsync(ids); + return await _goodsService.RebuildSearchIndexAsync(ids); } /// - /// 删除商品(按数据库主键 ID)。是否软删除由配置 Delete:SoftDelete 决定, + /// 删除商品(按商品编号)。是否软删除由配置 Delete:SoftDelete 决定, /// 软删除将 Status 置 1,硬删除物理删除记录;两种方式都会删除 OpenSearch 中对应文档。 /// [HttpPost] @@ -286,19 +288,28 @@ namespace Store.Modules.CloudProduct.Controllers throw new AppException("请提供要删除的商品ID", ResultCode.ValidationError); } - return await _productService.DeleteGoodsAsync(ids); - } - [HttpGet] - public async Task ClearSearch() - { - return await _searchServer.ClearIndexAsync(); - } - [HttpGet] - public async Task GetSearchCount() - { - return await _searchServer.CountDocumentsAsync(); + return await _goodsService.DeleteGoodsAsync(ids); } + /// + /// 清空并重建搜索索引(路由保持 ClearSearch 不变)。 + /// + [HttpGet] + [ActionName("ClearSearch")] + public async Task ClearSearchIndex() + { + return await _searchService.ClearIndexAsync(); + } + + /// + /// 统计索引中的文档数(路由保持 GetSearchCount 不变)。 + /// + [HttpGet] + [ActionName("GetSearchCount")] + public async Task GetSearchDocumentCount() + { + return await _searchService.CountDocumentsAsync(); + } private async Task GetRawDataOrThrowAsync(long goodsId) { diff --git a/Models/CloudProduct/Dtos/GoodsImportItem.cs b/Models/CloudProduct/Dtos/GoodsImportItem.cs index c0837ca..be83e6f 100644 --- a/Models/CloudProduct/Dtos/GoodsImportItem.cs +++ b/Models/CloudProduct/Dtos/GoodsImportItem.cs @@ -7,8 +7,14 @@ namespace Store.Modules.CloudProduct.Dtos /// public class GoodsImportItem { - public ProductEntity ProductEntity { get; set; } = null!; + /// + /// 解析后的商品模型 + /// + public GoodsEntity Goods { get; set; } = null!; + /// + /// 原始数据(JSON 原文,压缩后落盘) + /// public string RawData { get; set; } = string.Empty; } -} \ No newline at end of file +} diff --git a/Models/CloudProduct/Dtos/GoodsImportResult.cs b/Models/CloudProduct/Dtos/GoodsImportResult.cs index 1003ac6..7541330 100644 --- a/Models/CloudProduct/Dtos/GoodsImportResult.cs +++ b/Models/CloudProduct/Dtos/GoodsImportResult.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Text; namespace Store.Modules.CloudProduct.Dtos { @@ -12,17 +10,17 @@ namespace Store.Modules.CloudProduct.Dtos /// /// 成功条数 /// - public int Success { get; set; } + public int SuccessCount { get; set; } /// /// 失败条数 /// - public int Failed { get; set; } + public int FailedCount { get; set; } /// /// 总条数 /// - public int Total => Success + Failed; + public int Total => SuccessCount + FailedCount; /// /// 错误明细 diff --git a/Models/CloudProduct/Dtos/ProductSearchInfo.cs b/Models/CloudProduct/Dtos/GoodsSearchDocument.cs similarity index 76% rename from Models/CloudProduct/Dtos/ProductSearchInfo.cs rename to Models/CloudProduct/Dtos/GoodsSearchDocument.cs index 08cf448..2617b4d 100644 --- a/Models/CloudProduct/Dtos/ProductSearchInfo.cs +++ b/Models/CloudProduct/Dtos/GoodsSearchDocument.cs @@ -1,14 +1,11 @@ -using OpenSearch.Client; -using System; -using System.Collections.Generic; -using System.Text; +using OpenSearch.Client; namespace Store.Modules.CloudProduct.Dtos { /// - /// 商品搜索模型:Id 为主键,Title 用于分词搜索 + /// 商品搜索文档:OpenSearch 索引中的商品模型,Id 为主键,Title 用于分词搜索 /// - public class ProductSearchInfo + public class GoodsSearchDocument { [Number(NumberType.Byte, Name = "platform")] public byte Platform { get; set; } @@ -18,6 +15,7 @@ namespace Store.Modules.CloudProduct.Dtos [Text(Name = "title", Analyzer = "ik_max_word", SearchAnalyzer = "ik_smart")] public string Title { get; set; } = string.Empty; + [Text(Name = "thumbUrl")] public string? ThumbUrl { get; set; } = string.Empty; @@ -47,14 +45,4 @@ namespace Store.Modules.CloudProduct.Dtos public override string ToString() => $"[Id={Id}] Title={Title} Cat=({CatId1},{CatId2},{CatId3},{CatId4}) Price={MinPrice} Sold={SoldQuantity}"; } - /// - /// 搜索排序方式 - /// - public enum GoodsSearchSortBy - { - Relevance = 0, // 相关性(默认) - PriceAsc = 1, // 价格从低到高 - PriceDesc = 2, // 价格从高到低 - SoldDesc = 3, // 销量从高到低 - } } diff --git a/Models/CloudProduct/Dtos/ProductSearchRequest.cs b/Models/CloudProduct/Dtos/GoodsSearchRequest.cs similarity index 93% rename from Models/CloudProduct/Dtos/ProductSearchRequest.cs rename to Models/CloudProduct/Dtos/GoodsSearchRequest.cs index bb94f27..d149a56 100644 --- a/Models/CloudProduct/Dtos/ProductSearchRequest.cs +++ b/Models/CloudProduct/Dtos/GoodsSearchRequest.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace Store.Modules.CloudProduct.Dtos { /// /// 商品搜索请求 /// - public class ProductSearchRequest + public class GoodsSearchRequest { /// /// 标题(模糊搜索) @@ -59,7 +55,6 @@ namespace Store.Modules.CloudProduct.Dtos /// public byte? Platform { get; set; } - /// /// 页码(默认1) /// diff --git a/Models/CloudProduct/Entities/ProductEntity.cs b/Models/CloudProduct/Entities/GoodsEntity.cs similarity index 96% rename from Models/CloudProduct/Entities/ProductEntity.cs rename to Models/CloudProduct/Entities/GoodsEntity.cs index 2ea4a4c..674f28e 100644 --- a/Models/CloudProduct/Entities/ProductEntity.cs +++ b/Models/CloudProduct/Entities/GoodsEntity.cs @@ -4,10 +4,10 @@ using System.Text.Json.Serialization; namespace Store.Modules.CloudProduct.Entities { /// - /// 商品表实体 + /// 商品表实体(对应数据库表 goods) /// - [Table(Name = "goods")] // 请根据实际表名调整 - public class ProductEntity + [Table(Name = "goods")] + public class GoodsEntity { /// /// 自增ID @@ -101,7 +101,6 @@ namespace Store.Modules.CloudProduct.Entities /// [Column(Name = "createdTime", IsNullable = false, ServerTime = DateTimeKind.Utc)] [JsonIgnore] - public DateTime CreatedTime { get; set; } /// @@ -109,16 +108,13 @@ namespace Store.Modules.CloudProduct.Entities /// [Column(Name = "updatedTime", IsNullable = false, ServerTime = DateTimeKind.Utc)] [JsonIgnore] - public DateTime UpdatedTime { get; set; } - /// /// 是否已分词:0未 1已 /// [Column(Name = "is_segmented", IsNullable = false)] [JsonIgnore] public byte IsSegmented { get; set; } - } -} \ No newline at end of file +} diff --git a/Models/CloudProduct/Enums/GoodsSearchSortBy.cs b/Models/CloudProduct/Enums/GoodsSearchSortBy.cs new file mode 100644 index 0000000..1dfbb29 --- /dev/null +++ b/Models/CloudProduct/Enums/GoodsSearchSortBy.cs @@ -0,0 +1,20 @@ +namespace Store.Modules.CloudProduct.Enums +{ + /// + /// 搜索排序方式 + /// + public enum GoodsSearchSortBy + { + /// 相关性(默认) + Relevance = 0, + + /// 价格从低到高 + PriceAsc = 1, + + /// 价格从高到低 + PriceDesc = 2, + + /// 销量从高到低 + SoldDesc = 3, + } +} diff --git a/Models/CloudProduct/ImportConverters/IGoodsConverter.cs b/Models/CloudProduct/ImportConverters/GoodsConverter.cs similarity index 58% rename from Models/CloudProduct/ImportConverters/IGoodsConverter.cs rename to Models/CloudProduct/ImportConverters/GoodsConverter.cs index 762a591..c74c0e3 100644 --- a/Models/CloudProduct/ImportConverters/IGoodsConverter.cs +++ b/Models/CloudProduct/ImportConverters/GoodsConverter.cs @@ -1,12 +1,6 @@ -using Store.Modules.CloudProduct.Entities; +using Store.Modules.CloudProduct.Entities; using Store.Modules.CloudProduct.Enums; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; using System.Text.Json.Nodes; -using System.Threading.Tasks; namespace Store.Modules.CloudProduct.ImportConverters { @@ -16,7 +10,7 @@ namespace Store.Modules.CloudProduct.ImportConverters public interface IGoodsConverter { /// - /// 原始数据 + /// 原始数据(JSON 原文) /// string RawData { get; } @@ -29,11 +23,13 @@ namespace Store.Modules.CloudProduct.ImportConverters /// 支持的平台类型 /// PlatformType Platform { get; } + /// /// 读取内容 /// - /// + /// JSON 原文内容 void ReadContent(string content); + /// /// 判断是否匹配当前转换器 /// @@ -43,37 +39,47 @@ namespace Store.Modules.CloudProduct.ImportConverters /// /// 转换为标准商品模型 /// - ProductEntity Convert(); + GoodsEntity Convert(); } - public abstract class GoodsConverterBase : IGoodsConverter + /// + /// 转换器基类:统一实现读取、校验与转换骨架,子类只需实现匹配判定与字段映射。 + /// + public abstract class GoodsConverter : IGoodsConverter { private bool _isMatch = false; - public string RawData { get; private set; } + + public string RawData { get; private set; } = null!; + public abstract string ConverterName { get; } + public abstract PlatformType Platform { get; } - void IGoodsConverter.ReadContent(string filePath) + void IGoodsConverter.ReadContent(string content) { - ReadContent(filePath); - } - protected virtual void ReadContent(string filePath) - { - RawData = filePath; + ReadContent(content); } + protected virtual void ReadContent(string content) + { + RawData = content; + } public virtual bool IsMatch() { _isMatch = true; - return IsMatchProvider(); + return IsMatchCore(); } - protected abstract bool IsMatchProvider(); + + /// + /// 由子类实现的具体匹配判定 + /// + protected abstract bool IsMatchCore(); /// /// 带校验的转换(基类统一实现) /// - public virtual ProductEntity Convert() + public virtual GoodsEntity Convert() { if (_isMatch == false) throw new Exception("未校验数据"); try @@ -82,18 +88,20 @@ namespace Store.Modules.CloudProduct.ImportConverters } catch { throw new Exception("数据解析失败,不是有效的JSON格式"); } - - var product = ConverterProvider(); - if (product == null) + var goods = ConvertCore(); + if (goods == null) throw new Exception("转换失败"); + // 基础校验 - if (product.GoodsId == 0) + if (goods.GoodsId == 0) throw new Exception("商品ID不能为空"); - return product; + return goods; } - protected abstract ProductEntity ConverterProvider(); - + /// + /// 由子类实现的具体字段映射 + /// + protected abstract GoodsEntity ConvertCore(); } } diff --git a/Models/CloudProduct/ImportConverters/ConverterToGoodsFactory.cs b/Models/CloudProduct/ImportConverters/GoodsConverterFactory.cs similarity index 68% rename from Models/CloudProduct/ImportConverters/ConverterToGoodsFactory.cs rename to Models/CloudProduct/ImportConverters/GoodsConverterFactory.cs index 3d47e04..e4bc497 100644 --- a/Models/CloudProduct/ImportConverters/ConverterToGoodsFactory.cs +++ b/Models/CloudProduct/ImportConverters/GoodsConverterFactory.cs @@ -1,27 +1,23 @@ using Store.Modules.CloudProduct.Entities; -using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Store.Modules.CloudProduct.ImportConverters { /// /// 导入商品转换工厂:按顺序尝试各转换器,返回标准商品模型。 /// - public class ConverterToGoodsFactory + public class GoodsConverterFactory { private static readonly List _converters = new List { - new ConverterV1(), + new PddGoodsConverter(), }; /// /// 根据本地 JSON 文件路径转换。 /// - public static ProductEntity? Convert(string filePath) + public static GoodsEntity? Convert(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException(nameof(filePath)); @@ -29,24 +25,24 @@ namespace Store.Modules.CloudProduct.ImportConverters if (!File.Exists(filePath)) throw new FileNotFoundException("找不到导入文件", filePath); - string content = File.ReadAllText(filePath); + var content = File.ReadAllText(filePath); return ConvertContent(content); } /// /// 根据 JSON 内容字符串转换。 /// - public static ProductEntity? ConvertContent(string content) + public static GoodsEntity? ConvertContent(string content) { if (string.IsNullOrWhiteSpace(content)) throw new ArgumentNullException(nameof(content)); - foreach (IGoodsConverter item in _converters) + foreach (var converter in _converters) { - item.ReadContent(content); - if (item.IsMatch()) + converter.ReadContent(content); + if (converter.IsMatch()) { - var goods = item.Convert(); + var goods = converter.Convert(); if (goods != null) return goods; } @@ -55,4 +51,4 @@ namespace Store.Modules.CloudProduct.ImportConverters return null; } } -} \ No newline at end of file +} diff --git a/Models/CloudProduct/ImportConverters/ConverterV1.cs b/Models/CloudProduct/ImportConverters/PddGoodsConverter.cs similarity index 93% rename from Models/CloudProduct/ImportConverters/ConverterV1.cs rename to Models/CloudProduct/ImportConverters/PddGoodsConverter.cs index 465c3fd..52c5972 100644 --- a/Models/CloudProduct/ImportConverters/ConverterV1.cs +++ b/Models/CloudProduct/ImportConverters/PddGoodsConverter.cs @@ -1,9 +1,5 @@ - using Store.Modules.CloudProduct.Entities; using Store.Modules.CloudProduct.Enums; -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json.Nodes; namespace Store.Modules.CloudProduct.ImportConverters @@ -11,13 +7,13 @@ namespace Store.Modules.CloudProduct.ImportConverters /// /// PDD 前端商品详情数据转换器(数据源格式见 data.txt) /// - internal class ConverterV1 : GoodsConverterBase + internal class PddGoodsConverter : GoodsConverter { - public override string ConverterName => "ConverterV1"; + public override string ConverterName => nameof(PddGoodsConverter); public override PlatformType Platform => PlatformType.PDD; - protected override bool IsMatchProvider() + protected override bool IsMatchCore() { try { @@ -40,7 +36,7 @@ namespace Store.Modules.CloudProduct.ImportConverters } } - protected override ProductEntity ConverterProvider() + protected override GoodsEntity ConvertCore() { if (JsonNode.Parse(RawData) is not JsonObject root) throw new Exception("无法解析 PDD 商品数据"); @@ -48,7 +44,7 @@ namespace Store.Modules.CloudProduct.ImportConverters var goodsNode = root["goods"] as JsonObject ?? root; var priceNode = root["price"] as JsonObject; - var goods = new ProductEntity + var goods = new GoodsEntity { Platform = (byte)PlatformType.PDD, GoodsId = GetLong(goodsNode, "goods_id"), @@ -177,4 +173,4 @@ namespace Store.Modules.CloudProduct.ImportConverters return (null, null); } } -} \ No newline at end of file +} diff --git a/Models/CloudProduct/Repositorys/GoodsRepository.cs b/Models/CloudProduct/Repositorys/GoodsRepository.cs new file mode 100644 index 0000000..4b275df --- /dev/null +++ b/Models/CloudProduct/Repositorys/GoodsRepository.cs @@ -0,0 +1,16 @@ +using FreeSql; +using MAX.Core.DependencyInjection; +using Store.Modules.CloudProduct.Entities; + +namespace Store.Modules.CloudProduct.Repositorys +{ + /// + /// 商品仓储(对应 goods 表) + /// + public class GoodsRepository : BaseRepository, IScopedDependency + { + public GoodsRepository(IFreeSql fsql) : base(fsql) + { + } + } +} diff --git a/Models/CloudProduct/Services/ProductService.cs b/Models/CloudProduct/Services/GoodsService.cs similarity index 77% rename from Models/CloudProduct/Services/ProductService.cs rename to Models/CloudProduct/Services/GoodsService.cs index d314c83..895e643 100644 --- a/Models/CloudProduct/Services/ProductService.cs +++ b/Models/CloudProduct/Services/GoodsService.cs @@ -2,44 +2,42 @@ using MAX.Core.DependencyInjection; using Microsoft.Extensions.Configuration; using Store.Modules.CloudProduct.Dtos; using Store.Modules.CloudProduct.Entities; +using Store.Modules.CloudProduct.Repositorys; namespace Store.Modules.CloudProduct.Services { - public interface IProductService : IScopedDependency + public interface IGoodsService : IScopedDependency { Task DeleteGoodsAsync(IEnumerable ids); - Task> GetGoodsByIdsAsync(IEnumerable ids); + Task> GetGoodsByGoodsIdsAsync(IEnumerable goodsIds); Task ImportGoodsAsync(IEnumerable items); Task IndexPendingGoodsAsync(int batchSize); Task RebuildSearchIndexAsync(IEnumerable? ids = null); - Task<(List List, long Total)> SearchGoods(ProductSearchRequest request); - Task UpdateImage(IEnumerable goods); + Task<(List List, long Total)> SearchGoodsAsync(GoodsSearchRequest request); + Task UpdateThumbUrlAsync(IEnumerable goodsList); } - internal class ProductService : IProductService + internal class GoodsService : IGoodsService { - private readonly ProductRepository _repository; - private readonly OpenSearchServer _search; - private readonly RawDataStorage _rawDataStorage; + private readonly GoodsRepository _repository; + private readonly IOpenSearchService _searchService; + private readonly IRawDataStorage _rawDataStorage; private readonly IConfiguration _configuration; - public ProductService(ProductRepository repository, OpenSearchServer search, RawDataStorage rawDataStorage, IConfiguration configuration) + public GoodsService(GoodsRepository repository, IOpenSearchService searchService, IRawDataStorage rawDataStorage, IConfiguration configuration) { _repository = repository; - _search = search; + _searchService = searchService; _rawDataStorage = rawDataStorage; _configuration = configuration; } - public async Task<(List List, long Total)> SearchGoods(ProductSearchRequest request) + public async Task<(List List, long Total)> SearchGoodsAsync(GoodsSearchRequest request) { - // 1. 从搜索引擎获取标题匹配的商品ID列表 - List goodsList = new(); - long total = 0; + // 1. 从搜索引擎获取标题匹配的商品 + var searchResult = await _searchService.SearchByTitleAsync(request); - // 从搜索引擎查询标题匹配的ID - (long Total, List list) searchRet = (await _search.SearchByTitleAsync(request)); - goodsList = searchRet.list.Select(a => new ProductEntity + var goodsList = searchResult.Documents.Select(a => new GoodsEntity { Platform = a.Platform, GoodsId = a.Id, @@ -52,17 +50,9 @@ namespace Store.Modules.CloudProduct.Services MaxPrice = 0, SoldQuantity = a.SoldQuantity, ThumbUrl = a.ThumbUrl, - }).ToList(); - //goodsIds = searchRet.list.Select(d => d.Id).ToList(); - total = searchRet.Total; - //// 如果没有匹配的结果,直接返回空 - //if (!goodsIds.Any()) - //{ - // return (new List(), 0); - //} - return (goodsList, total); + return (goodsList, searchResult.Total); //// 2. 构建数据库查询(只查正常状态,排除软删除) //var query = _repository.Select.Where(x => x.Status == 0); @@ -155,15 +145,15 @@ namespace Store.Modules.CloudProduct.Services return result; var indexImmediately = IsImmediateIndexing(); - var docs = new List(); + var documents = new List(); var indexedIds = new List(); foreach (var item in items) { - var goods = item?.ProductEntity; + var goods = item?.Goods; if (goods == null) { - result.Failed++; + result.FailedCount++; result.Errors.Add("商品数据为空"); continue; } @@ -171,7 +161,7 @@ namespace Store.Modules.CloudProduct.Services var goodsId = goods.GoodsId; if (goodsId == 0) { - result.Failed++; + result.FailedCount++; result.Errors.Add("商品编号(GoodsId)不能为空"); continue; } @@ -191,7 +181,7 @@ namespace Store.Modules.CloudProduct.Services if (indexImmediately) { - docs.Add(new ProductSearchInfo + documents.Add(new GoodsSearchDocument { Platform = saved.Platform, Id = saved.GoodsId, @@ -207,27 +197,27 @@ namespace Store.Modules.CloudProduct.Services indexedIds.Add(saved.GoodsId); } - result.Success++; + result.SuccessCount++; } catch (Exception ex) { - result.Failed++; + result.FailedCount++; result.Errors.Add($"GoodsId={goodsId}: {ex.Message}"); } } - if (docs.Count > 0) + if (documents.Count > 0) { try { - await _search.BulkIndexGoodsAsync(docs); + await _searchService.BulkIndexGoodsAsync(documents); await MarkIndexedAsync(indexedIds); } catch (Exception ex) { // 写入失败时保持“未同步”,由后台定时任务重试 - result.Failed += docs.Count; - result.Success -= docs.Count; + result.FailedCount += documents.Count; + result.SuccessCount -= documents.Count; result.Errors.Add("写入 OpenSearch 失败: " + ex.Message); } } @@ -236,7 +226,7 @@ namespace Store.Modules.CloudProduct.Services } /// - /// 后台任务:批量处理未同步(IndexStatus=0)的商品,写入 OpenSearch 后标记为已同步。 + /// 后台任务:批量处理未同步(IsSegmented=0)的商品,写入 OpenSearch 后标记为已同步。 /// public async Task IndexPendingGoodsAsync(int batchSize) { @@ -252,8 +242,8 @@ namespace Store.Modules.CloudProduct.Services if (pending.Count == 0) return 0; - var docs = pending - .Select(x => new ProductSearchInfo + var documents = pending + .Select(x => new GoodsSearchDocument { Platform = x.Platform, Id = x.GoodsId, @@ -268,7 +258,7 @@ namespace Store.Modules.CloudProduct.Services }) .ToList(); - await _search.BulkIndexGoodsAsync(docs); + await _searchService.BulkIndexGoodsAsync(documents); await MarkIndexedAsync(pending.Select(x => x.GoodsId)); return pending.Count; @@ -277,7 +267,7 @@ namespace Store.Modules.CloudProduct.Services /// /// 按平台 + 商品编号查找已存在记录,存在则更新,不存在则新增。 /// - private async Task UpsertGoodsAsync(ProductEntity goods) + private async Task UpsertGoodsAsync(GoodsEntity goods) { var existing = await _repository.Select .Where(x => x.Platform == goods.Platform && x.GoodsId == goods.GoodsId) @@ -299,13 +289,13 @@ namespace Store.Modules.CloudProduct.Services } /// - /// 根据数据库主键 ID 列表读取商品。 + /// 根据商品编号(GoodsId)列表读取商品。 /// - public async Task> GetGoodsByIdsAsync(IEnumerable ids) + public async Task> GetGoodsByGoodsIdsAsync(IEnumerable goodsIds) { - var idList = ids?.ToList(); + var idList = goodsIds?.ToList(); if (idList == null || idList.Count == 0) - return new List(); + return new List(); return await _repository.Select .Where(x => idList.Contains(x.GoodsId)) @@ -318,7 +308,7 @@ namespace Store.Modules.CloudProduct.Services /// public async Task RebuildSearchIndexAsync(IEnumerable? ids = null) { - List goodsList; + List goodsList; var idList = ids?.ToList(); if (idList == null || idList.Count == 0) @@ -332,7 +322,7 @@ namespace Store.Modules.CloudProduct.Services .ToListAsync(); } - var count = await _search.RebuildIndexAsync(goodsList); + var count = await _searchService.RebuildIndexAsync(goodsList); await MarkIndexedAsync(goodsList.Where(x => x.GoodsId > 0).Select(x => x.GoodsId)); return count; } @@ -350,7 +340,7 @@ namespace Store.Modules.CloudProduct.Services if (IsSoftDelete()) { // 软删除:仅用 Status 标记(0正常 1已删除) - await _repository.Orm.Update() + await _repository.Orm.Update() .Set(x => x.Status, (byte)1) .Where(x => idList.Contains(x.GoodsId)) .ExecuteAffrowsAsync(); @@ -358,25 +348,28 @@ namespace Store.Modules.CloudProduct.Services else { // 硬删除:直接删除数据库记录 - await _repository.Orm.Delete() + await _repository.Orm.Delete() .Where(x => idList.Contains(x.GoodsId)) .ExecuteAffrowsAsync(); } // 无论软删除还是硬删除,都要删除 OpenSearch 中的对应文档 - await _search.DeleteDocumentsAsync(idList); + await _searchService.DeleteDocumentsAsync(idList); return idList.Count; } - public async Task UpdateImage(IEnumerable goods) + /// + /// 批量更新商品缩略图。 + /// + public async Task UpdateThumbUrlAsync(IEnumerable goodsList) { - //string sql = _repository.Orm.Update().SetSource(goods, a => a.GoodsId).UpdateColumns(a => new { a.ThumbUrl }).ToSql(); - return await _repository.Orm.Update() - .SetSource(goods, a => a.GoodsId) + //string sql = _repository.Orm.Update().SetSource(goodsList, a => a.GoodsId).UpdateColumns(a => new { a.ThumbUrl }).ToSql(); + return await _repository.Orm.Update() + .SetSource(goodsList, a => a.GoodsId) .UpdateColumns(a => new { a.ThumbUrl }) .ExecuteAffrowsAsync(); - } + /// /// 是否导入时立即分词(Indexing:Mode = Immediate)。 /// @@ -400,10 +393,10 @@ namespace Store.Modules.CloudProduct.Services if (idList == null || idList.Count == 0) return; - await _repository.Orm.Update() + await _repository.Orm.Update() .Set(x => x.IsSegmented, (byte)1) .Where(x => idList.Contains(x.GoodsId)) .ExecuteAffrowsAsync(); } } -} \ No newline at end of file +} diff --git a/Models/CloudProduct/Services/OpenSearchServer.cs b/Models/CloudProduct/Services/OpenSearchService.cs similarity index 69% rename from Models/CloudProduct/Services/OpenSearchServer.cs rename to Models/CloudProduct/Services/OpenSearchService.cs index 236b02f..9c8c7f9 100644 --- a/Models/CloudProduct/Services/OpenSearchServer.cs +++ b/Models/CloudProduct/Services/OpenSearchService.cs @@ -1,53 +1,53 @@ using MAX.Core.DependencyInjection; using MAX.Framework.Commons; using Microsoft.Extensions.Configuration; -using MySqlX.XDevAPI; using OpenSearch.Client; using OpenSearch.Net; using Store.Modules.CloudProduct.Dtos; using Store.Modules.CloudProduct.Entities; +using Store.Modules.CloudProduct.Enums; namespace Store.Modules.CloudProduct.Services { - public interface IOpenSearchServer: ISingletonDependency + public interface IOpenSearchService : ISingletonDependency { - Task AddOneRecordAsync(ProductSearchInfo goods); - Task BulkIndexGoodsAsync(IEnumerable docs); + Task AddDocumentAsync(GoodsSearchDocument document); + Task BulkIndexGoodsAsync(IEnumerable documents); Task ClearIndexAsync(); Task CountDocumentsAsync(); Task CreateIndexAsync(); Task DeleteDocumentsAsync(IEnumerable ids); Task DeleteIndexAsync(); Task GetStatusAsync(); - Task RebuildIndexAsync(IEnumerable goodsList); - Task<(long Total, List list)> SearchByTitleAsync(ProductSearchRequest request, GoodsSearchSortBy sortBy = GoodsSearchSortBy.Relevance); + Task RebuildIndexAsync(IEnumerable goodsList); + Task<(long Total, List Documents)> SearchByTitleAsync(GoodsSearchRequest request, GoodsSearchSortBy sortBy = GoodsSearchSortBy.Relevance); } - internal class OpenSearchServer : IOpenSearchServer + internal class OpenSearchService : IOpenSearchService { - private readonly string nodeUrl; - private IOpenSearchClient _client = null!; - private readonly string IndexName = ""; + private readonly string _nodeUrl; + private readonly string _indexName; + private readonly IOpenSearchClient _client = null!; // 查询类操作(search/count/status/ping)统一超时 private static readonly TimeSpan QueryTimeout = TimeSpan.FromSeconds(5); // 批量写入/建索引/删除索引等管理类操作超时(数据量大时需要更长,可挪到配置文件) private static readonly TimeSpan AdminTimeout = TimeSpan.FromSeconds(30); - public OpenSearchServer(IConfiguration configuration) + public OpenSearchService(IConfiguration configuration) { var config = configuration.GetSection("Search"); - nodeUrl = config.GetValue("Url") ?? ""; - IndexName = config.GetValue("IndexName") ?? ""; + _nodeUrl = config.GetValue("Url") ?? ""; + _indexName = config.GetValue("IndexName") ?? ""; _client = CreateClient(); } private IOpenSearchClient CreateClient() { - var pool = new SingleNodeConnectionPool(new Uri(nodeUrl)); + var pool = new SingleNodeConnectionPool(new Uri(_nodeUrl)); var settings = new ConnectionSettings(pool) - .DefaultIndex(IndexName) - .DefaultMappingFor(m => m.IndexName(IndexName)) + .DefaultIndex(_indexName) + .DefaultMappingFor(m => m.IndexName(_indexName)) .RequestTimeout(QueryTimeout) .MaxRetryTimeout(QueryTimeout) .PrettyJson() @@ -61,18 +61,18 @@ namespace Store.Modules.CloudProduct.Services /// public async Task CreateIndexAsync() { - var existsResponse = await _client.Indices.ExistsAsync(IndexName, d => d + var existsResponse = await _client.Indices.ExistsAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(AdminTimeout))); if (existsResponse.Exists) { if (await IsIndexMappingHealthyAsync()) { - Console.WriteLine($"索引 [{IndexName}] 已存在且映射正确,无需重复创建。"); + Console.WriteLine($"索引 [{_indexName}] 已存在且映射正确,无需重复创建。"); return true; } - Console.WriteLine($"索引 [{IndexName}] 的映射不完整(可能由清空索引等操作造成),正在重建..."); - var deleteResponse = await _client.Indices.DeleteAsync(IndexName, d => d + Console.WriteLine($"索引 [{_indexName}] 的映射不完整(可能由清空索引等操作造成),正在重建..."); + var deleteResponse = await _client.Indices.DeleteAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(AdminTimeout))); if (!deleteResponse.IsValid) { @@ -91,7 +91,7 @@ namespace Store.Modules.CloudProduct.Services if (createResponse.IsValid) { - Console.WriteLine($"索引 [{IndexName}] 创建成功。"); + Console.WriteLine($"索引 [{_indexName}] 创建成功。"); return true; } else @@ -107,9 +107,9 @@ namespace Store.Modules.CloudProduct.Services { try { - var getResponse = await _client.Indices.GetAsync(IndexName, d => d + var getResponse = await _client.Indices.GetAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(QueryTimeout))); - if (!getResponse.IsValid || !getResponse.Indices.TryGetValue(IndexName, out var indexState)) + if (!getResponse.IsValid || !getResponse.Indices.TryGetValue(_indexName, out var indexState)) { return false; } @@ -135,12 +135,12 @@ namespace Store.Modules.CloudProduct.Services var indexAnalyzer = useIk ? "ik_max_word" : "standard"; var searchAnalyzer = useIk ? "ik_smart" : "standard"; - return await _client.Indices.CreateAsync(IndexName, c => c + return await _client.Indices.CreateAsync(_indexName, c => c .Settings(s => s .NumberOfShards(1) .NumberOfReplicas(0) ) - .Map(m => m + .Map(m => m .Properties(p => p .Number(n => n.Name(x => x.Id).Type(NumberType.Long)) .Text(t => t @@ -165,7 +165,7 @@ namespace Store.Modules.CloudProduct.Services /// public async Task DeleteIndexAsync() { - var response = await _client.Indices.DeleteAsync(IndexName, d => d + var response = await _client.Indices.DeleteAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(AdminTimeout))); if (!response.IsValid) { @@ -176,18 +176,18 @@ namespace Store.Modules.CloudProduct.Services /// /// ============ 3.1 批量写入商品搜索文档(存在则覆盖) ============ /// - public async Task BulkIndexGoodsAsync(IEnumerable docs) + public async Task BulkIndexGoodsAsync(IEnumerable documents) { - if (docs == null) throw new ArgumentNullException(nameof(docs)); + if (documents == null) throw new ArgumentNullException(nameof(documents)); - var list = docs.ToList(); + var list = documents.ToList(); if (list.Count == 0) return; // 确保索引已按 IK 分词映射创建,否则中文分词无效 await CreateIndexAsync(); var bulkResponse = await _client.BulkAsync(b => b - .Index(IndexName) + .Index(_indexName) .IndexMany(list, (descriptor, doc) => descriptor.Id(doc.Id)) .RequestConfiguration(r => r.RequestTimeout(AdminTimeout)) ); @@ -198,20 +198,20 @@ namespace Store.Modules.CloudProduct.Services } // 刷新索引,保证数据立即可搜索(仅用于演示,生产环境不建议频繁 Refresh) - await _client.Indices.RefreshAsync(IndexName, d => d + await _client.Indices.RefreshAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(AdminTimeout))); } /// /// ============ 3.2 根据数据库商品重建搜索索引 ============ /// - public async Task RebuildIndexAsync(IEnumerable goodsList) + public async Task RebuildIndexAsync(IEnumerable goodsList) { if (goodsList == null) throw new ArgumentNullException(nameof(goodsList)); - var docs = goodsList + var documents = goodsList .Where(x => x != null && x.GoodsId > 0) - .Select(x => new ProductSearchInfo + .Select(x => new GoodsSearchDocument { Id = x.GoodsId, Title = x.Title ?? string.Empty, @@ -224,21 +224,21 @@ namespace Store.Modules.CloudProduct.Services }) .ToList(); - if (docs.Count == 0) return 0; + if (documents.Count == 0) return 0; - await BulkIndexGoodsAsync(docs); - return docs.Count; + await BulkIndexGoodsAsync(documents); + return documents.Count; } /// - /// ============ 3.3 按主键批量删除 OpenSearch 文档(软/硬删除共用) ============ + /// ============ 3.3 按商品编号批量删除 OpenSearch 文档(软/硬删除共用) ============ /// public async Task DeleteDocumentsAsync(IEnumerable ids) { var list = ids?.Distinct().ToList() ?? new List(); if (list.Count == 0) return; - var exists = await _client.Indices.ExistsAsync(IndexName, d => d + var exists = await _client.Indices.ExistsAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(QueryTimeout))); if (!exists.Exists) { @@ -249,7 +249,7 @@ namespace Store.Modules.CloudProduct.Services { foreach (var id in list) { - b.Delete(d => d.Index(IndexName).Id(id)); + b.Delete(d => d.Index(_indexName).Id(id)); } b.RequestConfiguration(r => r.RequestTimeout(AdminTimeout)); return b; @@ -260,25 +260,26 @@ namespace Store.Modules.CloudProduct.Services throw new AppException("删除 OpenSearch 文档失败: " + response.DebugInformation); } - await _client.Indices.RefreshAsync(IndexName, d => d + await _client.Indices.RefreshAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(AdminTimeout))); } /// - /// ============ 4. 手动新增一条 ============ + /// ============ 4. 手动新增一条搜索文档 ============ /// - public async Task AddOneRecordAsync(ProductSearchInfo goods) + public async Task AddDocumentAsync(GoodsSearchDocument document) { - if (goods == null) throw new ArgumentNullException(nameof(goods)); - var response = await _client.IndexAsync(goods, i => i - .Index(IndexName) - .Id(goods.Id) + if (document == null) throw new ArgumentNullException(nameof(document)); + + var response = await _client.IndexAsync(document, i => i + .Index(_indexName) + .Id(document.Id) .RequestConfiguration(r => r.RequestTimeout(QueryTimeout)) ); if (response.IsValid) { - await _client.Indices.RefreshAsync(IndexName, d => d + await _client.Indices.RefreshAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(QueryTimeout))); } else @@ -288,51 +289,37 @@ namespace Store.Modules.CloudProduct.Services } /// - /// ============ 5. 分词搜索(分页 + 类目/价格过滤 + 排序) ============ + /// ============ 5. 分词搜索(分页 + 类目/价格/销量/平台过滤 + 排序) ============ /// - /// 搜索关键词 - /// 一级类目,不传则不限制 - /// 二级类目,不传则不限制 - /// 三级类目,不传则不限制 - /// 四级类目,不传则不限制 - /// 最低价(含),不传则不限制 - /// 最高价(含),不传则不限制 + /// 搜索条件(关键词、类目、价格、销量、平台、分页) /// 排序方式 - /// 页码,从 1 开始 - /// 每页条数 - public async Task<(long Total, List list)> SearchByTitleAsync(ProductSearchRequest request, + public async Task<(long Total, List Documents)> SearchByTitleAsync(GoodsSearchRequest request, GoodsSearchSortBy sortBy = GoodsSearchSortBy.Relevance) { - string keyword = request.Title; + var keyword = request.Title; int? catId1 = request.CatId1; int? catId2 = request.CatId2; int? catId3 = request.CatId3; int? catId4 = request.CatId4; long? minPrice = request.MinPrice; long? maxPrice = request.MaxPrice; - int? minsold = request.MinSoldQuantity; - int? maxsold = request.MaxSoldQuantity; + int? minSoldQuantity = request.MinSoldQuantity; + int? maxSoldQuantity = request.MaxSoldQuantity; int? platform = request.Platform; int pageIndex = request.PageIndex; int pageSize = request.PageSize; - - //if (string.IsNullOrWhiteSpace(keyword)) - //{ - // throw new AppException("关键词不能为空。"); - //} - if (pageIndex < 1) pageIndex = 1; if (pageSize < 1) pageSize = 20; if (pageSize > 2000) pageSize = 2000; // 避免深分页/单页过大拖垮性能,按需调整上限 var from = (pageIndex - 1) * pageSize; - ISearchResponse searchResponse; + ISearchResponse searchResponse; try { - searchResponse = await _client.SearchAsync(s => s - .Index(IndexName) + searchResponse = await _client.SearchAsync(s => s + .Index(_indexName) .Query(q => q .Bool(b => { @@ -341,7 +328,7 @@ namespace Store.Modules.CloudProduct.Services b.Must(m => m.Match(mm => mm.Field(f => f.Title).Query(keyword))); } - var filters = new List, QueryContainer>>(); + var filters = new List, QueryContainer>>(); if (platform.HasValue && platform!.Value > 0) filters.Add(f => f.Term(t => t.Field(x => x.Platform).Value(platform.Value))); if (catId1.HasValue) filters.Add(f => f.Term(t => t.Field(x => x.CatId1).Value(catId1.Value))); @@ -360,19 +347,17 @@ namespace Store.Modules.CloudProduct.Services })); } - if (minsold.HasValue || maxsold.HasValue) + if (minSoldQuantity.HasValue || maxSoldQuantity.HasValue) { filters.Add(f => f.Range(r => { var range = r.Field(x => x.SoldQuantity); - if (minsold.HasValue) range = range.GreaterThanOrEquals(minsold.Value); - if (maxsold.HasValue) range = range.LessThanOrEquals(maxsold.Value); + if (minSoldQuantity.HasValue) range = range.GreaterThanOrEquals(minSoldQuantity.Value); + if (maxSoldQuantity.HasValue) range = range.LessThanOrEquals(maxSoldQuantity.Value); return range; })); } - - if (filters.Count > 0) b.Filter(filters.ToArray()); return b; }) @@ -400,7 +385,6 @@ namespace Store.Modules.CloudProduct.Services throw new AppException("搜索失败: " + searchResponse.DebugInformation); } - //var ids = searchResponse.Documents.Select(d => d.Id).ToList(); return (searchResponse.Total, searchResponse.Documents.ToList()); } @@ -409,13 +393,13 @@ namespace Store.Modules.CloudProduct.Services /// public async Task CountDocumentsAsync() { - var countResponse = await _client.CountAsync(c => c - .Index(IndexName) + var countResponse = await _client.CountAsync(c => c + .Index(_indexName) .RequestConfiguration(r => r.RequestTimeout(QueryTimeout)) ); if (countResponse.IsValid) { - Console.WriteLine($"索引 [{IndexName}] 当前文档总数: {countResponse.Count}"); + Console.WriteLine($"索引 [{_indexName}] 当前文档总数: {countResponse.Count}"); return countResponse.Count; } else @@ -432,8 +416,8 @@ namespace Store.Modules.CloudProduct.Services { var status = new OpenSearchStatusDto { - NodeUrl = nodeUrl, - IndexName = IndexName, + NodeUrl = _nodeUrl, + IndexName = _indexName, }; try @@ -447,14 +431,14 @@ namespace Store.Modules.CloudProduct.Services return status; } - var exists = await _client.Indices.ExistsAsync(IndexName, d => d + var exists = await _client.Indices.ExistsAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(QueryTimeout))); status.IndexExists = exists.Exists; if (exists.Exists) { - var count = await _client.CountAsync(c => c - .Index(IndexName) + var count = await _client.CountAsync(c => c + .Index(_indexName) .RequestConfiguration(r => r.RequestTimeout(QueryTimeout))); status.DocumentCount = count.IsValid ? count.Count : -1; if (!count.IsValid) @@ -475,24 +459,24 @@ namespace Store.Modules.CloudProduct.Services /// public async Task ClearIndexAsync() { - var existsResponse = await _client.Indices.ExistsAsync(IndexName, d => d + var existsResponse = await _client.Indices.ExistsAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(QueryTimeout))); if (!existsResponse.Exists) { - Console.WriteLine($"索引 {IndexName} 不存在"); + Console.WriteLine($"索引 {_indexName} 不存在"); return false; } - var deleteResponse = await _client.Indices.DeleteAsync(IndexName, d => d + var deleteResponse = await _client.Indices.DeleteAsync(_indexName, d => d .RequestConfiguration(r => r.RequestTimeout(AdminTimeout))); if (!deleteResponse.IsValid) { - Console.WriteLine($"删除索引 {IndexName} 失败: {deleteResponse.DebugInformation}"); + Console.WriteLine($"删除索引 {_indexName} 失败: {deleteResponse.DebugInformation}"); return false; } - Console.WriteLine($"索引 {IndexName} 已删除"); + Console.WriteLine($"索引 {_indexName} 已删除"); return await CreateIndexAsync(); } } -} \ No newline at end of file +} diff --git a/Models/CloudProduct/Services/ProductRepository.cs b/Models/CloudProduct/Services/ProductRepository.cs deleted file mode 100644 index 2011e23..0000000 --- a/Models/CloudProduct/Services/ProductRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FreeSql; -using MAX.Core.DependencyInjection; -using Store.Modules.CloudProduct.Entities; - -namespace Store.Modules.CloudProduct.Services -{ - public class ProductRepository : BaseRepository, IScopedDependency - { - public ProductRepository(IFreeSql fsql) : base(fsql) - { - } - } -} diff --git a/Models/CloudProduct/Services/RawDataStorage.cs b/Models/CloudProduct/Services/RawDataStorage.cs index ddce2b6..0b953d6 100644 --- a/Models/CloudProduct/Services/RawDataStorage.cs +++ b/Models/CloudProduct/Services/RawDataStorage.cs @@ -1,4 +1,4 @@ -using MAX.Core.DependencyInjection; +using MAX.Core.DependencyInjection; using Microsoft.Extensions.Configuration; using System.IO.Compression; using System.Text; @@ -8,7 +8,25 @@ namespace Store.Modules.CloudProduct.Services /// /// 商品原始数据文件存储:按 goodsId % 100 分目录,文件名即商品ID,GZip 压缩保存。 /// - public class RawDataStorage : IScopedDependency + public interface IRawDataStorage : IScopedDependency + { + /// + /// 保存原始数据,返回保存后的完整路径。 + /// + Task SaveAsync(long goodsId, string rawData); + + /// + /// 读取并解压原始数据;文件不存在时返回 null。 + /// + Task ReadAsync(long goodsId); + + /// + /// 判断指定商品的原始数据文件是否存在。 + /// + bool Exists(long goodsId); + } + + internal class RawDataStorage : IRawDataStorage { private readonly string _rootPath; @@ -21,9 +39,7 @@ namespace Store.Modules.CloudProduct.Services _rootPath = Path.GetFullPath(configured); } - /// - /// 保存原始数据,返回保存后的完整路径。 - /// + /// public async Task SaveAsync(long goodsId, string rawData) { var filePath = GetFilePath(goodsId); @@ -38,9 +54,7 @@ namespace Store.Modules.CloudProduct.Services return filePath; } - /// - /// 读取并解压原始数据;文件不存在时返回 null。 - /// + /// public async Task ReadAsync(long goodsId) { var filePath = GetFilePath(goodsId); @@ -53,9 +67,7 @@ namespace Store.Modules.CloudProduct.Services return await reader.ReadToEndAsync(); } - /// - /// 判断指定商品的原始数据文件是否存在。 - /// + /// public bool Exists(long goodsId) { return File.Exists(GetFilePath(goodsId)); @@ -66,12 +78,5 @@ namespace Store.Modules.CloudProduct.Services var subDir = (goodsId % 100).ToString(); return Path.Combine(_rootPath, subDir, goodsId.ToString()); } - public async Task ReadAsync(string filePath) - { - await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - await using var gzip = new GZipStream(fileStream, CompressionMode.Decompress); - using var reader = new StreamReader(gzip, new UTF8Encoding(false)); - return await reader.ReadToEndAsync(); - } } -} \ No newline at end of file +} diff --git a/Models/CloudProduct/Store.Modules.CloudProduct.csproj b/Models/CloudProduct/Store.Modules.CloudProduct.csproj index e19e8eb..834fc3d 100644 --- a/Models/CloudProduct/Store.Modules.CloudProduct.csproj +++ b/Models/CloudProduct/Store.Modules.CloudProduct.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -11,9 +11,6 @@ - - -