Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.Service/WxMallService.cs
glz 8291b82362 feat: 完成宠物模块重构与社区功能开发
本次提交包含多项核心更新:
1.  重构宠物模块数据结构:拆分皮肤图片为独立表,优化Pet、PetEvolution实体,调整字段类型与冗余字段
2.  新增宠物皮肤图片管理表,支持多进化阶段多图片展示
3.  完善宠物DTO,新增当前形态名称、皮肤信息与图片序列返回
4.  新增社区功能模块:
    - 微信端社区Feed流、点赞/取消点赞接口
    - 后台社区消息管理接口与服务实现
5.  优化商城与背包模块,替换皮肤图片获取逻辑为从新表读取预览图
6.  重构勋章服务,新增规则校验逻辑
7.  调整命名规范,修复原有控制器命名问题
2026-06-05 17:15:30 +08:00

547 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}