- 新增30+业务枚举类型覆盖用户、宠物、商城、社区、积分等模块 - 完成实体类、DTO、服务层的字符串枚举替换为强类型枚举 - 修复用户状态枚举名称变更,将Frozen改为Disabled - 新增批量发布社区消息接口与控制器实现 - 新增操作日志、积分管理、补偿任务相关服务与DTO
529 lines
20 KiB
C#
529 lines
20 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 小程序商城服务实现
|
||
/// </summary>
|
||
public class WxMallService(
|
||
BaseRepository<ExchangeRecord> exchangeRecordRepository,
|
||
ICheckInService checkInService,
|
||
IPointsService pointsService,
|
||
ILogger<WxMallService> logger)
|
||
: BaseRepository<ExchangeRecord>, IWxMallService
|
||
{
|
||
/// <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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取商城商品列表(仅上架 + 在售)
|
||
/// </summary>
|
||
public async Task<List<WxProductOutput>> GetProductsAsync(long userId, string? type = null)
|
||
{
|
||
var query = exchangeRecordRepository.Context.Queryable<Product>()
|
||
.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<PetSkin>()
|
||
.Where(s => skinIds.Contains(s.Id) && !s.IsDeleted)
|
||
.ToListAsync()
|
||
: new List<PetSkin>();
|
||
|
||
// 批量查询皮肤预览图(取每个皮肤的第一张图)
|
||
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);
|
||
|
||
// 批量查询用户背包(判断已拥有)
|
||
var skinProductIds = skinProductMap.Select(x => x.ProductId).ToList();
|
||
var bagItems = skinProductIds.Count > 0
|
||
? await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||
.Where(b => b.UserId == userId && !b.IsDeleted && b.Status == UserBagStatusEnum.Available
|
||
&& skinProductIds.Contains(b.ItemId))
|
||
.ToListAsync()
|
||
: new List<UserBag>();
|
||
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取商品详情
|
||
/// </summary>
|
||
public async Task<WxProductOutput?> GetProductDetailAsync(long userId, long productId)
|
||
{
|
||
var product = await exchangeRecordRepository.Context.Queryable<Product>()
|
||
.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<PetSkin>()
|
||
.Where(s => s.Id == skinId && !s.IsDeleted)
|
||
.FirstAsync();
|
||
|
||
if (skin != null)
|
||
{
|
||
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
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
var owned = await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||
.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
|
||
};
|
||
}
|
||
|
||
/// <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)
|
||
throw new BusinessException("商品Id无效", 400);
|
||
|
||
if (input.Quantity <= 0)
|
||
throw new BusinessException("兑换数量必须大于0", 400);
|
||
|
||
// 查询商品
|
||
var product = await exchangeRecordRepository.Context.Queryable<Product>()
|
||
.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<ExchangeRecord>()
|
||
.SetColumns(r => r.PointsBalance == pointsResult.NewBalance)
|
||
.Where(r => r.Id == recordId)
|
||
.ExecuteCommandAsync();
|
||
|
||
// 加入背包
|
||
var existingBag = await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||
.Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == UserBagStatusEnum.Available)
|
||
.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,
|
||
ItemType = product.Type.ToString(),
|
||
Quantity = input.Quantity,
|
||
MetaData = product.MetaData,
|
||
Type = Enum.TryParse<UserBagTypeEnum>(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} 已放入背包"
|
||
};
|
||
}
|
||
|
||
/// <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,
|
||
Status = r.Status.ToString(),
|
||
CreatedAt = r.CreatedAt
|
||
})
|
||
.ToListAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取用户背包物品
|
||
/// </summary>
|
||
public async Task<List<UserBagOutput>> GetBagItemsAsync(long userId, string? itemType = null)
|
||
{
|
||
var query = exchangeRecordRepository.Context.Queryable<UserBag>()
|
||
.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<Product>()
|
||
.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<PetSkin>()
|
||
.Where(s => skinIds.Contains(s.Id) && !s.IsDeleted)
|
||
.ToListAsync()
|
||
: new List<PetSkin>();
|
||
|
||
// 批量查询皮肤预览图
|
||
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);
|
||
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用背包物品(目前仅支持补签卡)
|
||
/// </summary>
|
||
public async Task<UseItemOutput> 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<UserBag>()
|
||
.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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用补签卡
|
||
/// </summary>
|
||
private async Task<UseItemOutput> 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<UserBag>()
|
||
.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<UserBag>()
|
||
.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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 宠物换肤
|
||
/// </summary>
|
||
public async Task EquipSkinAsync(long userId, EquipSkinInput input)
|
||
{
|
||
logger.LogInformation("宠物换肤,UserId: {UserId}, SkinId: {SkinId}", userId, input.SkinId);
|
||
|
||
// 查询用户宠物
|
||
var pet = await exchangeRecordRepository.Context.Queryable<UserPet>()
|
||
.Where(p => p.UserId == userId && !p.IsDeleted)
|
||
.FirstAsync();
|
||
|
||
if (pet == null)
|
||
throw new BusinessException("您还没有宠物", 404);
|
||
|
||
if (input.SkinId == 0)
|
||
{
|
||
// 恢复默认皮肤
|
||
await exchangeRecordRepository.Context.Updateable<UserPet>()
|
||
.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)
|
||
throw new BusinessException("皮肤不存在", 404);
|
||
|
||
// 校验背包中是否拥有该皮肤(通过 MetaData 中的 SkinId 判断)
|
||
var hasSkin = await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||
.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<UserPet>()
|
||
.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);
|
||
}
|
||
}
|