feat: add wechat mini program mall, check-in supplement and pet skin system
This commit implements a complete WeChat mini program mall and related features: 1. Add pet skin entity and backpack item type support 2. Add make-up check-in and missed date query functions 3. Implement mall product browsing, exchange and record query 4. Add backpack management and item usage (make-up card) 5. Add pet skin equipping function 6. Fix UserBag ItemId type from int to long 7. Adjust Dockerfile exposed port to 8080 8. Fix session cookie unprotect warning
This commit is contained in:
470
QYZH.InteractiveMagazine.Service/WxMallService.cs
Normal file
470
QYZH.InteractiveMagazine.Service/WxMallService.cs
Normal file
@ -0,0 +1,470 @@
|
||||
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;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 小程序商城服务实现
|
||||
/// </summary>
|
||||
public class WxMallService(
|
||||
BaseRepository<ExchangeRecord> exchangeRecordRepository,
|
||||
ICheckInService checkInService,
|
||||
ILogger<WxMallService> logger)
|
||||
: BaseRepository<ExchangeRecord>, IWxMallService
|
||||
{
|
||||
/// <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();
|
||||
|
||||
// 批量查询皮肤信息
|
||||
var skinProductIds = products.Where(p => p.Type == "PetBg").Select(p => p.Id).ToList();
|
||||
var skins = skinProductIds.Count > 0
|
||||
? await exchangeRecordRepository.Context.Queryable<PetSkin>()
|
||||
.Where(s => skinProductIds.Contains(s.ProductId) && !s.IsDeleted)
|
||||
.ToListAsync()
|
||||
: new List<PetSkin>();
|
||||
|
||||
// 批量查询用户背包(判断已拥有)
|
||||
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 skin = skins.FirstOrDefault(s => s.ProductId == p.Id);
|
||||
var owned = bagItems.Any(b => b.ItemId == p.Id);
|
||||
|
||||
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,
|
||||
SkinImage = skin.ImageUrl,
|
||||
Description = skin.Description,
|
||||
Rarity = skin.Rarity
|
||||
} : 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 skin = await exchangeRecordRepository.Context.Queryable<PetSkin>()
|
||||
.Where(s => s.ProductId == product.Id && !s.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
if (skin != null)
|
||||
{
|
||||
skinBrief = new PetSkinBrief
|
||||
{
|
||||
SkinId = skin.Id,
|
||||
SkinName = skin.Name,
|
||||
SkinImage = skin.ImageUrl,
|
||||
Description = skin.Description,
|
||||
Rarity = skin.Rarity
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 类型)
|
||||
var skinProductIds = bagItems.Where(b => b.ItemType == "PetBg").Select(b => b.ItemId).Distinct().ToList();
|
||||
var skins = skinProductIds.Count > 0
|
||||
? await exchangeRecordRepository.Context.Queryable<PetSkin>()
|
||||
.Where(s => skinProductIds.Contains(s.ProductId) && !s.IsDeleted)
|
||||
.ToListAsync()
|
||||
: new List<PetSkin>();
|
||||
|
||||
return bagItems.Select(b =>
|
||||
{
|
||||
var product = products.FirstOrDefault(p => p.Id == b.ItemId);
|
||||
var skin = skins.FirstOrDefault(s => s.ProductId == b.ItemId);
|
||||
|
||||
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,
|
||||
SkinImage = skin.ImageUrl,
|
||||
Rarity = skin.Rarity
|
||||
} : 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);
|
||||
|
||||
// 校验背包中是否拥有该皮肤(通过关联商品Id判断)
|
||||
var hasSkin = await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||||
.Where(b => b.UserId == userId && b.ItemId == skin.ProductId
|
||||
&& !b.IsDeleted && b.Status == "Available" && b.Quantity > 0)
|
||||
.AnyAsync();
|
||||
|
||||
if (!hasSkin)
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user