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;
using System.Text.Json;
namespace QYZH.InteractiveMagazine.Service;
///
/// 小程序商城服务实现
///
public class WxMallService(
BaseRepository exchangeRecordRepository,
ICheckInService checkInService,
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 == type)
.OrderByDescending(p => p.CreatedAt);
var products = await query.ToListAsync();
// 批量提取 PetBg 商品的 SkinId
var skinProductMap = products
.Where(p => p.Type == "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 == "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,
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 == "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 == "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 背包物品的 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,
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 == "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);
// 校验背包中是否拥有该皮肤(通过 MetaData 中的 SkinId 判断)
var hasSkin = await exchangeRecordRepository.Context.Queryable()
.Where(b => b.UserId == userId && !b.IsDeleted && b.Status == "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);
}
}