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.Entity; using QYZH.InteractiveMagazine.Repository; namespace QYZH.InteractiveMagazine.Service; /// /// 小程序商城服务实现 /// public class WxMallService( BaseRepository exchangeRecordRepository, ICheckInService checkInService, ILogger logger) : BaseRepository, IWxMallService { /// /// 获取商城商品列表(仅上架 + 在售) /// 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 == type) .OrderByDescending(p => p.CreatedAt); var products = await query.ToListAsync(); // 批量查询皮肤信息 var skinProductIds = products.Where(p => p.Type == "PetBg").Select(p => p.Id).ToList(); var skins = skinProductIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() .Where(s => skinProductIds.Contains(s.ProductId) && !s.IsDeleted) .ToListAsync() : new List(); // 批量查询用户背包(判断已拥有) var bagItems = skinProductIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && !b.IsDeleted && b.Status == "Available" && skinProductIds.Contains(b.ItemId)) .ToListAsync() : new List(); return products.Select(p => { var skin = skins.FirstOrDefault(s => s.ProductId == p.Id); var owned = bagItems.Any(b => b.ItemId == p.Id); return new WxProductOutput { Id = p.Id, Name = p.Name, Description = p.Description, ImageUrl = p.ImageUrl, Price = p.Price, Type = p.Type, Owned = owned, Skin = skin != null ? new PetSkinBrief { SkinId = skin.Id, SkinName = skin.Name, SkinImage = skin.ImageUrl, Description = skin.Description, Rarity = skin.Rarity } : 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 == "PetBg") { var skin = await exchangeRecordRepository.Context.Queryable() .Where(s => s.ProductId == product.Id && !s.IsDeleted) .FirstAsync(); if (skin != null) { skinBrief = new PetSkinBrief { SkinId = skin.Id, SkinName = skin.Name, SkinImage = skin.ImageUrl, Description = skin.Description, Rarity = skin.Rarity }; } } var owned = await exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == "Available") .AnyAsync(); return new WxProductOutput { Id = product.Id, Name = product.Name, Description = product.Description, ImageUrl = product.ImageUrl, Price = product.Price, Type = product.Type, 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; // 查询用户积分 var user = await exchangeRecordRepository.Context.Queryable() .Where(u => u.Id == userId && !u.IsDeleted) .FirstAsync(); if (user == null) throw new BusinessException("用户不存在", 404); if (user.Points < totalCost) throw new BusinessException($"积分不足,需要 {totalCost} 积分,当前余额 {user.Points}", 400); var newPointsBalance = user.Points - totalCost; long recordId = 0; await exchangeRecordRepository.UseTranAsync(async () => { // 扣除用户积分 await exchangeRecordRepository.Context.Updateable() .SetColumns(u => u.Points == newPointsBalance) .SetColumns(u => u.UpdatedAt == DateTime.Now) .Where(u => u.Id == userId && !u.IsDeleted) .ExecuteCommandAsync(); // 创建兑换记录 var record = new ExchangeRecord { UserId = userId, ProductId = product.Id, ProductName = product.Name, ProductType = product.Type, PointsCost = totalCost, PointsBalance = newPointsBalance, Quantity = input.Quantity, Status = "Success", IsDeleted = false, CreatedBy = userId.ToString(), CreatedAt = DateTime.Now, UpdatedBy = userId.ToString(), UpdatedAt = DateTime.Now }; var inserted = await exchangeRecordRepository.InsertReturnEntityAsync(record); recordId = inserted.Id; // 加入背包 var existingBag = await exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == "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, Quantity = input.Quantity, MetaData = product.MetaData, Type = product.Type, Status = "Available", IsDeleted = false, CreatedBy = userId.ToString(), CreatedAt = DateTime.Now, UpdatedBy = userId.ToString(), UpdatedAt = DateTime.Now }; await exchangeRecordRepository.Context.Insertable(bagItem).ExecuteCommandAsync(); } // 创建积分消耗记录 var pointsRecord = new PointsRecord { UserId = userId, ChangeAmount = -totalCost, BalanceAfter = newPointsBalance, ChangeType = "Exchange", RelatedId = recordId, Description = $"兑换 {product.Name} x{input.Quantity}", Type = "Expense", Status = "Success", IsDeleted = false, CreatedBy = userId.ToString(), CreatedAt = DateTime.Now, UpdatedBy = userId.ToString(), UpdatedAt = DateTime.Now }; await exchangeRecordRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync(); }); logger.LogInformation("兑换成功,UserId: {UserId}, Product: {Product}, Cost: {Cost}", userId, product.Name, totalCost); return new ExchangeOutput { RecordId = recordId, ProductName = product.Name, PointsCost = totalCost, PointsBalance = newPointsBalance, 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, 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 == "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 类型) var skinProductIds = bagItems.Where(b => b.ItemType == "PetBg").Select(b => b.ItemId).Distinct().ToList(); var skins = skinProductIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() .Where(s => skinProductIds.Contains(s.ProductId) && !s.IsDeleted) .ToListAsync() : new List(); return bagItems.Select(b => { var product = products.FirstOrDefault(p => p.Id == b.ItemId); var skin = skins.FirstOrDefault(s => s.ProductId == b.ItemId); return new UserBagOutput { Id = b.Id, ItemId = b.ItemId, ItemType = b.ItemType, Quantity = b.Quantity, Status = b.Status, 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, SkinImage = skin.ImageUrl, Rarity = skin.Rarity } : 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 == "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 == "UsedUp") .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); // 校验背包中是否拥有该皮肤(通过关联商品Id判断) var hasSkin = await exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && b.ItemId == skin.ProductId && !b.IsDeleted && b.Status == "Available" && b.Quantity > 0) .AnyAsync(); if (!hasSkin) 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); } }