diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/CardFlipStatusOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/CardFlipStatusOutput.cs new file mode 100644 index 00000000..c1c218e7 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/CardFlipStatusOutput.cs @@ -0,0 +1,88 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip; + +/// +/// 翻牌任务状态输出 +/// +public class CardFlipStatusOutput +{ + /// + /// 本周总翻牌次数 + /// + public int TotalFlips { get; set; } + + /// + /// 剩余免费次数 + /// + public int RemainingFreeFlips { get; set; } + + /// + /// 剩余赠送次数 + /// + public int RemainingBonusFlips { get; set; } + + /// + /// 剩余邀请解锁次数 + /// + public int RemainingInviteFlips { get; set; } + + /// + /// 是否可以翻牌 + /// + public bool CanFlip { get; set; } + + /// + /// 用户的邀请码 + /// + public string? MyInviteCode { get; set; } + + /// + /// 本周邀请人数 + /// + public int InvitedCount { get; set; } + + /// + /// 是否已被邀请(被邀请后不可再提供邀请码) + /// + public bool IsInvited { get; set; } + + /// + /// 翻牌记录 + /// + public List FlipRecords { get; set; } = new(); + + /// + /// 下次可翻牌提示 + /// + public string? NextFlipTip { get; set; } +} + +/// +/// 翻牌记录 +/// +public class CardFlipRecord +{ + /// + /// 翻牌序号(1-10) + /// + public int FlipNumber { get; set; } + + /// + /// 是否已翻 + /// + public bool IsFlipped { get; set; } + + /// + /// 是否中奖 + /// + public bool IsWin { get; set; } + + /// + /// 奖励金额(token数) + /// + public long? RewardAmount { get; set; } + + /// + /// 翻牌类型描述 + /// + public string? FlipTypeDesc { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/FlipCardInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/FlipCardInput.cs new file mode 100644 index 00000000..e974dfd6 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/FlipCardInput.cs @@ -0,0 +1,12 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip; + +/// +/// 翻牌输入 +/// +public class FlipCardInput +{ + /// + /// 翻牌序号(1-10) + /// + public int FlipNumber { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/FlipCardOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/FlipCardOutput.cs new file mode 100644 index 00000000..af6e0167 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/FlipCardOutput.cs @@ -0,0 +1,37 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip; + +/// +/// 翻牌输出 +/// +public class FlipCardOutput +{ + /// + /// 翻牌序号(1-10) + /// + public int FlipNumber { get; set; } + + /// + /// 是否中奖 + /// + public bool IsWin { get; set; } + + /// + /// 奖励金额(token数) + /// + public long? RewardAmount { get; set; } + + /// + /// 奖励描述 + /// + public string? RewardDesc { get; set; } + + /// + /// 是否显示翻倍包提示(第9次中奖后显示) + /// + public bool ShowDoubleRewardTip { get; set; } + + /// + /// 剩余可翻次数 + /// + public int RemainingFlips { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/InviteCodeOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/InviteCodeOutput.cs new file mode 100644 index 00000000..2531498a --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/InviteCodeOutput.cs @@ -0,0 +1,48 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip; + +/// +/// 邀请码信息输出 +/// +public class InviteCodeOutput +{ + /// + /// 我的邀请码 + /// + public string? MyInviteCode { get; set; } + + /// + /// 本周邀请人数 + /// + public int InvitedCount { get; set; } + + /// + /// 是否已被邀请 + /// + public bool IsInvited { get; set; } + + /// + /// 邀请历史记录 + /// + public List InvitationHistory { get; set; } = new(); +} + +/// +/// 邀请历史记录项 +/// +public class InvitationHistoryItem +{ + /// + /// 被邀请人昵称(脱敏) + /// + public string InvitedUserName { get; set; } = string.Empty; + + /// + /// 邀请时间 + /// + public DateTime InvitationTime { get; set; } + + /// + /// 本周所在 + /// + public string WeekDescription { get; set; } = string.Empty; +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/UseInviteCodeInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/UseInviteCodeInput.cs new file mode 100644 index 00000000..e02cc6ab --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/CardFlip/UseInviteCodeInput.cs @@ -0,0 +1,12 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip; + +/// +/// 使用邀请码输入 +/// +public class UseInviteCodeInput +{ + /// + /// 邀请码 + /// + public string InviteCode { get; set; } = string.Empty; +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/ICardFlipService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/ICardFlipService.cs new file mode 100644 index 00000000..9dbc2544 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/ICardFlipService.cs @@ -0,0 +1,41 @@ +using Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip; + +namespace Yi.Framework.AiHub.Application.Contracts.IServices; + +/// +/// 翻牌服务接口 +/// +public interface ICardFlipService +{ + /// + /// 获取本周翻牌任务状态 + /// + /// + Task GetWeeklyTaskStatusAsync(); + + /// + /// 翻牌 + /// + /// 翻牌输入 + /// + Task FlipCardAsync(FlipCardInput input); + + /// + /// 使用邀请码解锁翻牌次数 + /// + /// 邀请码输入 + /// + Task UseInviteCodeAsync(UseInviteCodeInput input); + + /// + /// 获取我的邀请码信息 + /// + /// + Task GetMyInviteCodeAsync(); + + /// + /// 生成我的邀请码(如果没有) + /// + /// + Task GenerateMyInviteCodeAsync(); +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/CardFlipService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/CardFlipService.cs new file mode 100644 index 00000000..56f0fc1d --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/CardFlipService.cs @@ -0,0 +1,581 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; +using SqlSugar; +using Volo.Abp.Application.Services; +using Volo.Abp.Users; +using Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip; +using Yi.Framework.AiHub.Application.Contracts.IServices; +using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.Extensions; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace Yi.Framework.AiHub.Application.Services; + +/// +/// 翻牌服务 +/// +[Authorize] +public class CardFlipService : ApplicationService, ICardFlipService +{ + private readonly ISqlSugarRepository _cardFlipTaskRepository; + private readonly ISqlSugarRepository _inviteCodeRepository; + private readonly ISqlSugarRepository _invitationRecordRepository; + private readonly ISqlSugarRepository _premiumPackageRepository; + private readonly ILogger _logger; + + // 翻牌规则配置 + private const int MAX_FREE_FLIPS = 5; // 免费翻牌次数 + private const int MAX_BONUS_FLIPS = 3; // 赠送翻牌次数 + private const int MAX_INVITE_FLIPS = 2; // 邀请解锁翻牌次数 + private const int TOTAL_MAX_FLIPS = 10; // 总最大翻牌次数 + + private const int NINTH_FLIP = 9; // 第9次翻牌 + private const int TENTH_FLIP = 10; // 第10次翻牌 + + private const long NINTH_MIN_REWARD = 3000000; // 第9次最小奖励 300w + private const long NINTH_MAX_REWARD = 7000000; // 第9次最大奖励 700w + private const long TENTH_MIN_REWARD = 8000000; // 第10次最小奖励 800w + private const long TENTH_MAX_REWARD = 12000000; // 第10次最大奖励 1200w + + public CardFlipService( + ISqlSugarRepository cardFlipTaskRepository, + ISqlSugarRepository inviteCodeRepository, + ISqlSugarRepository invitationRecordRepository, + ISqlSugarRepository premiumPackageRepository, + ILogger logger) + { + _cardFlipTaskRepository = cardFlipTaskRepository; + _inviteCodeRepository = inviteCodeRepository; + _invitationRecordRepository = invitationRecordRepository; + _premiumPackageRepository = premiumPackageRepository; + _logger = logger; + } + + /// + /// 获取本周翻牌任务状态 + /// + public async Task GetWeeklyTaskStatusAsync() + { + var userId = CurrentUser.GetId(); + var weekStart = GetWeekStartDate(DateTime.Now); + + // 获取或创建本周任务 + var task = await GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: false); + + // 获取邀请码信息 + var inviteCode = await _inviteCodeRepository._DbQueryable + .Where(x => x.UserId == userId) + .FirstAsync(); + + // 统计本周邀请人数 + var invitedCount = await _invitationRecordRepository._DbQueryable + .Where(x => x.InviterId == userId) + .Where(x => x.InvitationTime >= weekStart) + .CountAsync(); + + // 检查用户是否已被邀请 + var isInvited = inviteCode?.IsUserInvited ?? false; + + var output = new CardFlipStatusOutput + { + TotalFlips = task?.TotalFlips ?? 0, + RemainingFreeFlips = MAX_FREE_FLIPS - (task?.FreeFlipsUsed ?? 0), + RemainingBonusFlips = MAX_BONUS_FLIPS - (task?.BonusFlipsUsed ?? 0), + RemainingInviteFlips = MAX_INVITE_FLIPS - (task?.InviteFlipsUsed ?? 0), + CanFlip = CanFlipCard(task), + MyInviteCode = inviteCode?.Code, + InvitedCount = invitedCount, + IsInvited = isInvited, + FlipRecords = BuildFlipRecords(task) + }; + + // 生成提示信息 + output.NextFlipTip = GenerateNextFlipTip(output); + + return output; + } + + /// + /// 翻牌 + /// + public async Task FlipCardAsync(FlipCardInput input) + { + var userId = CurrentUser.GetId(); + var weekStart = GetWeekStartDate(DateTime.Now); + + if (input.FlipNumber < 1 || input.FlipNumber > TOTAL_MAX_FLIPS) + { + throw new UserFriendlyException($"翻牌序号必须在1-{TOTAL_MAX_FLIPS}之间"); + } + + // 获取或创建本周任务 + var task = await GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: true); + + // 验证翻牌次数 + if (task.TotalFlips >= TOTAL_MAX_FLIPS) + { + throw new UserFriendlyException("本周翻牌次数已用完,请下周再来!"); + } + + // 验证顺序翻牌 + if (input.FlipNumber != task.TotalFlips + 1) + { + throw new UserFriendlyException("请按顺序翻牌!"); + } + + // 判断翻牌类型 + var flipType = DetermineFlipType(task); + + // 验证是否有足够的次数 + if (!CanUseFlipType(task, flipType)) + { + throw new UserFriendlyException(GetFlipTypeErrorMessage(flipType)); + } + + // 翻牌逻辑 + var output = new FlipCardOutput + { + FlipNumber = input.FlipNumber, + IsWin = false, + ShowDoubleRewardTip = false + }; + + // 前8次固定失败 + if (input.FlipNumber <= 8) + { + output.IsWin = false; + output.RewardDesc = "很遗憾,未中奖"; + } + // 第9次中奖 + else if (input.FlipNumber == NINTH_FLIP) + { + var rewardAmount = GenerateRandomReward(NINTH_MIN_REWARD, NINTH_MAX_REWARD); + output.IsWin = true; + output.RewardAmount = rewardAmount; + output.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000}w tokens!"; + output.ShowDoubleRewardTip = true; // 显示翻倍包提示 + + // 发放奖励 + await GrantRewardAsync(userId, rewardAmount, $"翻牌活动第{input.FlipNumber}次中奖"); + + // 记录奖励 + task.SetNinthReward(rewardAmount); + } + // 第10次中奖(翻倍) + else if (input.FlipNumber == TENTH_FLIP) + { + var rewardAmount = GenerateRandomReward(TENTH_MIN_REWARD, TENTH_MAX_REWARD); + output.IsWin = true; + output.RewardAmount = rewardAmount; + output.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000}w tokens(翻倍奖励)!"; + + // 发放奖励 + await GrantRewardAsync(userId, rewardAmount, $"翻牌活动第{input.FlipNumber}次中奖(翻倍)"); + + // 记录奖励 + task.SetTenthReward(rewardAmount); + } + + // 更新翻牌次数 + task.IncrementFlip(flipType); + await _cardFlipTaskRepository.UpdateAsync(task); + + // 计算剩余次数 + output.RemainingFlips = TOTAL_MAX_FLIPS - task.TotalFlips; + + _logger.LogInformation($"用户 {userId} 完成第 {input.FlipNumber} 次翻牌,中奖:{output.IsWin}"); + + return output; + } + + /// + /// 使用邀请码解锁翻牌次数 + /// + public async Task UseInviteCodeAsync(UseInviteCodeInput input) + { + var userId = CurrentUser.GetId(); + var weekStart = GetWeekStartDate(DateTime.Now); + + if (string.IsNullOrWhiteSpace(input.InviteCode)) + { + throw new UserFriendlyException("邀请码不能为空"); + } + + // 查找邀请码 + var inviteCode = await _inviteCodeRepository._DbQueryable + .Where(x => x.Code == input.InviteCode) + .FirstAsync(); + + if (inviteCode == null) + { + throw new UserFriendlyException("邀请码不存在"); + } + + // 验证不能使用自己的邀请码 + if (inviteCode.UserId == userId) + { + throw new UserFriendlyException("不能使用自己的邀请码"); + } + + // 验证邀请码是否已被使用 + if (inviteCode.IsUsed) + { + throw new UserFriendlyException("该邀请码已被使用"); + } + + // 验证邀请码拥有者是否已被邀请 + if (inviteCode.IsUserInvited) + { + throw new UserFriendlyException("该用户已被邀请,邀请码无效"); + } + + // 检查当前用户是否已被邀请 + var myInviteCode = await _inviteCodeRepository._DbQueryable + .Where(x => x.UserId == userId) + .FirstAsync(); + + if (myInviteCode?.IsUserInvited == true) + { + throw new UserFriendlyException("您已使用过邀请码,无法重复使用"); + } + + // 获取本周任务 + var task = await GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: true); + + // 验证是否已经使用了所有邀请解锁次数 + if (task.InviteFlipsUsed >= MAX_INVITE_FLIPS) + { + throw new UserFriendlyException("本周邀请解锁次数已用完"); + } + + // 标记邀请码为已使用 + inviteCode.MarkAsUsed(userId); + await _inviteCodeRepository.UpdateAsync(inviteCode); + + // 标记当前用户已被邀请 + if (myInviteCode == null) + { + myInviteCode = new InviteCodeAggregateRoot(userId, GenerateInviteCode()); + myInviteCode.MarkUserAsInvited(); + await _inviteCodeRepository.InsertAsync(myInviteCode); + } + else + { + myInviteCode.MarkUserAsInvited(); + await _inviteCodeRepository.UpdateAsync(myInviteCode); + } + + // 创建邀请记录 + var invitationRecord = new InvitationRecordAggregateRoot( + inviteCode.UserId, + userId, + input.InviteCode); + await _invitationRecordRepository.InsertAsync(invitationRecord); + + _logger.LogInformation($"用户 {userId} 使用邀请码 {input.InviteCode} 成功"); + } + + /// + /// 获取我的邀请码信息 + /// + public async Task GetMyInviteCodeAsync() + { + var userId = CurrentUser.GetId(); + var weekStart = GetWeekStartDate(DateTime.Now); + + // 获取我的邀请码 + var inviteCode = await _inviteCodeRepository._DbQueryable + .Where(x => x.UserId == userId) + .FirstAsync(); + + // 统计本周邀请人数 + var invitedCount = await _invitationRecordRepository._DbQueryable + .Where(x => x.InviterId == userId) + .Where(x => x.InvitationTime >= weekStart) + .CountAsync(); + + // 获取邀请历史 + var invitationHistory = await _invitationRecordRepository._DbQueryable + .Where(x => x.InviterId == userId) + .OrderBy(x => x.InvitationTime, OrderByType.Desc) + .Take(10) + .Select(x => new InvitationHistoryItem + { + InvitedUserName = "用户***", // 脱敏处理 + InvitationTime = x.InvitationTime, + WeekDescription = GetWeekDescription(x.InvitationTime) + }) + .ToListAsync(); + + return new InviteCodeOutput + { + MyInviteCode = inviteCode?.Code, + InvitedCount = invitedCount, + IsInvited = inviteCode?.IsUserInvited ?? false, + InvitationHistory = invitationHistory + }; + } + + /// + /// 生成我的邀请码 + /// + public async Task GenerateMyInviteCodeAsync() + { + var userId = CurrentUser.GetId(); + + // 检查是否已有邀请码 + var existingCode = await _inviteCodeRepository._DbQueryable + .Where(x => x.UserId == userId) + .FirstAsync(); + + if (existingCode != null) + { + return existingCode.Code; + } + + // 生成新邀请码 + var code = GenerateInviteCode(); + var inviteCode = new InviteCodeAggregateRoot(userId, code); + await _inviteCodeRepository.InsertAsync(inviteCode); + + _logger.LogInformation($"用户 {userId} 生成邀请码 {code}"); + + return code; + } + + #region 私有辅助方法 + + /// + /// 获取或创建本周任务 + /// + private async Task GetOrCreateWeeklyTaskAsync( + Guid userId, + DateTime weekStart, + bool createIfNotExists) + { + var task = await _cardFlipTaskRepository._DbQueryable + .Where(x => x.UserId == userId && x.WeekStartDate == weekStart) + .FirstAsync(); + + if (task == null && createIfNotExists) + { + task = new CardFlipTaskAggregateRoot(userId, weekStart); + await _cardFlipTaskRepository.InsertAsync(task); + } + + return task; + } + + /// + /// 获取本周开始日期(周一) + /// + private DateTime GetWeekStartDate(DateTime date) + { + var dayOfWeek = (int)date.DayOfWeek; + // 将周日(0)转换为7 + if (dayOfWeek == 0) dayOfWeek = 7; + + // 计算本周一的日期 + var monday = date.Date.AddDays(-(dayOfWeek - 1)); + return monday; + } + + /// + /// 判断是否可以翻牌 + /// + private bool CanFlipCard(CardFlipTaskAggregateRoot? task) + { + if (task == null) return true; // 没有任务记录,可以开始翻牌 + + return task.TotalFlips < TOTAL_MAX_FLIPS; + } + + /// + /// 判断翻牌类型 + /// + private FlipType DetermineFlipType(CardFlipTaskAggregateRoot task) + { + if (task.FreeFlipsUsed < MAX_FREE_FLIPS) + { + return FlipType.Free; + } + else if (task.BonusFlipsUsed < MAX_BONUS_FLIPS) + { + return FlipType.Bonus; + } + else + { + return FlipType.Invite; + } + } + + /// + /// 判断是否可以使用该翻牌类型 + /// + private bool CanUseFlipType(CardFlipTaskAggregateRoot task, FlipType flipType) + { + return flipType switch + { + FlipType.Free => task.FreeFlipsUsed < MAX_FREE_FLIPS, + FlipType.Bonus => task.BonusFlipsUsed < MAX_BONUS_FLIPS, + FlipType.Invite => task.InviteFlipsUsed < MAX_INVITE_FLIPS, + _ => false + }; + } + + /// + /// 获取翻牌类型错误提示 + /// + private string GetFlipTypeErrorMessage(FlipType flipType) + { + return flipType switch + { + FlipType.Free => "免费翻牌次数已用完", + FlipType.Bonus => "赠送翻牌次数已用完", + FlipType.Invite => "需要使用邀请码解锁更多次数", + _ => "无法翻牌" + }; + } + + /// + /// 构建翻牌记录列表 + /// + private List BuildFlipRecords(CardFlipTaskAggregateRoot? task) + { + var records = new List(); + + for (int i = 1; i <= TOTAL_MAX_FLIPS; i++) + { + var record = new CardFlipRecord + { + FlipNumber = i, + IsFlipped = task != null && i <= task.TotalFlips, + IsWin = false, + FlipTypeDesc = GetFlipTypeDesc(i) + }; + + // 设置中奖信息 + if (task != null && i <= task.TotalFlips) + { + if (i == NINTH_FLIP && task.HasNinthReward) + { + record.IsWin = true; + record.RewardAmount = task.NinthRewardAmount; + } + else if (i == TENTH_FLIP && task.HasTenthReward) + { + record.IsWin = true; + record.RewardAmount = task.TenthRewardAmount; + } + } + + records.Add(record); + } + + return records; + } + + /// + /// 获取翻牌类型描述 + /// + private string GetFlipTypeDesc(int flipNumber) + { + if (flipNumber <= MAX_FREE_FLIPS) + { + return "免费"; + } + else if (flipNumber <= MAX_FREE_FLIPS + MAX_BONUS_FLIPS) + { + return "赠送"; + } + else + { + return "邀请解锁"; + } + } + + /// + /// 生成下次翻牌提示 + /// + private string GenerateNextFlipTip(CardFlipStatusOutput status) + { + if (status.TotalFlips >= TOTAL_MAX_FLIPS) + { + return "本周翻牌次数已用完,请下周再来!"; + } + + if (status.RemainingFreeFlips > 0) + { + return $"还有{status.RemainingFreeFlips}次免费翻牌机会"; + } + else if (status.RemainingBonusFlips > 0) + { + return $"还有{status.RemainingBonusFlips}次赠送翻牌机会"; + } + else if (status.RemainingInviteFlips > 0) + { + if (status.TotalFlips == 8) + { + return "再邀请一个人,马上中奖!"; + } + return $"使用邀请码可解锁{status.RemainingInviteFlips}次翻牌"; + } + + return "继续加油!"; + } + + /// + /// 生成随机奖励金额 + /// + private long GenerateRandomReward(long min, long max) + { + var random = new Random(); + return (long)(random.NextDouble() * (max - min) + min); + } + + /// + /// 发放奖励 + /// + private async Task GrantRewardAsync(Guid userId, long amount, string description) + { + var premiumPackage = new PremiumPackageAggregateRoot(userId, amount, description) + { + PurchaseAmount = 0, // 奖励不需要付费 + Remark = $"翻牌活动奖励:{amount / 10000}w tokens" + }; + + await _premiumPackageRepository.InsertAsync(premiumPackage); + + _logger.LogInformation($"用户 {userId} 获得翻牌奖励 {amount / 10000}w tokens"); + } + + /// + /// 生成邀请码 + /// + private string GenerateInviteCode() + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + var random = new Random(); + var code = new char[8]; + + for (int i = 0; i < code.Length; i++) + { + code[i] = chars[random.Next(chars.Length)]; + } + + return new string(code); + } + + /// + /// 获取周描述 + /// + private string GetWeekDescription(DateTime date) + { + var weekStart = GetWeekStartDate(date); + var weekEnd = weekStart.AddDays(6); + + return $"{weekStart:MM-dd} 至 {weekEnd:MM-dd}"; + } + + #endregion +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/CardFlipTaskAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/CardFlipTaskAggregateRoot.cs new file mode 100644 index 00000000..4b795299 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/CardFlipTaskAggregateRoot.cs @@ -0,0 +1,160 @@ +using SqlSugar; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Yi.Framework.AiHub.Domain.Entities; + +/// +/// 翻牌任务记录 +/// +[SugarTable("Ai_CardFlipTask")] +[SugarIndex($"index_{nameof(UserId)}_{nameof(WeekStartDate)}", + nameof(UserId), OrderByType.Asc, + nameof(WeekStartDate), OrderByType.Desc)] +public class CardFlipTaskAggregateRoot : FullAuditedAggregateRoot +{ + public CardFlipTaskAggregateRoot() + { + } + + public CardFlipTaskAggregateRoot(Guid userId, DateTime weekStartDate) + { + UserId = userId; + WeekStartDate = weekStartDate.Date; // 确保只存储日期部分 + TotalFlips = 0; + FreeFlipsUsed = 0; + BonusFlipsUsed = 0; + InviteFlipsUsed = 0; + IsFirstFlipDone = false; + HasNinthReward = false; + HasTenthReward = false; + } + + /// + /// 用户ID + /// + public Guid UserId { get; set; } + + /// + /// 本周开始日期(每周一) + /// + public DateTime WeekStartDate { get; set; } + + /// + /// 总共已翻牌次数 + /// + public int TotalFlips { get; set; } + + /// + /// 已使用的免费次数(最多5次) + /// + public int FreeFlipsUsed { get; set; } + + /// + /// 已使用的赠送次数(最多3次) + /// + public int BonusFlipsUsed { get; set; } + + /// + /// 已使用的邀请解锁次数(最多2次) + /// + public int InviteFlipsUsed { get; set; } + + /// + /// 是否已完成首次翻牌(用于判断是否创建任务) + /// + public bool IsFirstFlipDone { get; set; } + + /// + /// 是否已获得第9次奖励 + /// + public bool HasNinthReward { get; set; } + + /// + /// 第9次奖励金额(300-700w) + /// + public long? NinthRewardAmount { get; set; } + + /// + /// 是否已获得第10次奖励 + /// + public bool HasTenthReward { get; set; } + + /// + /// 第10次奖励金额(800-1200w) + /// + public long? TenthRewardAmount { get; set; } + + /// + /// 备注信息 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string? Remark { get; set; } + + /// + /// 增加翻牌次数 + /// + /// 翻牌类型 + public void IncrementFlip(FlipType flipType) + { + TotalFlips++; + + switch (flipType) + { + case FlipType.Free: + FreeFlipsUsed++; + break; + case FlipType.Bonus: + BonusFlipsUsed++; + break; + case FlipType.Invite: + InviteFlipsUsed++; + break; + } + + if (!IsFirstFlipDone) + { + IsFirstFlipDone = true; + } + } + + /// + /// 记录第9次奖励 + /// + /// 奖励金额 + public void SetNinthReward(long amount) + { + HasNinthReward = true; + NinthRewardAmount = amount; + } + + /// + /// 记录第10次奖励 + /// + /// 奖励金额 + public void SetTenthReward(long amount) + { + HasTenthReward = true; + TenthRewardAmount = amount; + } +} + +/// +/// 翻牌类型枚举 +/// +public enum FlipType +{ + /// + /// 免费翻牌(1-5次) + /// + Free = 0, + + /// + /// 赠送翻牌(6-8次) + /// + Bonus = 1, + + /// + /// 邀请解锁翻牌(9-10次) + /// + Invite = 2 +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/InvitationRecordAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/InvitationRecordAggregateRoot.cs new file mode 100644 index 00000000..6e1bea35 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/InvitationRecordAggregateRoot.cs @@ -0,0 +1,76 @@ +using SqlSugar; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Yi.Framework.AiHub.Domain.Entities; + +/// +/// 邀请记录 +/// +[SugarTable("Ai_InvitationRecord")] +[SugarIndex($"index_{nameof(InviterId)}_{nameof(InvitedUserId)}", + nameof(InviterId), OrderByType.Asc, + nameof(InvitedUserId), OrderByType.Asc)] +[SugarIndex($"index_{nameof(InvitedUserId)}", nameof(InvitedUserId), OrderByType.Asc)] +public class InvitationRecordAggregateRoot : FullAuditedAggregateRoot +{ + public InvitationRecordAggregateRoot() + { + } + + public InvitationRecordAggregateRoot(Guid inviterId, Guid invitedUserId, string inviteCode) + { + InviterId = inviterId; + InvitedUserId = invitedUserId; + InviteCode = inviteCode; + InvitationTime = DateTime.Now; + Status = InvitationStatus.Valid; + } + + /// + /// 邀请人ID + /// + public Guid InviterId { get; set; } + + /// + /// 被邀请人ID + /// + public Guid InvitedUserId { get; set; } + + /// + /// 使用的邀请码 + /// + [SugarColumn(Length = 50)] + public string InviteCode { get; set; } = string.Empty; + + /// + /// 邀请时间 + /// + public DateTime InvitationTime { get; set; } + + /// + /// 邀请状态(0=有效,1=已撤销) + /// + public InvitationStatus Status { get; set; } + + /// + /// 备注信息 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string? Remark { get; set; } +} + +/// +/// 邀请状态枚举 +/// +public enum InvitationStatus +{ + /// + /// 有效 + /// + Valid = 0, + + /// + /// 已撤销 + /// + Revoked = 1 +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/InviteCodeAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/InviteCodeAggregateRoot.cs new file mode 100644 index 00000000..de569b49 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/InviteCodeAggregateRoot.cs @@ -0,0 +1,88 @@ +using SqlSugar; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Yi.Framework.AiHub.Domain.Entities; + +/// +/// 用户邀请码 +/// +[SugarTable("Ai_InviteCode")] +[SugarIndex($"index_{nameof(UserId)}", nameof(UserId), OrderByType.Asc, true)] +[SugarIndex($"index_{nameof(Code)}", nameof(Code), OrderByType.Asc, true)] +public class InviteCodeAggregateRoot : FullAuditedAggregateRoot +{ + public InviteCodeAggregateRoot() + { + } + + public InviteCodeAggregateRoot(Guid userId, string code) + { + UserId = userId; + Code = code; + IsUsed = false; + IsUserInvited = false; + UsedCount = 0; + } + + /// + /// 用户ID(邀请码拥有者) + /// + public Guid UserId { get; set; } + + /// + /// 邀请码(唯一) + /// + [SugarColumn(Length = 50)] + public string Code { get; set; } = string.Empty; + + /// + /// 是否已被使用(一个邀请码只能被使用一次) + /// + public bool IsUsed { get; set; } + + /// + /// 邀请码拥有者是否已被他人邀请(被邀请后不可再提供邀请码) + /// + public bool IsUserInvited { get; set; } + + /// + /// 被使用次数(统计用) + /// + public int UsedCount { get; set; } + + /// + /// 使用时间 + /// + public DateTime? UsedTime { get; set; } + + /// + /// 使用人ID + /// + public Guid? UsedByUserId { get; set; } + + /// + /// 备注信息 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string? Remark { get; set; } + + /// + /// 标记邀请码已被使用 + /// + /// 使用者ID + public void MarkAsUsed(Guid usedByUserId) + { + IsUsed = true; + UsedTime = DateTime.Now; + UsedByUserId = usedByUserId; + UsedCount++; + } + + /// + /// 标记用户已被邀请 + /// + public void MarkUserAsInvited() + { + IsUserInvited = true; + } +} diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index 9e08520e..b53f7c11 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -29,6 +29,7 @@ using Volo.Abp.Swashbuckle; using Yi.Abp.Application; using Yi.Abp.SqlsugarCore; using Yi.Framework.AiHub.Application; +using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AspNetCore; using Yi.Framework.AspNetCore.Authentication.OAuth; using Yi.Framework.AspNetCore.Authentication.OAuth.Gitee; @@ -46,6 +47,7 @@ using Yi.Framework.Rbac.Application; using Yi.Framework.Rbac.Domain.Authorization; using Yi.Framework.Rbac.Domain.Shared.Consts; using Yi.Framework.Rbac.Domain.Shared.Options; +using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.Stock.Application; using Yi.Framework.TenantManagement.Application; @@ -350,6 +352,9 @@ namespace Yi.Abp.Web var app = context.GetApplicationBuilder(); app.UseRouting(); + // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); //跨域 app.UseCors(DefaultCorsPolicyName); diff --git a/Yi.Ai.Vue3/.eslintrc-auto-import.json b/Yi.Ai.Vue3/.eslintrc-auto-import.json index af1083b7..313e6711 100644 --- a/Yi.Ai.Vue3/.eslintrc-auto-import.json +++ b/Yi.Ai.Vue3/.eslintrc-auto-import.json @@ -5,6 +5,8 @@ "ComputedRef": true, "DirectiveBinding": true, "EffectScope": true, + "ElMessage": true, + "ElMessageBox": true, "ExtractDefaultPropTypes": true, "ExtractPropTypes": true, "ExtractPublicPropTypes": true, diff --git a/Yi.Ai.Vue3/src/api/cardFlip/index.ts b/Yi.Ai.Vue3/src/api/cardFlip/index.ts new file mode 100644 index 00000000..570fb730 --- /dev/null +++ b/Yi.Ai.Vue3/src/api/cardFlip/index.ts @@ -0,0 +1,33 @@ +import { get, post } from '@/utils/request'; +import type { + CardFlipStatusOutput, + FlipCardInput, + FlipCardOutput, + UseInviteCodeInput, + InviteCodeOutput +} from './types'; + +// 获取本周翻牌任务状态 +export function getWeeklyTaskStatus() { + return get('/card-flip/weekly-task-status').json(); +} + +// 翻牌 +export function flipCard(data: FlipCardInput) { + return post('/card-flip/flip-card', data).json(); +} + +// 使用邀请码解锁翻牌次数 +export function useInviteCode(data: UseInviteCodeInput) { + return post('/card-flip/use-invite-code', data).json(); +} + +// 获取我的邀请码信息 +export function getMyInviteCode() { + return get('/card-flip/my-invite-code').json(); +} + +// 生成我的邀请码(如果没有) +export function generateMyInviteCode() { + return post('/card-flip/generate-my-invite-code').json(); +} diff --git a/Yi.Ai.Vue3/src/api/cardFlip/types.ts b/Yi.Ai.Vue3/src/api/cardFlip/types.ts new file mode 100644 index 00000000..91d452bb --- /dev/null +++ b/Yi.Ai.Vue3/src/api/cardFlip/types.ts @@ -0,0 +1,57 @@ +// 翻牌任务状态输出 +export interface CardFlipStatusOutput { + totalFlips: number; // 本周总翻牌次数 + remainingFreeFlips: number; // 剩余免费次数 + remainingBonusFlips: number; // 剩余赠送次数 + remainingInviteFlips: number; // 剩余邀请解锁次数 + canFlip: boolean; // 是否可以翻牌 + myInviteCode?: string; // 用户的邀请码 + invitedCount: number; // 本周邀请人数 + isInvited: boolean; // 是否已被邀请 + flipRecords: CardFlipRecord[]; // 翻牌记录 + nextFlipTip?: string; // 下次可翻牌提示 +} + +// 翻牌记录 +export interface CardFlipRecord { + flipNumber: number; // 翻牌序号(1-10) + isFlipped: boolean; // 是否已翻 + isWin: boolean; // 是否中奖 + rewardAmount?: number; // 奖励金额(token数) + flipTypeDesc?: string; // 翻牌类型描述 +} + +// 翻牌输入 +export interface FlipCardInput { + flipNumber: number; // 翻牌序号(1-10) +} + +// 翻牌输出 +export interface FlipCardOutput { + flipNumber: number; // 翻牌序号(1-10) + isWin: boolean; // 是否中奖 + rewardAmount?: number; // 奖励金额(token数) + rewardDesc?: string; // 奖励描述 + showDoubleRewardTip: boolean; // 是否显示翻倍包提示 + remainingFlips: number; // 剩余可翻次数 +} + +// 使用邀请码输入 +export interface UseInviteCodeInput { + inviteCode: string; // 邀请码 +} + +// 邀请码信息输出 +export interface InviteCodeOutput { + myInviteCode?: string; // 我的邀请码 + invitedCount: number; // 本周邀请人数 + isInvited: boolean; // 是否已被邀请 + invitationHistory: InvitationHistoryItem[]; // 邀请历史记录 +} + +// 邀请历史记录项 +export interface InvitationHistoryItem { + invitedUserName: string; // 被邀请人昵称(脱敏) + invitationTime: string; // 邀请时间 + weekDescription: string; // 本周所在 +} diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue new file mode 100644 index 00000000..28fb0f38 --- /dev/null +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/CardFlipActivity.vue @@ -0,0 +1,1028 @@ + + + + + diff --git a/Yi.Ai.Vue3/src/layouts/components/Header/components/Avatar.vue b/Yi.Ai.Vue3/src/layouts/components/Header/components/Avatar.vue index c4589079..7d76c2f0 100644 --- a/Yi.Ai.Vue3/src/layouts/components/Header/components/Avatar.vue +++ b/Yi.Ai.Vue3/src/layouts/components/Header/components/Avatar.vue @@ -69,7 +69,8 @@ const navItems = [ { name: 'rechargeLog', label: '充值记录', icon: 'Document' }, { name: 'usageStatistics', label: '用量统计', icon: 'Histogram' }, { name: 'premiumService', label: '尊享服务', icon: 'ColdDrink' }, - { name: 'dailyTask', label: '每日任务', icon: 'Trophy' } + { name: 'dailyTask', label: '每日任务', icon: 'Trophy' }, + { name: 'cardFlip', label: '翻牌活动', icon: 'Present' } // { name: 'usageStatistics2', label: '用量统计2', icon: 'Histogram' }, ]; function openDialog() { @@ -349,6 +350,9 @@ function onProductPackage() { + diff --git a/Yi.Ai.Vue3/types/components.d.ts b/Yi.Ai.Vue3/types/components.d.ts index 702505a4..64cfc0d4 100644 --- a/Yi.Ai.Vue3/types/components.d.ts +++ b/Yi.Ai.Vue3/types/components.d.ts @@ -10,6 +10,7 @@ declare module 'vue' { export interface GlobalComponents { AccountPassword: typeof import('./../src/components/LoginDialog/components/FormLogin/AccountPassword.vue')['default'] APIKeyManagement: typeof import('./../src/components/userPersonalCenter/components/APIKeyManagement.vue')['default'] + CardFlipActivity: typeof import('./../src/components/userPersonalCenter/components/CardFlipActivity.vue')['default'] DailyTask: typeof import('./../src/components/userPersonalCenter/components/DailyTask.vue')['default'] DeepThinking: typeof import('./../src/components/DeepThinking/index.vue')['default'] ElAlert: typeof import('element-plus/es')['ElAlert'] @@ -19,6 +20,7 @@ declare module 'vue' { ElCard: typeof import('element-plus/es')['ElCard'] ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] + ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition'] ElContainer: typeof import('element-plus/es')['ElContainer'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDivider: typeof import('element-plus/es')['ElDivider'] diff --git a/Yi.Ai.Vue3/types/import_meta.d.ts b/Yi.Ai.Vue3/types/import_meta.d.ts index d8a60d41..b3e9d275 100644 --- a/Yi.Ai.Vue3/types/import_meta.d.ts +++ b/Yi.Ai.Vue3/types/import_meta.d.ts @@ -6,7 +6,6 @@ interface ImportMetaEnv { readonly VITE_WEB_ENV: string; readonly VITE_WEB_BASE_API: string; readonly VITE_API_URL: string; - readonly VITE_BUILD_COMPRESS: string; readonly VITE_SSO_SEVER_URL: string; readonly VITE_APP_VERSION: string; }