Files

464 lines
19 KiB
C#
Raw Permalink Normal View History

using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
using QYZH.InteractiveMagazine.Models.Dto.Pet;
using QYZH.InteractiveMagazine.Models.Dto.Points;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
namespace QYZH.InteractiveMagazine.Service;
/// <summary>
/// 签到服务实现
/// </summary>
public class CheckInService(
BaseRepository<CheckInRecord> checkInRecordRepository,
IPetService petService,
ICompensationTaskService compensationTaskService,
IPointsService pointsService,
ILogger<CheckInService> logger)
: BaseRepository<CheckInRecord>, ICheckInService
{
/// <summary>
/// 默认签到奖励积分(无配置时的兜底值)
/// </summary>
private const int DefaultRewardPoints = 10;
/// <summary>
/// 用户签到
/// </summary>
public async Task<CheckInOutput> CheckInAsync(long userId)
{
logger.LogInformation("用户签到UserId: {UserId}", userId);
var today = DateTime.Now.Date;
// 1. 检查今日是否已签到
var alreadyCheckedIn = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= today && r.CheckInDate < today.AddDays(1))
.AnyAsync();
if (alreadyCheckedIn)
{
throw new BusinessException("今日已签到,请明天再来", ResultCode.BAD_REQUEST);
}
// 2. 计算连续签到天数
var consecutiveDays = await CalculateConsecutiveDaysAsync(userId, today);
// 3. 查询签到配置,计算奖励
var (pointsReward, growthReward) = await CalculateRewardsAsync(consecutiveDays);
// 4. 查询用户信息
var user = await checkInRecordRepository.Context.Queryable<Users>()
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstAsync();
if (user == null)
{
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
}
// 5. 查询用户宠物(如果有)
var pet = await checkInRecordRepository.Context.Queryable<UserPet>()
.Where(p => p.UserId == userId && !p.IsDeleted)
.FirstAsync();
// 6. 事务执行签到相关写操作(含宠物喂养,统一事务)
var result = new CheckInOutput();
await checkInRecordRepository.UseTranAsync(async () =>
{
var duplicate = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= today && r.CheckInDate < today.AddDays(1))
.AnyAsync();
if (duplicate)
{
throw new BusinessException("今日已签到,请明天再来", ResultCode.CONFLICT);
}
// 6a. 创建签到记录
var checkInRecord = new CheckInRecord
{
UserId = userId,
CheckInDate = today,
PointsAwarded = pointsReward,
GrowthPointsAwarded = growthReward,
ConsecutiveDays = consecutiveDays,
Type = CheckInRecordTypeEnum.Normal,
Status = (int)CheckInRecordStatusEnum.Success,
CreatedBy = userId.ToString(),
UpdatedBy = userId.ToString()
};
var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync();
checkInRecord.Id = recordId;
// 6b. 更新用户成长值
var newGrowthBalance = user.GrowthPoints + growthReward;
await checkInRecordRepository.Context.Updateable<Users>()
.SetColumns(u => u.GrowthPoints == u.GrowthPoints + growthReward)
.SetColumns(u => u.UpdatedAt == DateTime.Now)
.Where(u => u.Id == userId && !u.IsDeleted)
.ExecuteCommandAsync();
newGrowthBalance = await checkInRecordRepository.Context.Queryable<Users>()
.Where(u => u.Id == userId && !u.IsDeleted)
.Select(u => u.GrowthPoints)
.FirstAsync();
// 6c. 通过积分服务增加积分
var pointsResult = await pointsService.AddPointsInTranAsync(new AddPointsInput
{
UserId = userId,
Amount = pointsReward,
ChangeType = PointsChangeTypeEnum.SignIn,
RelatedId = recordId,
Description = $"签到奖励(连续{consecutiveDays}天)"
});
// 6d. 如果有活跃宠物,喂养宠物(同一事务内)
FeedPetOutput? feedResult = null;
if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0)
{
feedResult = await petService.FeedPetInTranAsync(userId, new FeedPetInput
{
PetId = pet.Id,
GrowthPoints = growthReward
});
}
// 构建返回结果
result.RecordId = (long)recordId;
result.CheckInDate = today;
result.ConsecutiveDays = consecutiveDays;
result.PointsAwarded = pointsReward;
result.GrowthPointsAwarded = growthReward;
result.PointsBalance = pointsResult.NewBalance;
result.GrowthPointsBalance = newGrowthBalance;
result.HasPet = pet != null;
if (feedResult != null)
{
result.HasEvolved = feedResult.HasEvolved;
result.EvolvedStageName = feedResult.EvolvedStageName;
}
});
if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0)
{
logger.LogInformation("签到成长值已喂养宠物PetId: {PetId}, 进化: {HasEvolved}",
pet.Id, result.HasEvolved);
}
logger.LogInformation("用户签到成功UserId: {UserId}, 连续{Days}天, 积分+{Points}, 成长值+{Growth}",
userId, consecutiveDays, pointsReward, growthReward);
return result;
}
/// <summary>
/// 获取用户签到信息
/// </summary>
public async Task<CheckInInfoOutput> GetCheckInInfoAsync(long userId)
{
logger.LogInformation("获取签到信息UserId: {UserId}", userId);
var today = DateTime.Now.Date;
// 今日是否已签到
var hasCheckedInToday = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= today && r.CheckInDate < today.AddDays(1))
.AnyAsync();
// 累计签到天数
var totalCheckInDays = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted)
.CountAsync();
// 最近一次签到记录(用于获取连续天数)
var lastRecord = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted)
.OrderBy(r => r.CheckInDate, SqlSugar.OrderByType.Desc)
.FirstAsync();
// 判断连续天数:如果最后一次签到是今天或昨天,则连续天数延续
var consecutiveDays = 0;
if (lastRecord != null)
{
var lastDate = lastRecord.CheckInDate.Date;
if (lastDate == today || lastDate == today.AddDays(-1))
{
consecutiveDays = lastRecord.ConsecutiveDays;
}
}
// 查询用户余额
var user = await checkInRecordRepository.Context.Queryable<Users>()
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstAsync();
// 最近 7 天签到记录
var thirtyDaysAgo = today.AddDays(-6);
var recentRecords = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= thirtyDaysAgo)
.OrderBy(r => r.CheckInDate, SqlSugar.OrderByType.Desc)
.Select(r => new CheckInRecordOutput
{
Id = (long)r.Id,
CheckInDate = r.CheckInDate,
CreatedAt = r.CreatedAt,
ConsecutiveDays = r.ConsecutiveDays,
PointsAwarded = r.PointsAwarded,
GrowthPointsAwarded = r.GrowthPointsAwarded,
Type = r.Type.ToString(),
Status = r.Status.ToString()
})
.ToListAsync();
return new CheckInInfoOutput
{
HasCheckedInToday = hasCheckedInToday,
ConsecutiveDays = consecutiveDays,
TotalCheckInDays = totalCheckInDays,
PointsBalance = user?.Points ?? 0,
GrowthPointsBalance = user?.GrowthPoints ?? 0,
RecentRecords = recentRecords
};
}
/// <summary>
/// 补签(消耗补签卡,补签历史漏签日期)
/// </summary>
public async Task<CheckInOutput> MakeUpCheckInAsync(long userId, DateTime targetDate)
{
logger.LogInformation("用户补签UserId: {UserId}, TargetDate: {Date}", userId, targetDate);
targetDate = targetDate.Date;
if (targetDate >= DateTime.Now.Date)
throw new BusinessException("只能补签过去的日期", ResultCode.BAD_REQUEST);
// 检查目标日期是否已有签到记录
var alreadyCheckedIn = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted
&& r.CheckInDate >= targetDate && r.CheckInDate < targetDate.AddDays(1))
.AnyAsync();
if (alreadyCheckedIn)
throw new BusinessException($"{targetDate:yyyy-MM-dd} 已签到,无需补签", ResultCode.BAD_REQUEST);
// 检查用户背包中是否有补签卡
var makeUpCard = await checkInRecordRepository.Context.Queryable<UserBag>()
.Where(b => b.UserId == userId
&& b.ItemType == "MakeUpCard"
&& b.Quantity > 0
&& b.Status == (int)UserBagStatusEnum.Available
&& !b.IsDeleted)
.OrderByDescending(b => b.CreatedAt)
.FirstAsync();
if (makeUpCard == null)
throw new BusinessException("补签卡不足,无法补签", ResultCode.BAD_REQUEST);
// 查询用户信息
var user = await checkInRecordRepository.Context.Queryable<Users>()
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstAsync();
if (user == null)
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
// 查询宠物
var pet = await checkInRecordRepository.Context.Queryable<UserPet>()
.Where(p => p.UserId == userId && !p.IsDeleted)
.FirstAsync();
// 补签奖励按基础值计算(不享受连续签到加成)
var (pointsReward, growthReward) = await CalculateRewardsAsync(1);
var result = new CheckInOutput();
await checkInRecordRepository.UseTranAsync(async () =>
{
var duplicate = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= targetDate && r.CheckInDate < targetDate.AddDays(1))
.AnyAsync();
if (duplicate)
{
throw new BusinessException($"{targetDate:yyyy-MM-dd} 已签到,无需补签", ResultCode.CONFLICT);
}
// 创建补签记录
var checkInRecord = new CheckInRecord
{
UserId = userId,
CheckInDate = targetDate,
PointsAwarded = pointsReward,
GrowthPointsAwarded = growthReward,
ConsecutiveDays = 0, // 补签不纳入连续天数
Type = CheckInRecordTypeEnum.MakeUp,
Status = (int)CheckInRecordStatusEnum.Success,
IsDeleted = false,
CreatedBy = userId.ToString(),
CreatedAt = DateTime.Now,
UpdatedBy = userId.ToString(),
UpdatedAt = DateTime.Now
};
var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync();
checkInRecord.Id = recordId;
await checkInRecordRepository.Context.Updateable<Users>()
.SetColumns(u => u.GrowthPoints == u.GrowthPoints + growthReward)
.SetColumns(u => u.UpdatedAt == DateTime.Now)
.Where(u => u.Id == userId && !u.IsDeleted)
.ExecuteCommandAsync();
var newGrowthBalance = await checkInRecordRepository.Context.Queryable<Users>()
.Where(u => u.Id == userId && !u.IsDeleted)
.Select(u => u.GrowthPoints)
.FirstAsync();
// 通过积分服务增加积分
var pointsResult = await pointsService.AddPointsInTranAsync(new AddPointsInput
{
UserId = userId,
Amount = pointsReward,
ChangeType = PointsChangeTypeEnum.MakeUpSign,
RelatedId = recordId,
Description = $"补签奖励({targetDate:yyyy-MM-dd}"
});
// 扣减补签卡
if (makeUpCard.Quantity <= 1)
{
var cardAffectedRows = await checkInRecordRepository.Context.Updateable<UserBag>()
.SetColumns(b => b.Quantity == b.Quantity - 1)
.SetColumns(b => b.UpdatedAt == DateTime.Now)
.Where(b => b.Id == makeUpCard.Id && b.UserId == userId && b.Quantity > 0 && b.Status == (int)UserBagStatusEnum.Available && !b.IsDeleted)
.ExecuteCommandAsync();
if (cardAffectedRows <= 0)
{
throw new BusinessException("补签卡不足,无法补签", ResultCode.CONFLICT);
}
await checkInRecordRepository.Context.Updateable<UserBag>()
.SetColumns(b => b.Status == (int)UserBagStatusEnum.Expired)
.SetColumns(b => b.UpdatedAt == DateTime.Now)
.Where(b => b.Id == makeUpCard.Id && b.Quantity <= 0 && !b.IsDeleted)
.ExecuteCommandAsync();
}
// 如果有活跃宠物,喂养宠物(同一事务内)
FeedPetOutput? feedResult = null;
if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0)
{
feedResult = await petService.FeedPetInTranAsync(userId, new Models.Dto.Pet.FeedPetInput
{
PetId = pet.Id,
GrowthPoints = growthReward
});
}
result.RecordId = (long)recordId;
result.CheckInDate = targetDate;
result.ConsecutiveDays = 0;
result.PointsAwarded = pointsReward;
result.GrowthPointsAwarded = growthReward;
result.PointsBalance = pointsResult.NewBalance;
result.GrowthPointsBalance = newGrowthBalance;
result.HasPet = pet != null;
if (feedResult != null)
{
result.HasEvolved = feedResult.HasEvolved;
result.EvolvedStageName = feedResult.EvolvedStageName;
}
});
if (pet != null && pet.Status == (int)UserPetStatusEnum.Active && growthReward > 0)
{
logger.LogInformation("补签成长值已喂养宠物PetId: {PetId}, 进化: {HasEvolved}",
pet.Id, result.HasEvolved);
}
logger.LogInformation("补签成功UserId: {UserId}, Date: {Date}, 积分+{Points}, 成长值+{Growth}",
userId, targetDate, pointsReward, growthReward);
return result;
}
/// <summary>
/// 获取用户漏签日期列表
/// </summary>
public async Task<List<DateTime>> GetMissedDatesAsync(long userId, int days = 7)
{
var startDate = DateTime.Now.Date.AddDays(-days);
// 查询该时间段内所有签到记录
var checkedDates = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= startDate)
.Select(r => r.CheckInDate.Date)
.ToListAsync();
var checkedDateSet = new HashSet<DateTime>(checkedDates);
var missedDates = new List<DateTime>();
// 遍历每一天,找出漏签的日期(排除今天,今天不算漏签)
for (var date = startDate; date < DateTime.Now.Date; date = date.AddDays(1))
{
if (!checkedDateSet.Contains(date))
{
missedDates.Add(date);
}
}
return missedDates;
}
/// <summary>
/// 计算连续签到天数
/// </summary>
private async Task<int> CalculateConsecutiveDaysAsync(long userId, DateTime today)
{
var yesterday = today.AddDays(-1);
var yesterdayRecord = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= yesterday && r.CheckInDate < today)
.FirstAsync();
// 昨天有签到记录,连续天数 +1否则从 1 开始
return yesterdayRecord != null ? yesterdayRecord.ConsecutiveDays + 1 : 1;
}
/// <summary>
/// 根据连续签到天数计算奖励(积分 + 成长值)
/// </summary>
private async Task<(int PointsReward, int GrowthReward)> CalculateRewardsAsync(int consecutiveDays)
{
// 查询签到配置(按 DayNumber 升序)
var configs = await checkInRecordRepository.Context.Queryable<CheckInConfig>()
.Where(c => c.Status == (int)DefaultStatusEnum.Active && !c.IsDeleted)
.OrderBy(c => c.DayNumber)
.ToListAsync();
if (configs.Count == 0)
{
// 无配置时使用默认值
return (DefaultRewardPoints, DefaultRewardPoints);
}
// 找到匹配的奖励档位:取 DayNumber <= 连续天数 的最大档位
var matchedConfig = configs.LastOrDefault(c => c.DayNumber <= consecutiveDays)
?? configs.First();
var totalPoints = matchedConfig.RewardPoints + matchedConfig.BonusPoints;
// 成长值与积分相同(签到同时获得积分和成长值)
return (totalPoints, totalPoints);
}
}