Files
glz cbdee5068a feat: 新增消息Outbox机制、雪花ID配置优化及多项功能完善
1.  新增数据库唯一约束和Message_Outbox表脚本
2.  新增雪花ID、Hangfire存储、MQ重试等配置实体
3.  重构各项目雪花ID生成逻辑,改为从配置读取WorkerId
4.  优化积分服务分页查询、用户背包更新逻辑
5.  新增JWT令牌Redis过期刷新逻辑
6.  完善RabbitMQ死信队列消息头信息
7.  新增可靠MQ消息发布服务和Outbox派发后台服务
8.  替换原有RabbitMQ直接发送为Outbox可靠发布
9.  优化签到服务逻辑,新增重复签到校验和补签卡扣减逻辑
10. 修复自动铺码消费逻辑,新增点阵页预占和释放机制
2026-07-10 10:44:00 +08:00

464 lines
19 KiB
C#
Raw Permalink 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.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);
}
}