本次提交包含多项核心更新:
1. 重构宠物模块数据结构:拆分皮肤图片为独立表,优化Pet、PetEvolution实体,调整字段类型与冗余字段
2. 新增宠物皮肤图片管理表,支持多进化阶段多图片展示
3. 完善宠物DTO,新增当前形态名称、皮肤信息与图片序列返回
4. 新增社区功能模块:
- 微信端社区Feed流、点赞/取消点赞接口
- 后台社区消息管理接口与服务实现
5. 优化商城与背包模块,替换皮肤图片获取逻辑为从新表读取预览图
6. 重构勋章服务,新增规则校验逻辑
7. 调整命名规范,修复原有控制器命名问题
547 lines
21 KiB
C#
547 lines
21 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.Entity;
|
||
using QYZH.InteractiveMagazine.Repository;
|
||
using System.Text.Json;
|
||
|
||
namespace QYZH.InteractiveMagazine.Service;
|
||
|
||
/// <summary>
|
||
/// 小程序商城服务实现
|
||
/// </summary>
|
||
public class WxMallService(
|
||
BaseRepository<ExchangeRecord> exchangeRecordRepository,
|
||
ICheckInService checkInService,
|
||
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 == 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<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 == "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,
|
||
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 == "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 == "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
|
||
};
|
||
}
|
||
|
||
/// <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;
|
||
|
||
// 查询用户积分
|
||
var user = await exchangeRecordRepository.Context.Queryable<Users>()
|
||
.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<Users>()
|
||
.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<UserBag>()
|
||
.Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == "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,
|
||
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} 已放入背包"
|
||
};
|
||
}
|
||
|
||
/// <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,
|
||
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 == "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,
|
||
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 == "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 == "UsedUp")
|
||
.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<Pet>()
|
||
.Where(p => p.UserId == userId && !p.IsDeleted)
|
||
.FirstAsync();
|
||
|
||
if (pet == null)
|
||
throw new BusinessException("您还没有宠物", 404);
|
||
|
||
if (input.SkinId == 0)
|
||
{
|
||
// 恢复默认皮肤
|
||
await exchangeRecordRepository.Context.Updateable<Pet>()
|
||
.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 == "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<Pet>()
|
||
.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);
|
||
}
|
||
}
|