using Asp.Versioning; using MAX.Framework.Commons; using MAX.Framework.Dtos; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Store.Framework.Base; using Store.Modules.CloudProduct.Dtos; using Store.Modules.CloudProduct.Entities; using Store.Modules.CloudProduct.ImportConverters; using Store.Modules.CloudProduct.Services; using System.Text.Json.Nodes; namespace Store.Modules.CloudProduct.Controllers { [ApiVersionNeutral] [Route("api/CloudGoods/[action]")] public class GoodsController : AppControllerBase { private readonly IGoodsService _goodsService; private readonly IOpenSearchService _searchService; private readonly IRawDataStorage _rawDataStorage; private readonly GoodsIndexJob _goodsIndexJob; public GoodsController(IGoodsService goodsService, IOpenSearchService searchService, IRawDataStorage rawDataStorage, GoodsIndexJob goodsIndexJob) { _goodsService = goodsService; _searchService = searchService; _rawDataStorage = rawDataStorage; _goodsIndexJob = goodsIndexJob; } [HttpGet] public async Task Time() { return DateTime.Now.ToLongDateString(); } [HttpGet] public async Task Job() { await _goodsIndexJob.ProcessPendingAsync(); } /// /// 搜索 /// /// /// [HttpPost] public async Task> SearchGoods(GoodsSearchRequest request) { // 1. 参数基础校验 if (request == null) { throw new AppException("请求参数不能为空", ResultCode.ValidationError); } // 2. 分页参数校验 if (request.PageIndex < 1) { throw new AppException("页码必须大于0", ResultCode.ValidationError); } if (request.PageSize < 1 || request.PageSize > 2000) { throw new AppException("每页数量必须在1-2000之间", ResultCode.ValidationError); } // 3. 价格区间校验 if (request.MinPrice.HasValue && request.MinPrice < 0) { throw new AppException("最低价格不能小于0", ResultCode.ValidationError); } if (request.MaxPrice.HasValue && request.MaxPrice < 0) { throw new AppException("最高价格不能小于0", ResultCode.ValidationError); } if (request.MinPrice.HasValue && request.MaxPrice.HasValue && request.MinPrice > request.MaxPrice) { throw new AppException("最低价格不能大于最高价格", ResultCode.ValidationError); } // 4. 销量区间校验 if (request.MinSoldQuantity.HasValue && request.MinSoldQuantity < 0) { throw new AppException("最低销量不能小于0", ResultCode.ValidationError); } if (request.MaxSoldQuantity.HasValue && request.MaxSoldQuantity < 0) { throw new AppException("最高销量不能小于0", ResultCode.ValidationError); } if (request.MinSoldQuantity.HasValue && request.MaxSoldQuantity.HasValue && request.MinSoldQuantity > request.MaxSoldQuantity) { throw new AppException("最低销量不能大于最高销量", ResultCode.ValidationError); } // 5. 类目层级校验 if (request.CatId2.HasValue && !request.CatId1.HasValue) { throw new AppException("选择二级类目时必须先选择一级类目", ResultCode.ValidationError); } if (request.CatId3.HasValue && !request.CatId2.HasValue) { throw new AppException("选择三级类目时必须先选择二级类目", ResultCode.ValidationError); } if (request.CatId4.HasValue && !request.CatId3.HasValue) { throw new AppException("选择四级类目时必须先选择三级类目", ResultCode.ValidationError); } // 6. 平台校验 if (request.Platform.HasValue && request.Platform < 0) { throw new AppException("平台类型无效", ResultCode.ValidationError); } // 10. 至少有一个搜索条件 if (string.IsNullOrWhiteSpace(request.Title) && !request.MinPrice.HasValue && !request.MaxPrice.HasValue && !request.CatId1.HasValue && !request.CatId2.HasValue && !request.CatId3.HasValue && !request.CatId4.HasValue && !request.MinSoldQuantity.HasValue && !request.MaxSoldQuantity.HasValue && !request.Platform.HasValue) { throw new AppException("请至少提供一个搜索条件", ResultCode.ValidationError); } var result = await _goodsService.SearchGoodsAsync(request); return new PageData { List = result.List, Total = result.Total, PageSize = request.PageSize, PageIndex = request.PageIndex, }; } /// /// 分词搜索,直接返回索引中的商品摘要(路由保持 SearchGoodsToIds 不变) /// [HttpGet] [ActionName("SearchGoodsToIds")] public async Task> SearchGoodsSummary(string keyword, int pageindex = 1, int pagesize = 20) { if (string.IsNullOrWhiteSpace(keyword)) { throw new AppException("填写关键字", ResultCode.ValidationError); } var result = await _searchService.SearchByTitleAsync(new GoodsSearchRequest { Title = keyword, PageIndex = pageindex, PageSize = pagesize, }); return new PageData { PageIndex = pageindex, PageSize = pagesize, List = result.Documents, Total = result.Total, }; } /// /// 导入测试数据:读取 ImportConverters/data.txt 并走完整解析 + 入库 + 分词流程。 /// [HttpGet] public async Task ImportTestData() { var filePath = GetDataFilePath(); if (!System.IO.File.Exists(filePath)) { throw new AppException($"找不到测试数据文件: {filePath}", ResultCode.NotFound); } var content = await System.IO.File.ReadAllTextAsync(filePath); var goods = GoodsConverterFactory.ConvertContent(content); if (goods == null) { throw new AppException("未找到匹配的转换器,无法解析测试数据"); } return await _goodsService.ImportGoodsAsync(new[] { new GoodsImportItem { Goods = goods, RawData = content } }); } /// /// 导入接口:接收客户端批量上传的 JSON 文件,解析后入库并写入分词索引。 /// [HttpPost] public async Task ImportFiles([FromForm] List files) { if (files == null || files.Count == 0) { throw new AppException("请选择要导入的 JSON 文件", ResultCode.ValidationError); } var items = new List(); var parseErrors = new List(); foreach (var file in files) { if (file == null || file.Length == 0) { continue; } try { using var reader = new StreamReader(file.OpenReadStream()); var content = await reader.ReadToEndAsync(); var goods = GoodsConverterFactory.ConvertContent(content); if (goods == null) { parseErrors.Add($"{file.FileName}: 未找到匹配的转换器"); } else { items.Add(new GoodsImportItem { Goods = goods, RawData = content }); } } catch (Exception ex) { parseErrors.Add($"{file.FileName}: {ex.Message}"); } } var result = await _goodsService.ImportGoodsAsync(items); result.FailedCount += parseErrors.Count; result.Errors.InsertRange(0, parseErrors); return result; } /// /// 查询 OpenSearch 状态。 /// [HttpGet] public async Task GetOpenSearchStatus() { return await _searchService.GetStatusAsync(); } /// /// 检查搜索索引是否存在。 /// [HttpGet] public async Task IndexExists() { return await _searchService.IndexExistsAsync(); } /// /// 检查索引映射(分析器)是否符合当前服务要求。 /// [HttpGet] public async Task IndexMappingHealthy() { return await _searchService.IsIndexMappingHealthyAsync(); } /// /// 创建搜索索引(含 IK 分词映射,已存在且映射正常时跳过)。 /// [HttpPost] public async Task CreateIndex() { return await _searchService.CreateIndexAsync(); } /// /// 删除搜索索引。 /// [HttpPost] public async Task DeleteIndex() { await _searchService.DeleteIndexAsync(); } /// /// 手动刷新搜索索引,使最新写入立即可搜索。 /// [HttpPost] public async Task RefreshIndex() { await _searchService.RefreshIndexAsync(); } /// /// 手动新增或覆盖一条搜索文档。 /// [HttpPost] public async Task AddSearchDocument([FromBody] GoodsSearchDocument document) { if (document == null || document.Id <= 0) { throw new AppException("请提供有效的搜索文档", ResultCode.ValidationError); } await _searchService.AddDocumentAsync(document); } /// /// 读取指定商品详情:解压缩原始数据并返回 JSON。 /// [HttpPost] public async Task GetGoodsDetail([FromQuery] long goodsId) { var raw = await GetRawDataOrThrowAsync(goodsId); var node = JsonNode.Parse(raw); return node ?? throw new AppException("商品详情解析失败", ResultCode.BusinessError); } /// /// 读取指定商品的原始数据(解压缩后原样返回)。 /// [HttpPost] public async Task GetGoodsRawData([FromQuery] long goodsId) { return await GetRawDataOrThrowAsync(goodsId); } /// /// 重新从数据库读取指定商品,覆盖 OpenSearch 已有分词并重新分词。 /// ids 为空时重建全部商品。 /// [HttpPost] public async Task RebuildIndex([FromBody] long[]? ids) { return await _goodsService.RebuildSearchIndexAsync(ids); } /// /// 删除商品(按商品编号)。是否软删除由配置 Delete:SoftDelete 决定, /// 软删除将 Status 置 1,硬删除物理删除记录;两种方式都会删除 OpenSearch 中对应文档。 /// [HttpPost] public async Task DeleteGoods([FromBody] long[]? ids) { if (ids == null || ids.Length == 0) { throw new AppException("请提供要删除的商品ID", ResultCode.ValidationError); } 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) { if (goodsId <= 0) throw new AppException("商品ID无效", ResultCode.ValidationError); var raw = await _rawDataStorage.ReadAsync(goodsId); if (raw == null) throw new AppException($"商品原始数据不存在: {goodsId}", ResultCode.NotFound); return raw; } private static string GetDataFilePath() { var outputPath = Path.Combine(AppContext.BaseDirectory, "ImportConverters", "data.txt"); if (System.IO.File.Exists(outputPath)) return outputPath; var workPath = Path.Combine(Directory.GetCurrentDirectory(), "ImportConverters", "data.txt"); if (System.IO.File.Exists(workPath)) return workPath; return outputPath; } } }