修改代码
增加云商品数据
This commit is contained in:
1 parent
2f406c840f
commit
db4f030c17
58 files changed
+1550
-120
No files matched your search
@@ -0,0 +1,402 @@
|
||||
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 IGoodsService : IScopedDependency
|
||||
{
|
||||
Task<int> DeleteGoodsAsync(IEnumerable<long> ids);
|
||||
Task<List<GoodsEntity>> GetGoodsByGoodsIdsAsync(IEnumerable<long> goodsIds);
|
||||
Task<GoodsImportResult> ImportGoodsAsync(IEnumerable<GoodsImportItem> items);
|
||||
Task<int> IndexPendingGoodsAsync(int batchSize);
|
||||
Task<long> RebuildSearchIndexAsync(IEnumerable<long>? ids = null);
|
||||
Task<(List<GoodsEntity> List, long Total)> SearchGoodsAsync(GoodsSearchRequest request);
|
||||
Task<int> UpdateThumbUrlAsync(IEnumerable<GoodsEntity> goodsList);
|
||||
}
|
||||
|
||||
internal class GoodsService : IGoodsService
|
||||
{
|
||||
private readonly GoodsRepository _repository;
|
||||
private readonly IOpenSearchService _searchService;
|
||||
private readonly IRawDataStorage _rawDataStorage;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public GoodsService(GoodsRepository repository, IOpenSearchService searchService, IRawDataStorage rawDataStorage, IConfiguration configuration)
|
||||
{
|
||||
_repository = repository;
|
||||
_searchService = searchService;
|
||||
_rawDataStorage = rawDataStorage;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<(List<GoodsEntity> List, long Total)> SearchGoodsAsync(GoodsSearchRequest request)
|
||||
{
|
||||
// 1. 从搜索引擎获取标题匹配的商品
|
||||
var searchResult = await _searchService.SearchByTitleAsync(request);
|
||||
|
||||
var goodsList = searchResult.Documents.Select(a => new GoodsEntity
|
||||
{
|
||||
Platform = a.Platform,
|
||||
GoodsId = a.Id,
|
||||
Title = a.Title,
|
||||
CatId1 = a.CatId1,
|
||||
CatId2 = a.CatId2,
|
||||
CatId3 = a.CatId3,
|
||||
CatId4 = a.CatId4,
|
||||
MinPrice = a.MinPrice,
|
||||
MaxPrice = 0,
|
||||
SoldQuantity = a.SoldQuantity,
|
||||
ThumbUrl = a.ThumbUrl,
|
||||
}).ToList();
|
||||
|
||||
return (goodsList, searchResult.Total);
|
||||
|
||||
//// 2. 构建数据库查询(只查正常状态,排除软删除)
|
||||
//var query = _repository.Select.Where(x => x.Status == 0);
|
||||
|
||||
//// 如果有标题搜索,先用ID过滤
|
||||
//if (goodsIds.Any())
|
||||
//{
|
||||
// query = query.Where(x => goodsIds.Contains(x.GoodsId));
|
||||
//}
|
||||
|
||||
//// 3. 价格区间查询
|
||||
//if (request.MinPrice.HasValue)
|
||||
//{
|
||||
// query = query.Where(x => x.MinPrice >= request.MinPrice.Value);
|
||||
//}
|
||||
//if (request.MaxPrice.HasValue)
|
||||
//{
|
||||
// query = query.Where(x => x.MaxPrice <= request.MaxPrice.Value);
|
||||
//}
|
||||
|
||||
//// 4. 类目查询
|
||||
//if (request.CatId1.HasValue)
|
||||
//{
|
||||
// query = query.Where(x => x.CatId1 == request.CatId1);
|
||||
//}
|
||||
//if (request.CatId2.HasValue)
|
||||
//{
|
||||
// query = query.Where(x => x.CatId2 == request.CatId2);
|
||||
//}
|
||||
//if (request.CatId3.HasValue)
|
||||
//{
|
||||
// query = query.Where(x => x.CatId3 == request.CatId3);
|
||||
//}
|
||||
//if (request.CatId4.HasValue)
|
||||
//{
|
||||
// query = query.Where(x => x.CatId4 == request.CatId4);
|
||||
//}
|
||||
|
||||
//// 5. 销量区间查询
|
||||
//if (request.MinSoldQuantity.HasValue)
|
||||
//{
|
||||
// query = query.Where(x => x.SoldQuantity >= request.MinSoldQuantity.Value);
|
||||
//}
|
||||
//if (request.MaxSoldQuantity.HasValue)
|
||||
//{
|
||||
// query = query.Where(x => x.SoldQuantity <= request.MaxSoldQuantity.Value);
|
||||
//}
|
||||
|
||||
//// 6. 平台过滤
|
||||
//if (request.Platform.HasValue)
|
||||
//{
|
||||
// query = query.Where(x => x.Platform == request.Platform);
|
||||
//}
|
||||
|
||||
//// 8. 获取总数
|
||||
//total = await query.CountAsync();
|
||||
|
||||
//// 9. 排序
|
||||
//if (!string.IsNullOrEmpty(request.SortBy))
|
||||
//{
|
||||
// var isAsc = request.SortOrder?.ToLower() == "asc";
|
||||
// query = query.OrderByPropertyName(request.SortBy, isAsc);
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// query = query.OrderByDescending(x => x.CreatedTime);
|
||||
//}
|
||||
|
||||
//// 10. 分页查询
|
||||
//var list = await query
|
||||
// //.Skip((request.PageIndex - 1) * request.PageSize)
|
||||
// //.Limit(request.PageSize)
|
||||
// .ToListAsync();
|
||||
////内存排序
|
||||
//var orderMap = goodsIds.Select((id, idx) => new { id, idx })
|
||||
// .ToDictionary(x => x.id, x => x.idx);
|
||||
//list = list.OrderBy(x => orderMap.TryGetValue(x.GoodsId, out var idx) ? idx : int.MaxValue)
|
||||
// .ToList();
|
||||
//return (list, total);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量导入商品:先落库(按平台 + 商品编号去重)+ 保存原始数据,
|
||||
/// 然后根据配置决定“立即分词”还是“标记未同步交给后台任务分词”。
|
||||
/// </summary>
|
||||
public async Task<GoodsImportResult> ImportGoodsAsync(IEnumerable<GoodsImportItem> items)
|
||||
{
|
||||
var result = new GoodsImportResult();
|
||||
if (items == null)
|
||||
return result;
|
||||
|
||||
var indexImmediately = IsImmediateIndexing();
|
||||
var documents = new List<GoodsSearchDocument>();
|
||||
var indexedIds = new List<long>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var goods = item?.Goods;
|
||||
if (goods == null)
|
||||
{
|
||||
result.FailedCount++;
|
||||
result.Errors.Add("商品数据为空");
|
||||
continue;
|
||||
}
|
||||
|
||||
var goodsId = goods.GoodsId;
|
||||
if (goodsId == 0)
|
||||
{
|
||||
result.FailedCount++;
|
||||
result.Errors.Add("商品编号(GoodsId)不能为空");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 导入时统一先标记“未同步”;立即分词成功后再置为“已同步”,
|
||||
// 后台模式下保持“未同步”,由 Hangfire 定时任务分词。
|
||||
goods.IsSegmented = 0;
|
||||
var saved = await UpsertGoodsAsync(goods);
|
||||
|
||||
var rawData = item?.RawData ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(rawData))
|
||||
{
|
||||
await _rawDataStorage.SaveAsync(saved.GoodsId, rawData);
|
||||
}
|
||||
|
||||
if (indexImmediately)
|
||||
{
|
||||
documents.Add(new GoodsSearchDocument
|
||||
{
|
||||
Platform = saved.Platform,
|
||||
Id = saved.GoodsId,
|
||||
Title = saved.Title ?? string.Empty,
|
||||
ThumbUrl = saved.ThumbUrl,
|
||||
CatId1 = saved.CatId1,
|
||||
CatId2 = saved.CatId2,
|
||||
CatId3 = saved.CatId3,
|
||||
CatId4 = saved.CatId4,
|
||||
MinPrice = saved.MinPrice,
|
||||
SoldQuantity = saved.SoldQuantity,
|
||||
});
|
||||
indexedIds.Add(saved.GoodsId);
|
||||
}
|
||||
|
||||
result.SuccessCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.FailedCount++;
|
||||
result.Errors.Add($"GoodsId={goodsId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (documents.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _searchService.BulkIndexGoodsAsync(documents);
|
||||
await MarkIndexedAsync(indexedIds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 写入失败时保持“未同步”,由后台定时任务重试
|
||||
result.FailedCount += documents.Count;
|
||||
result.SuccessCount -= documents.Count;
|
||||
result.Errors.Add("写入 OpenSearch 失败: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台任务:批量处理未同步(IsSegmented=0)的商品,写入 OpenSearch 后标记为已同步。
|
||||
/// </summary>
|
||||
public async Task<int> IndexPendingGoodsAsync(int batchSize)
|
||||
{
|
||||
if (batchSize <= 0)
|
||||
batchSize = 200;
|
||||
|
||||
var pending = await _repository.Select
|
||||
.Where(x => x.IsSegmented == 0 && x.Status == 0)
|
||||
.OrderBy(x => x.GoodsId)
|
||||
.Limit(batchSize)
|
||||
.ToListAsync();
|
||||
|
||||
if (pending.Count == 0)
|
||||
return 0;
|
||||
|
||||
var documents = pending
|
||||
.Select(x => new GoodsSearchDocument
|
||||
{
|
||||
Platform = x.Platform,
|
||||
Id = x.GoodsId,
|
||||
Title = x.Title ?? string.Empty,
|
||||
ThumbUrl = x.ThumbUrl,
|
||||
CatId1 = x.CatId1,
|
||||
CatId2 = x.CatId2,
|
||||
CatId3 = x.CatId3,
|
||||
CatId4 = x.CatId4,
|
||||
MinPrice = x.MinPrice,
|
||||
SoldQuantity = x.SoldQuantity,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
await _searchService.BulkIndexGoodsAsync(documents);
|
||||
|
||||
await MarkIndexedAsync(pending.Select(x => x.GoodsId));
|
||||
return pending.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按平台 + 商品编号查找已存在记录,存在则更新,不存在则新增。
|
||||
/// </summary>
|
||||
private async Task<GoodsEntity> UpsertGoodsAsync(GoodsEntity goods)
|
||||
{
|
||||
var existing = await _repository.Select
|
||||
.Where(x => x.Platform == goods.Platform && x.GoodsId == goods.GoodsId)
|
||||
.FirstAsync();
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
goods.Id = existing.Id;
|
||||
goods.GoodsId = existing.GoodsId;
|
||||
goods.CreatedTime = existing.CreatedTime;
|
||||
goods.UpdatedTime = DateTime.Now;
|
||||
await _repository.UpdateAsync(goods);
|
||||
return goods;
|
||||
}
|
||||
|
||||
goods.CreatedTime = DateTime.Now;
|
||||
goods.UpdatedTime = DateTime.Now;
|
||||
return await _repository.InsertAsync(goods);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据商品编号(GoodsId)列表读取商品。
|
||||
/// </summary>
|
||||
public async Task<List<GoodsEntity>> GetGoodsByGoodsIdsAsync(IEnumerable<long> goodsIds)
|
||||
{
|
||||
var idList = goodsIds?.ToList();
|
||||
if (idList == null || idList.Count == 0)
|
||||
return new List<GoodsEntity>();
|
||||
|
||||
return await _repository.Select
|
||||
.Where(x => idList.Contains(x.GoodsId))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重新从数据库读取指定商品,覆盖 OpenSearch 中已有的分词并重新分词。
|
||||
/// ids 为空时重建全部商品。重建成功的记录标记为已同步。
|
||||
/// </summary>
|
||||
public async Task<long> RebuildSearchIndexAsync(IEnumerable<long>? ids = null)
|
||||
{
|
||||
List<GoodsEntity> goodsList;
|
||||
|
||||
var idList = ids?.ToList();
|
||||
if (idList == null || idList.Count == 0)
|
||||
{
|
||||
goodsList = await _repository.Select.ToListAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
goodsList = await _repository.Select
|
||||
.Where(x => idList.Contains(x.GoodsId))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
var count = await _searchService.RebuildIndexAsync(goodsList);
|
||||
await MarkIndexedAsync(goodsList.Where(x => x.GoodsId > 0).Select(x => x.GoodsId));
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除商品:按配置决定软删除(Status=1)还是硬删除(物理删除),
|
||||
/// 两种方式都会删除 OpenSearch 中对应的文档。
|
||||
/// </summary>
|
||||
public async Task<int> DeleteGoodsAsync(IEnumerable<long> ids)
|
||||
{
|
||||
var idList = ids?.Distinct().ToList();
|
||||
if (idList == null || idList.Count == 0)
|
||||
return 0;
|
||||
|
||||
if (IsSoftDelete())
|
||||
{
|
||||
// 软删除:仅用 Status 标记(0正常 1已删除)
|
||||
await _repository.Orm.Update<GoodsEntity>()
|
||||
.Set(x => x.Status, (byte)1)
|
||||
.Where(x => idList.Contains(x.GoodsId))
|
||||
.ExecuteAffrowsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 硬删除:直接删除数据库记录
|
||||
await _repository.Orm.Delete<GoodsEntity>()
|
||||
.Where(x => idList.Contains(x.GoodsId))
|
||||
.ExecuteAffrowsAsync();
|
||||
}
|
||||
|
||||
// 无论软删除还是硬删除,都要删除 OpenSearch 中的对应文档
|
||||
await _searchService.DeleteDocumentsAsync(idList);
|
||||
return idList.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新商品缩略图。
|
||||
/// </summary>
|
||||
public async Task<int> UpdateThumbUrlAsync(IEnumerable<GoodsEntity> goodsList)
|
||||
{
|
||||
//string sql = _repository.Orm.Update<GoodsEntity>().SetSource(goodsList, a => a.GoodsId).UpdateColumns(a => new { a.ThumbUrl }).ToSql();
|
||||
return await _repository.Orm.Update<GoodsEntity>()
|
||||
.SetSource(goodsList, a => a.GoodsId)
|
||||
.UpdateColumns(a => new { a.ThumbUrl })
|
||||
.ExecuteAffrowsAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否导入时立即分词(Indexing:Mode = Immediate)。
|
||||
/// </summary>
|
||||
private bool IsImmediateIndexing()
|
||||
{
|
||||
var mode = _configuration.GetValue<string>("Indexing:Mode");
|
||||
return string.Equals(mode, "Immediate", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否软删除(Delete:SoftDelete,默认 true)。
|
||||
/// </summary>
|
||||
private bool IsSoftDelete()
|
||||
{
|
||||
return _configuration.GetValue<bool?>("Delete:SoftDelete") ?? true;
|
||||
}
|
||||
|
||||
private async Task MarkIndexedAsync(IEnumerable<long> ids)
|
||||
{
|
||||
var idList = ids?.ToList();
|
||||
if (idList == null || idList.Count == 0)
|
||||
return;
|
||||
|
||||
await _repository.Orm.Update<GoodsEntity>()
|
||||
.Set(x => x.IsSegmented, (byte)1)
|
||||
.Where(x => idList.Contains(x.GoodsId))
|
||||
.ExecuteAffrowsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user