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