278 lines
11 KiB
C#
278 lines
11 KiB
C#
|
|
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<Pet>()
|
|||
|
|
.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;
|
|||
|
|
if (lastDate == today)
|
|||
|
|
{
|
|||
|
|
// 今天已签到,连续天数就是今天的值
|
|||
|
|
}
|
|||
|
|
// 如果是昨天,则连续天数保持(今天还没签到)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查询用户余额
|
|||
|
|
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>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|