1. 新增并完善ResultCode枚举,补充标准HTTP状态码对应的业务状态码 2. 重构BusinessException,新增基于ResultCode的构造函数和ThrowIf扩展方法 3. 替换所有硬编码的HTTP状态码为统一的ResultCode枚举 4. 优化全局异常中间件,根据业务状态码映射对应HTTP状态码并规范化JSON响应 5. 修复OssImageHelper和AutoDotCodeConsumer中的OSS文件处理逻辑 6. 新增用户答题快照实体类 7. 清理废弃的宠物模块迁移脚本
438 lines
17 KiB
C#
438 lines
17 KiB
C#
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 () =>
|
||
{
|
||
// 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 == newGrowthBalance)
|
||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||
.ExecuteCommandAsync();
|
||
|
||
// 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 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;
|
||
|
||
// 更新用户成长值
|
||
var newGrowthBalance = user.GrowthPoints + growthReward;
|
||
|
||
await checkInRecordRepository.Context.Updateable<Users>()
|
||
.SetColumns(u => u.GrowthPoints == newGrowthBalance)
|
||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||
.ExecuteCommandAsync();
|
||
|
||
// 通过积分服务增加积分
|
||
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)
|
||
{
|
||
await checkInRecordRepository.Context.Updateable<UserBag>()
|
||
.SetColumns(b => b.Status == (int)UserBagStatusEnum.Expired)
|
||
.SetColumns(b => b.Quantity == 0)
|
||
.SetColumns(b => b.UpdatedAt == DateTime.Now)
|
||
.Where(b => b.Id == makeUpCard.Id)
|
||
.ExecuteCommandAsync();
|
||
}
|
||
else
|
||
{
|
||
await checkInRecordRepository.Context.Updateable<UserBag>()
|
||
.SetColumns(b => b.Quantity == makeUpCard.Quantity - 1)
|
||
.SetColumns(b => b.UpdatedAt == DateTime.Now)
|
||
.Where(b => b.Id == makeUpCard.Id)
|
||
.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);
|
||
}
|
||
}
|