using Microsoft.Extensions.Logging; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto.Bag; using QYZH.InteractiveMagazine.Models.Dto.Mall; using QYZH.InteractiveMagazine.Models.Dto.Points; using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Repository; using System.Text.Json; namespace QYZH.InteractiveMagazine.Service; /// /// 小程序商城服务实现 /// public class WxMallService( BaseRepository exchangeRecordRepository, ICheckInService checkInService, IPointsService pointsService, ILogger logger) : BaseRepository, IWxMallService { /// /// 从 MetaData JSON 中提取 SkinId /// private static long GetSkinIdFromMetaData(string? metaData) { if (string.IsNullOrWhiteSpace(metaData)) return 0; try { using var doc = JsonDocument.Parse(metaData); if (doc.RootElement.TryGetProperty("SkinId", out var skinIdElement)) return skinIdElement.GetInt64(); if (doc.RootElement.TryGetProperty("skinId", out var skinIdLower)) return skinIdLower.GetInt64(); } catch { } return 0; } /// /// 获取商城商品列表(仅上架 + 在售) /// public async Task> GetProductsAsync(long userId, string? type = null) { var query = exchangeRecordRepository.Context.Queryable() .Where(p => !p.IsDeleted && p.IsActive && p.SaleStatus == "OnSale") .WhereIF(!string.IsNullOrEmpty(type), p => p.Type.ToString() == type) .OrderByDescending(p => p.CreatedAt); var products = await query.ToListAsync(); // 批量提取 PetBg 商品的 SkinId var skinProductMap = products .Where(p => p.Type == ProductTypeEnum.PetBg) .Select(p => new { ProductId = p.Id, SkinId = GetSkinIdFromMetaData(p.MetaData) }) .Where(x => x.SkinId > 0) .ToList(); var skinIds = skinProductMap.Select(x => x.SkinId).Distinct().ToList(); // 批量查询皮肤 var skins = skinIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() .Where(s => skinIds.Contains(s.Id) && !s.IsDeleted) .ToListAsync() : new List(); // 批量查询皮肤预览图(取每个皮肤的第一张图) var allPreviewImages = skinIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() .Where(i => skinIds.Contains(i.SkinId) && !i.IsDeleted) .OrderBy(i => i.SortOrder) .ToListAsync() : new List(); var previewImageMap = allPreviewImages .GroupBy(i => i.SkinId) .ToDictionary(g => g.Key, g => g.First().ImageUrl); // 批量查询用户背包(判断已拥有) var skinProductIds = skinProductMap.Select(x => x.ProductId).ToList(); var bagItems = skinProductIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && !b.IsDeleted && b.Status == UserBagStatusEnum.Available && skinProductIds.Contains(b.ItemId)) .ToListAsync() : new List(); return products.Select(p => { var skinMapping = skinProductMap.FirstOrDefault(x => x.ProductId == p.Id); var skin = skinMapping != null ? skins.FirstOrDefault(s => s.Id == skinMapping.SkinId) : null; var owned = bagItems.Any(b => b.ItemId == p.Id); var previewImg = skin != null && previewImageMap.ContainsKey(skin.Id) ? previewImageMap[skin.Id] : null; return new WxProductOutput { Id = p.Id, Name = p.Name, Description = p.Description, ImageUrl = p.ImageUrl, Price = p.Price, Type = p.Type.ToString(), Owned = owned, Skin = skin != null ? new PetSkinBrief { SkinId = skin.Id, SkinName = skin.Name, Description = skin.Description, Rarity = skin.Rarity, PreviewImage = previewImg } : null }; }).ToList(); } /// /// 获取商品详情 /// public async Task GetProductDetailAsync(long userId, long productId) { var product = await exchangeRecordRepository.Context.Queryable() .Where(p => p.Id == productId && !p.IsDeleted && p.IsActive && p.SaleStatus == "OnSale") .FirstAsync(); if (product == null) return null; PetSkinBrief? skinBrief = null; if (product.Type == ProductTypeEnum.PetBg) { var skinId = GetSkinIdFromMetaData(product.MetaData); if (skinId > 0) { var skin = await exchangeRecordRepository.Context.Queryable() .Where(s => s.Id == skinId && !s.IsDeleted) .FirstAsync(); if (skin != null) { var previewImg = await exchangeRecordRepository.Context.Queryable() .Where(i => i.SkinId == skinId && !i.IsDeleted) .OrderBy(i => i.SortOrder) .Select(i => i.ImageUrl) .FirstAsync(); skinBrief = new PetSkinBrief { SkinId = skin.Id, SkinName = skin.Name, Description = skin.Description, Rarity = skin.Rarity, PreviewImage = previewImg }; } } } var owned = await exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == UserBagStatusEnum.Available) .AnyAsync(); return new WxProductOutput { Id = product.Id, Name = product.Name, Description = product.Description, ImageUrl = product.ImageUrl, Price = product.Price, Type = product.Type.ToString(), Owned = owned, Skin = skinBrief }; } /// /// 积分兑换商品 /// public async Task ExchangeAsync(long userId, ExchangeInput input) { logger.LogInformation("用户兑换商品,UserId: {UserId}, ProductId: {ProductId}, Qty: {Qty}", userId, input.ProductId, input.Quantity); if (input.ProductId <= 0) throw new BusinessException("商品Id无效", 400); if (input.Quantity <= 0) throw new BusinessException("兑换数量必须大于0", 400); // 查询商品 var product = await exchangeRecordRepository.Context.Queryable() .Where(p => p.Id == input.ProductId && !p.IsDeleted && p.IsActive && p.SaleStatus == "OnSale") .FirstAsync(); if (product == null) throw new BusinessException("商品不存在或已下架", 404); var totalCost = product.Price * input.Quantity; DeductPointsOutput? pointsResult = null; long recordId = 0; await exchangeRecordRepository.UseTranAsync(async () => { // 创建兑换记录 var record = new ExchangeRecord { UserId = userId, ProductId = product.Id, ProductName = product.Name, ProductType = product.Type.ToString(), PointsCost = totalCost, PointsBalance = 0, // 稍后赋值 Quantity = input.Quantity, Status = ExchangeRecordStatusEnum.Success, IsDeleted = false, CreatedBy = userId.ToString(), CreatedAt = DateTime.Now, UpdatedBy = userId.ToString(), UpdatedAt = DateTime.Now }; var inserted = await exchangeRecordRepository.InsertReturnEntityAsync(record); recordId = inserted.Id; // 扣除积分 pointsResult = await pointsService.DeductPointsInTranAsync(new DeductPointsInput { UserId = userId, Amount = totalCost, ChangeType = PointsChangeTypeEnum.Exchange, RelatedId = recordId, Description = $"兑换 {product.Name} x{input.Quantity}" }); // 更新兑换记录的积分余额 await exchangeRecordRepository.Updateable() .SetColumns(r => r.PointsBalance == pointsResult.NewBalance) .Where(r => r.Id == recordId) .ExecuteCommandAsync(); // 加入背包 var existingBag = await exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == UserBagStatusEnum.Available) .FirstAsync(); if (existingBag != null) { // 已有同类物品,累加数量 await exchangeRecordRepository.Context.Updateable() .SetColumns(b => b.Quantity == existingBag.Quantity + input.Quantity) .SetColumns(b => b.UpdatedAt == DateTime.Now) .Where(b => b.Id == existingBag.Id) .ExecuteCommandAsync(); } else { // 新建背包物品 var bagItem = new UserBag { UserId = userId, ItemId = product.Id, ItemType = product.Type.ToString(), Quantity = input.Quantity, MetaData = product.MetaData, Type = Enum.TryParse(product.Type.ToString(), out var bagType) ? bagType : default, Status = UserBagStatusEnum.Available, IsDeleted = false, CreatedBy = userId.ToString(), CreatedAt = DateTime.Now, UpdatedBy = userId.ToString(), UpdatedAt = DateTime.Now }; await exchangeRecordRepository.Context.Insertable(bagItem).ExecuteCommandAsync(); } }); logger.LogInformation("兑换成功,UserId: {UserId}, Product: {Product}, Cost: {Cost}", userId, product.Name, totalCost); return new ExchangeOutput { RecordId = recordId, ProductName = product.Name, PointsCost = totalCost, PointsBalance = pointsResult!.NewBalance, Message = $"兑换成功!{product.Name} x{input.Quantity} 已放入背包" }; } /// /// 获取用户兑换记录 /// public async Task> GetExchangeRecordsAsync(long userId, int limit = 20) { return await exchangeRecordRepository.Queryable() .Where(r => r.UserId == userId && !r.IsDeleted) .OrderByDescending(r => r.CreatedAt) .Take(limit) .Select(r => new ExchangeRecordOutput { Id = r.Id, ProductId = r.ProductId, ProductName = r.ProductName, ProductType = r.ProductType, PointsCost = r.PointsCost, PointsBalance = r.PointsBalance, Quantity = r.Quantity, Status = r.Status.ToString(), CreatedAt = r.CreatedAt }) .ToListAsync(); } /// /// 获取用户背包物品 /// public async Task> GetBagItemsAsync(long userId, string? itemType = null) { var query = exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && !b.IsDeleted && b.Status == UserBagStatusEnum.Available) .WhereIF(!string.IsNullOrEmpty(itemType), b => b.ItemType == itemType) .OrderByDescending(b => b.CreatedAt); var bagItems = await query.ToListAsync(); if (bagItems.Count == 0) return []; // 批量查询关联商品 var itemIds = bagItems.Select(b => b.ItemId).Distinct().ToList(); var products = await exchangeRecordRepository.Context.Queryable() .Where(p => itemIds.Contains(p.Id) && !p.IsDeleted) .ToListAsync(); // 批量提取 PetBg 背包物品的 SkinId(从 MetaData 解析) var skinIdMap = bagItems .Where(b => b.ItemType == "PetBg") .Select(b => new { BagId = b.Id, SkinId = GetSkinIdFromMetaData(b.MetaData) }) .Where(x => x.SkinId > 0) .ToList(); var skinIds = skinIdMap.Select(x => x.SkinId).Distinct().ToList(); // 批量查询皮肤 var skins = skinIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() .Where(s => skinIds.Contains(s.Id) && !s.IsDeleted) .ToListAsync() : new List(); // 批量查询皮肤预览图 var allBagPreviewImages = skinIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() .Where(i => skinIds.Contains(i.SkinId) && !i.IsDeleted) .OrderBy(i => i.SortOrder) .ToListAsync() : new List(); var bagPreviewImageMap = allBagPreviewImages .GroupBy(i => i.SkinId) .ToDictionary(g => g.Key, g => g.First().ImageUrl); return bagItems.Select(b => { var product = products.FirstOrDefault(p => p.Id == b.ItemId); var skinMapping = skinIdMap.FirstOrDefault(x => x.BagId == b.Id); var skin = skinMapping != null ? skins.FirstOrDefault(s => s.Id == skinMapping.SkinId) : null; var previewImg = skin != null && bagPreviewImageMap.ContainsKey(skin.Id) ? bagPreviewImageMap[skin.Id] : null; return new UserBagOutput { Id = b.Id, ItemId = b.ItemId, ItemType = b.ItemType, Quantity = b.Quantity, Status = b.Status.ToString(), CreatedAt = b.CreatedAt, Product = product != null ? new BagProductBrief { Name = product.Name, ImageUrl = product.ImageUrl, Description = product.Description } : null, Skin = skin != null ? new BagSkinBrief { SkinId = skin.Id, SkinName = skin.Name, Rarity = skin.Rarity, PreviewImage = previewImg } : null }; }).ToList(); } /// /// 使用背包物品(目前仅支持补签卡) /// public async Task UseItemAsync(long userId, UseItemInput input) { logger.LogInformation("使用背包物品,UserId: {UserId}, BagItemId: {BagItemId}", userId, input.BagItemId); if (input.BagItemId <= 0) throw new BusinessException("背包物品Id无效", 400); var bagItem = await exchangeRecordRepository.Context.Queryable() .Where(b => b.Id == input.BagItemId && b.UserId == userId && !b.IsDeleted && b.Status == UserBagStatusEnum.Available) .FirstAsync(); if (bagItem == null) throw new BusinessException("背包物品不存在", 404); if (bagItem.Quantity <= 0) throw new BusinessException("物品数量不足", 400); switch (bagItem.ItemType) { case "MakeUpCard": return await UseMakeUpCardAsync(userId, bagItem, input); default: throw new BusinessException($"不支持使用该类型物品: {bagItem.ItemType}", 400); } } /// /// 使用补签卡 /// private async Task UseMakeUpCardAsync(long userId, UserBag bagItem, UseItemInput input) { if (string.IsNullOrEmpty(input.TargetDate)) throw new BusinessException("请指定补签日期", 400); if (!DateTime.TryParse(input.TargetDate, out var targetDate)) throw new BusinessException("日期格式无效", 400); targetDate = targetDate.Date; if (targetDate >= DateTime.Now.Date) throw new BusinessException("只能补签过去的日期", 400); // 调用签到服务执行补签 var checkInResult = await checkInService.MakeUpCheckInAsync(userId, targetDate); // 扣减补签卡数量 if (bagItem.Quantity <= 1) { await exchangeRecordRepository.Context.Updateable() .SetColumns(b => b.Status == UserBagStatusEnum.Expired) .SetColumns(b => b.Quantity == 0) .SetColumns(b => b.UpdatedAt == DateTime.Now) .Where(b => b.Id == bagItem.Id) .ExecuteCommandAsync(); } else { await exchangeRecordRepository.Context.Updateable() .SetColumns(b => b.Quantity == bagItem.Quantity - 1) .SetColumns(b => b.UpdatedAt == DateTime.Now) .Where(b => b.Id == bagItem.Id) .ExecuteCommandAsync(); } logger.LogInformation("补签卡使用成功,UserId: {UserId}, TargetDate: {Date}", userId, targetDate); return new UseItemOutput { IsSuccess = true, Message = $"补签 {targetDate:yyyy-MM-dd} 成功", Extra = checkInResult }; } /// /// 宠物换肤 /// public async Task EquipSkinAsync(long userId, EquipSkinInput input) { logger.LogInformation("宠物换肤,UserId: {UserId}, SkinId: {SkinId}", userId, input.SkinId); // 查询用户宠物 var pet = await exchangeRecordRepository.Context.Queryable() .Where(p => p.UserId == userId && !p.IsDeleted) .FirstAsync(); if (pet == null) throw new BusinessException("您还没有宠物", 404); if (input.SkinId == 0) { // 恢复默认皮肤 await exchangeRecordRepository.Context.Updateable() .SetColumns(p => p.CurrentSkinId == 0) .SetColumns(p => p.UpdatedAt == DateTime.Now) .Where(p => p.Id == pet.Id) .ExecuteCommandAsync(); logger.LogInformation("宠物恢复默认皮肤,UserId: {UserId}", userId); return; } // 查询皮肤 var skin = await exchangeRecordRepository.Context.Queryable() .Where(s => s.Id == input.SkinId && !s.IsDeleted) .FirstAsync(); if (skin == null) throw new BusinessException("皮肤不存在", 404); // 校验背包中是否拥有该皮肤(通过 MetaData 中的 SkinId 判断) var hasSkin = await exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && !b.IsDeleted && b.Status == UserBagStatusEnum.Available && b.Quantity > 0 && b.ItemType == "PetBg") .ToListAsync(); var owned = hasSkin.Any(b => GetSkinIdFromMetaData(b.MetaData) == input.SkinId); if (!owned) throw new BusinessException("您尚未拥有该皮肤,请先兑换", 400); // 换肤 await exchangeRecordRepository.Context.Updateable() .SetColumns(p => p.CurrentSkinId == skin.Id) .SetColumns(p => p.UpdatedAt == DateTime.Now) .Where(p => p.Id == pet.Id) .ExecuteCommandAsync(); logger.LogInformation("宠物换肤成功,UserId: {UserId}, Skin: {Skin}", userId, skin.Name); } }