Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.Service/WxMallService.cs
glz ae4cd627df feat: 完成多模块功能迭代与优化
1.  新增宠物皮肤初始枚举值、下拉列表接口及相关DTO
2.  完善商品实体与DTO,新增虚拟商品标记和类型ID字段
3.  重构用户认证体系,支持多用户切换并重新生成JWT令牌
4.  新增签到配置管理全套功能,包括增删改查和状态管理
5.  优化模型验证过滤器和基础响应类的命名规范
6.  新增补签功能,完善签到服务逻辑
7.  拆分用户详情DTO,新增各子数据分页查询接口
8.  重构微信控制器的用户ID获取逻辑,统一使用激活用户ID
9.  修复背包服务中补签卡的扣减逻辑
10. 新增家长姓名修改接口和相关服务实现
2026-06-10 17:53:38 +08:00

514 lines
20 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.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.Status == (int)ProductStatusEnum.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.PetSkin)
.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 == (int)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(),
IsVirtual = p.IsVirtual,
TypeId = p.TypeId,
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.Status == (int)ProductStatusEnum.OnSale)
.FirstAsync();
if (product == null) return null;
PetSkinBrief? skinBrief = null;
if (product.Type == ProductTypeEnum.PetSkin)
{
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 == (int)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(),
IsVirtual = product.IsVirtual,
TypeId = product.TypeId,
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.Status == (int)ProductStatusEnum.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 = (int)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 == (int)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 = (int)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 == (int)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.Status == (int)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);
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 == (int)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);
}
}