Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.Service/CheckInService.cs
glz 866549873a refactor(pet): 重构宠物模块实体与接口
1. 将原Pet实体重命名为UserPet,新增PetTemplate模板实体拆分宠物模板与实例数据
2. 调整IPetService泛型参数为IBaseService<UserPet>
3. 补充宠物模板相关字段与查询逻辑,完善创建默认宠物的流程
4. 替换所有Pet实体引用为UserPet,修正相关服务层查询与更新逻辑
2026-06-05 18:10:34 +08:00

427 lines
16 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.CheckIn;
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
using QYZH.InteractiveMagazine.Models.Dto.Pet;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Repository;
namespace QYZH.InteractiveMagazine.Service;
/// <summary>
/// 签到服务实现
/// </summary>
public class CheckInService(
BaseRepository<CheckInRecord> checkInRecordRepository,
IPetService petService,
ICompensationTaskService compensationTaskService,
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("今日已签到,请明天再来", 400);
}
// 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("用户不存在", 404);
}
// 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 () =>
{
// 6a. 创建签到记录
var checkInRecord = new CheckInRecord
{
UserId = userId,
CheckInDate = today,
PointsAwarded = pointsReward,
GrowthPointsAwarded = growthReward,
ConsecutiveDays = consecutiveDays,
Type = "Normal",
Status = "Success"
};
var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync();
checkInRecord.Id = recordId;
// 6b. 更新用户积分余额
var newPointsBalance = user.Points + pointsReward;
var newGrowthBalance = user.GrowthPoints + growthReward;
await checkInRecordRepository.Context.Updateable<Users>()
.SetColumns(u => u.Points == newPointsBalance)
.SetColumns(u => u.GrowthPoints == newGrowthBalance)
.Where(u => u.Id == userId && !u.IsDeleted)
.ExecuteCommandAsync();
// 6c. 创建积分变动记录
var pointsRecord = new PointsRecord
{
UserId = userId,
ChangeAmount = pointsReward,
BalanceAfter = newPointsBalance,
ChangeType = "SignIn",
RelatedId = recordId,
Description = $"签到奖励(连续{consecutiveDays}天)",
Type = "Income",
Status = "Success"
};
await checkInRecordRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync();
// 构建返回结果
result.RecordId = (long)recordId;
result.CheckInDate = today;
result.ConsecutiveDays = consecutiveDays;
result.PointsAwarded = pointsReward;
result.GrowthPointsAwarded = growthReward;
result.PointsBalance = newPointsBalance;
result.GrowthPointsBalance = newGrowthBalance;
result.HasPet = pet != null;
});
// 7. 如果用户有活跃宠物,调用 PetService 喂养(含进化检查),独立事务
if (pet != null && pet.Status == "Active" && growthReward > 0)
{
try
{
var feedResult = await petService.FeedPetAsync(userId, new FeedPetInput
{
PetId = pet.Id,
GrowthPoints = growthReward
});
result.HasEvolved = feedResult.HasEvolved;
result.EvolvedStageName = feedResult.EvolvedStageName;
logger.LogInformation("签到成长值已喂养宠物PetId: {PetId}, 进化: {HasEvolved}",
pet.Id, feedResult.HasEvolved);
}
catch (Exception ex)
{
logger.LogWarning(ex, "签到后喂养宠物失败PetId: {PetId},将创建补偿任务", pet.Id);
await compensationTaskService.CreateTaskAsync(new CreateCompensationTaskInput
{
TaskType = CompensationTaskType.PetFeeding,
BusinessSource = "CheckIn",
BusinessId = result.RecordId.ToString(),
UserId = userId,
Payload = new { PetId = pet.Id, GrowthPoints = growthReward },
ErrorMessage = ex.Message,
ErrorSource = "CheckInService.CheckInAsync → PetService.FeedPetAsync",
MaxRetries = 3
});
}
}
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();
// 最近 30 天签到记录
var thirtyDaysAgo = today.AddDays(-29);
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,
ConsecutiveDays = r.ConsecutiveDays,
PointsAwarded = r.PointsAwarded,
GrowthPointsAwarded = r.GrowthPointsAwarded,
Type = r.Type,
Status = r.Status
})
.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("只能补签过去的日期", 400);
// 检查目标日期是否已有签到记录
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} 已签到,无需补签", 400);
// 查询用户信息
var user = await checkInRecordRepository.Context.Queryable<Users>()
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstAsync();
if (user == null)
throw new BusinessException("用户不存在", 404);
// 查询宠物
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 checkInRecord = new CheckInRecord
{
UserId = userId,
CheckInDate = targetDate,
PointsAwarded = pointsReward,
GrowthPointsAwarded = growthReward,
ConsecutiveDays = 0, // 补签不纳入连续天数
Type = "MakeUp",
Status = "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;
// 更新用户积分和成长值
var newPointsBalance = user.Points + pointsReward;
var newGrowthBalance = user.GrowthPoints + growthReward;
await checkInRecordRepository.Context.Updateable<Users>()
.SetColumns(u => u.Points == newPointsBalance)
.SetColumns(u => u.GrowthPoints == newGrowthBalance)
.Where(u => u.Id == userId && !u.IsDeleted)
.ExecuteCommandAsync();
// 创建积分变动记录
var pointsRecord = new PointsRecord
{
UserId = userId,
ChangeAmount = pointsReward,
BalanceAfter = newPointsBalance,
ChangeType = "MakeUpSign",
RelatedId = recordId,
Description = $"补签奖励({targetDate:yyyy-MM-dd}",
Type = "Income",
Status = "Success",
IsDeleted = false,
CreatedBy = userId.ToString(),
CreatedAt = DateTime.Now,
UpdatedBy = userId.ToString(),
UpdatedAt = DateTime.Now
};
await checkInRecordRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync();
result.RecordId = (long)recordId;
result.CheckInDate = targetDate;
result.ConsecutiveDays = 0;
result.PointsAwarded = pointsReward;
result.GrowthPointsAwarded = growthReward;
result.PointsBalance = newPointsBalance;
result.GrowthPointsBalance = newGrowthBalance;
result.HasPet = pet != null;
});
// 如果有活跃宠物,喂养成长值
if (pet != null && pet.Status == "Active" && growthReward > 0)
{
try
{
var feedResult = await petService.FeedPetAsync(userId, new Models.Dto.Pet.FeedPetInput
{
PetId = pet.Id,
GrowthPoints = growthReward
});
result.HasEvolved = feedResult.HasEvolved;
result.EvolvedStageName = feedResult.EvolvedStageName;
}
catch (Exception ex)
{
logger.LogWarning(ex, "补签后喂养宠物失败PetId: {PetId}", pet.Id);
// 补偿机制:如需可在此创建补偿任务
}
}
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 = 30)
{
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 == "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);
}
}