diff --git a/.gitignore b/.gitignore index 8ed69463..7bd589c0 100644 --- a/.gitignore +++ b/.gitignore @@ -265,6 +265,7 @@ src/Acme.BookStore.Blazor.Server.Tiered/Logs/* **/wwwroot/libs/* public dist +dist - 副本 .vscode /Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.Development.json /Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.Production.json diff --git a/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs b/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs index 647ca0e8..65c83f13 100644 --- a/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs +++ b/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs @@ -60,8 +60,8 @@ namespace Yi.Framework.SqlSugarCore.Abstractions public bool EnabledSaasMultiTenancy { get; set; } = false; /// - /// 并发乐观锁异常,否则不处理 + /// 是否开启更新并发乐观锁 /// - public bool EnabledConcurrencyException { get; set; } = true; + public bool EnabledConcurrencyException { get;set; } = false; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs b/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs index 1cc1ea61..786254ec 100644 --- a/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs +++ b/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs @@ -1,12 +1,7 @@ -using System.Linq; using System.Linq.Expressions; -using System.Text; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Nito.AsyncEx; using SqlSugar; -using Volo.Abp; -using Volo.Abp.Auditing; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; @@ -17,18 +12,17 @@ using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.SqlSugarCore.Repositories { - public class SqlSugarRepository : ISqlSugarRepository, IRepository - where TEntity : class, IEntity, new() + public class SqlSugarRepository : ISqlSugarRepository, IRepository where TEntity : class, IEntity, new() { public ISqlSugarClient _Db => AsyncContext.Run(async () => await GetDbContextAsync()); public ISugarQueryable _DbQueryable => _Db.Queryable(); private readonly ISugarDbContextProvider _dbContextProvider; - + public IAbpLazyServiceProvider LazyServiceProvider { get; set; } + protected DbConnOptions? Options => LazyServiceProvider?.LazyGetService>().Value; - /// /// 异步查询执行器 /// @@ -64,26 +58,22 @@ namespace Yi.Framework.SqlSugarCore.Repositories #region Abp模块 - public virtual async Task FindAsync(Expression> predicate, - bool includeDetails = true, CancellationToken cancellationToken = default) + public virtual async Task FindAsync(Expression> predicate, bool includeDetails = true, CancellationToken cancellationToken = default) { return await GetFirstAsync(predicate); } - public virtual async Task GetAsync(Expression> predicate, - bool includeDetails = true, CancellationToken cancellationToken = default) + public virtual async Task GetAsync(Expression> predicate, bool includeDetails = true, CancellationToken cancellationToken = default) { return await GetFirstAsync(predicate); } - public virtual async Task DeleteAsync(Expression> predicate, bool autoSave = false, - CancellationToken cancellationToken = default) + public virtual async Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { await this.DeleteAsync(predicate); } - public virtual async Task DeleteDirectAsync(Expression> predicate, - CancellationToken cancellationToken = default) + public virtual async Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default) { await this.DeleteAsync(predicate); } @@ -113,71 +103,60 @@ namespace Yi.Framework.SqlSugarCore.Repositories throw new NotImplementedException(); } - public virtual async Task> GetListAsync(Expression> predicate, - bool includeDetails = false, CancellationToken cancellationToken = default) + public virtual async Task> GetListAsync(Expression> predicate, bool includeDetails = false, CancellationToken cancellationToken = default) { return await GetListAsync(predicate); } - public virtual async Task InsertAsync(TEntity entity, bool autoSave = false, - CancellationToken cancellationToken = default) + public virtual async Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { return await InsertReturnEntityAsync(entity); } - public virtual async Task InsertManyAsync(IEnumerable entities, bool autoSave = false, - CancellationToken cancellationToken = default) + public virtual async Task InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { await InsertRangeAsync(entities.ToList()); } - public virtual async Task UpdateAsync(TEntity entity, bool autoSave = false, - CancellationToken cancellationToken = default) + public virtual async Task UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { await UpdateAsync(entity); return entity; } - public virtual async Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, - CancellationToken cancellationToken = default) + public virtual async Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { await UpdateRangeAsync(entities.ToList()); } - public virtual async Task DeleteAsync(TEntity entity, bool autoSave = false, - CancellationToken cancellationToken = default) + public virtual async Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { await DeleteAsync(entity); } - public virtual async Task DeleteManyAsync(IEnumerable entities, bool autoSave = false, - CancellationToken cancellationToken = default) + public virtual async Task DeleteManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { await DeleteAsync(entities.ToList()); } - public virtual async Task> GetListAsync(bool includeDetails = false, - CancellationToken cancellationToken = default) + public virtual async Task> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default) { return await GetListAsync(); } public virtual async Task GetCountAsync(CancellationToken cancellationToken = default) { - return await this.CountAsync(_ => true); + return await this.CountAsync(_=>true); } - public virtual async Task> GetPagedListAsync(int skipCount, int maxResultCount, string sorting, - bool includeDetails = false, CancellationToken cancellationToken = default) + public virtual async Task> GetPagedListAsync(int skipCount, int maxResultCount, string sorting, bool includeDetails = false, CancellationToken cancellationToken = default) { return await GetPageListAsync(_ => true, skipCount, maxResultCount); } - #endregion #region 内置DB快捷操作 - public virtual async Task> AsDeleteable() { return (await GetDbSimpleClientAsync()).AsDeleteable(); @@ -192,7 +171,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories { return (await GetDbSimpleClientAsync()).AsInsertable(insertObj); } - + public virtual async Task> AsInsertable(TEntity[] insertObjs) { return (await GetDbSimpleClientAsync()).AsInsertable(insertObjs); @@ -232,11 +211,9 @@ namespace Yi.Framework.SqlSugarCore.Repositories { return (await GetDbSimpleClientAsync()).AsUpdateable(updateObjs); } - #endregion #region SimpleClient模块 - public virtual async Task CountAsync(Expression> whereExpression) { return await (await GetDbSimpleClientAsync()).CountAsync(whereExpression); @@ -253,6 +230,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories { return await (await GetDbSimpleClientAsync()).DeleteAsync(deleteObj); } + } public virtual async Task DeleteAsync(List deleteObjs) @@ -272,13 +250,13 @@ namespace Yi.Framework.SqlSugarCore.Repositories { if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) { - return await (await GetDbSimpleClientAsync()).AsUpdateable() - .SetColumns(nameof(ISoftDelete.IsDeleted), true).Where(whereExpression).ExecuteCommandAsync() > 0; + return await (await GetDbSimpleClientAsync()).AsUpdateable().SetColumns(nameof(ISoftDelete.IsDeleted), true).Where(whereExpression).ExecuteCommandAsync() > 0; } else { return await (await GetDbSimpleClientAsync()).DeleteAsync(whereExpression); } + } public virtual async Task DeleteByIdAsync(dynamic id) @@ -286,11 +264,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) { var entity = await GetByIdAsync(id); - if (entity is null) - { - return false; - } - + if (entity == null) return false; //反射赋值 ReflexHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, entity); return await UpdateAsync(entity); @@ -311,7 +285,6 @@ namespace Yi.Framework.SqlSugarCore.Repositories { return false; } - //反射赋值 entities.ForEach(e => ReflexHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, e)); return await UpdateRangeAsync(entities); @@ -320,6 +293,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories { return await (await GetDbSimpleClientAsync()).DeleteByIdAsync(ids); } + } public virtual async Task GetByIdAsync(dynamic id) @@ -328,6 +302,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories } + public virtual async Task GetFirstAsync(Expression> whereExpression) { return await (await GetDbSimpleClientAsync()).GetFirstAsync(whereExpression); @@ -343,19 +318,14 @@ namespace Yi.Framework.SqlSugarCore.Repositories return await (await GetDbSimpleClientAsync()).GetListAsync(whereExpression); } - public virtual async Task> GetPageListAsync(Expression> whereExpression, - int pageNum, int pageSize) + public virtual async Task> GetPageListAsync(Expression> whereExpression, int pageNum, int pageSize) { - return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression, - new PageModel() { PageIndex = pageNum, PageSize = pageSize }); + return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression, new PageModel() { PageIndex = pageNum, PageSize = pageSize }); } - public virtual async Task> GetPageListAsync(Expression> whereExpression, - int pageNum, int pageSize, Expression>? orderByExpression = null, - OrderByType orderByType = OrderByType.Asc) + public virtual async Task> GetPageListAsync(Expression> whereExpression, int pageNum, int pageSize, Expression>? orderByExpression = null, OrderByType orderByType = OrderByType.Asc) { - return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression, - new PageModel { PageIndex = pageNum, PageSize = pageSize }, orderByExpression, orderByType); + return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression, new PageModel { PageIndex = pageNum, PageSize = pageSize }, orderByExpression, orderByType); } public virtual async Task GetSingleAsync(Expression> whereExpression) @@ -410,9 +380,9 @@ namespace Yi.Framework.SqlSugarCore.Repositories public virtual async Task UpdateAsync(TEntity updateObj) { - if (typeof(TEntity).IsAssignableTo()) //带版本号乐观锁更新 + if (Options is not null && Options.EnabledConcurrencyException) { - if (Options is not null && Options.EnabledConcurrencyException) + if (typeof(TEntity).IsAssignableTo()) //带版本号乐观锁更新 { try { @@ -426,24 +396,18 @@ namespace Yi.Framework.SqlSugarCore.Repositories $"{ex.Message}[更新失败:ConcurrencyStamp不是最新版本],entityInfo:{updateObj}", ex); } } - else - { - int num = await (await GetDbSimpleClientAsync()) - .Context.Updateable(updateObj).ExecuteCommandAsync(); - return num > 0; - } } return await (await GetDbSimpleClientAsync()).UpdateAsync(updateObj); } - public virtual async Task UpdateAsync(Expression> columns, - Expression> whereExpression) + public virtual async Task UpdateAsync(Expression> columns, Expression> whereExpression) { return await (await GetDbSimpleClientAsync()).UpdateAsync(columns, whereExpression); } + public virtual async Task UpdateRangeAsync(List updateObjs) { return await (await GetDbSimpleClientAsync()).UpdateRangeAsync(updateObjs); @@ -452,36 +416,30 @@ namespace Yi.Framework.SqlSugarCore.Repositories #endregion } - public class SqlSugarRepository : SqlSugarRepository, ISqlSugarRepository, - IRepository where TEntity : class, IEntity, new() + public class SqlSugarRepository : SqlSugarRepository, ISqlSugarRepository, IRepository where TEntity : class, IEntity, new() { - public SqlSugarRepository(ISugarDbContextProvider dbContextProvider) : base( - dbContextProvider) + public SqlSugarRepository(ISugarDbContextProvider sugarDbContextProvider) : base(sugarDbContextProvider) { } - public virtual async Task DeleteAsync(TKey id, bool autoSave = false, - CancellationToken cancellationToken = default) + public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default) { await DeleteByIdAsync(id); } - public virtual async Task DeleteManyAsync(IEnumerable ids, bool autoSave = false, - CancellationToken cancellationToken = default) + public virtual async Task DeleteManyAsync(IEnumerable ids, bool autoSave = false, CancellationToken cancellationToken = default) { await DeleteByIdsAsync(ids.Select(x => (object)x).ToArray()); } - public virtual async Task FindAsync(TKey id, bool includeDetails = true, - CancellationToken cancellationToken = default) + public virtual async Task FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default) { return await GetByIdAsync(id); } - public virtual async Task GetAsync(TKey id, bool includeDetails = true, - CancellationToken cancellationToken = default) + public virtual async Task GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default) { return await GetByIdAsync(id); } } -} \ No newline at end of file +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementCacheDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementCacheDto.cs index 5b8cd6c7..b1e1e30c 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementCacheDto.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementCacheDto.cs @@ -14,4 +14,9 @@ public class AnnouncementCacheDto /// 公告日志列表 /// public List Logs { get; set; } = new List(); + + /// + /// 跳转链接 + /// + public string? Url { get; set; } } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementLogDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementLogDto.cs index 0a7ef430..6014896c 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementLogDto.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementLogDto.cs @@ -1,3 +1,5 @@ +using Yi.Framework.AiHub.Domain.Shared.Enums; + namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement; /// @@ -5,18 +7,38 @@ namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement; /// public class AnnouncementLogDto { - /// - /// 日期 - /// - public string Date { get; set; } = string.Empty; - /// /// 标题 /// - public string Title { get; set; } = string.Empty; + public string Title { get; set; } /// /// 内容列表 /// public List Content { get; set; } = new List(); + + /// + /// 图片url + /// + public string? ImageUrl { get; set; } + + /// + /// 开始时间(系统公告时间、活动开始时间) + /// + public DateTime StartTime { get; set; } + + /// + /// 活动结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 公告类型(系统、活动) + /// + public AnnouncementTypeEnum Type{ get; set; } + + /// + /// 跳转链接 + /// + public string? Url { get; set; } } 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 index 3594dbb8..082c20f9 100644 --- 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 @@ -39,12 +39,7 @@ public class CardFlipStatusOutput /// 本周邀请人数 /// public int InvitedCount { get; set; } - - /// - /// 是否已被邀请(被邀请后不可再提供邀请码) - /// - public bool IsInvited { get; set; } - + /// /// 翻牌记录 /// @@ -54,6 +49,11 @@ public class CardFlipStatusOutput /// 下次可翻牌提示 /// public string? NextFlipTip { get; set; } + + /// + /// 当前用户是否已经填写过邀请码 + /// + public bool IsFilledInviteCode { get; set; } } /// diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/PremiumTokenUsageGetListInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/PremiumTokenUsageGetListInput.cs new file mode 100644 index 00000000..e3766e10 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/PremiumTokenUsageGetListInput.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Application.Dtos; +using Yi.Framework.Ddd.Application.Contracts; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; + +public class PremiumTokenUsageGetListInput : PagedAllResultRequestDto +{ + /// + /// 是否免费 + /// + public bool? IsFree { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/PremiumTokenUsageGetListOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/PremiumTokenUsageGetListOutput.cs new file mode 100644 index 00000000..d3b29ffc --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/UsageStatistics/PremiumTokenUsageGetListOutput.cs @@ -0,0 +1,57 @@ +using Volo.Abp.Application.Dtos; +using Yi.Framework.Ddd.Application.Contracts; + +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; + +public class PremiumTokenUsageGetListOutput : CreationAuditedEntityDto +{ + /// + /// id + /// + public Guid Id { get; set; } + + /// + /// 用户ID + /// + public Guid UserId { get; set; } + + /// + /// 包名称 + /// + public string PackageName { get; set; } + + /// + /// 总用量(总token数) + /// + public long TotalTokens { get; set; } + + /// + /// 剩余用量(剩余token数) + /// + public long RemainingTokens { get; set; } + + /// + /// 已使用token数 + /// + public long UsedTokens { get; set; } + + /// + /// 到期时间 + /// + public DateTime? ExpireDateTime { get; set; } + + /// + /// 是否激活 + /// + public bool IsActive { get; set; } + + /// + /// 购买金额 + /// + public decimal PurchaseAmount { get; set; } + + /// + /// 备注 + /// + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs index 77d4d29a..acdda549 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs @@ -11,5 +11,5 @@ public interface IAnnouncementService /// 获取公告信息 /// /// 公告信息 - Task GetAsync(); + Task> GetAsync(); } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AnnouncementService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AnnouncementService.cs index 8914cd25..2399070e 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AnnouncementService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AnnouncementService.cs @@ -1,3 +1,4 @@ +using Mapster; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Volo.Abp.Application.Services; @@ -14,13 +15,13 @@ namespace Yi.Framework.AiHub.Application.Services; /// public class AnnouncementService : ApplicationService, IAnnouncementService { - private readonly ISqlSugarRepository _announcementRepository; + private readonly ISqlSugarRepository _announcementRepository; private readonly IConfiguration _configuration; private readonly IDistributedCache _announcementCache; private const string AnnouncementCacheKey = "AiHub:Announcement"; public AnnouncementService( - ISqlSugarRepository announcementRepository, + ISqlSugarRepository announcementRepository, IConfiguration configuration, IDistributedCache announcementCache) { @@ -32,7 +33,7 @@ public class AnnouncementService : ApplicationService, IAnnouncementService /// /// 获取公告信息 /// - public async Task GetAsync() + public async Task> GetAsync() { // 使用 GetOrAddAsync 从缓存获取或添加数据,缓存1小时 var cacheData = await _announcementCache.GetOrAddAsync( @@ -44,11 +45,7 @@ public class AnnouncementService : ApplicationService, IAnnouncementService } ); - return new AnnouncementOutput - { - Version = cacheData?.Version ?? "v1.0.0", - Logs = cacheData?.Logs ?? new List() - }; + return cacheData?.Logs ?? new List(); } /// @@ -56,25 +53,15 @@ public class AnnouncementService : ApplicationService, IAnnouncementService /// private async Task LoadAnnouncementDataAsync() { - // 从配置文件读取版本号,如果没有配置则使用默认值 - var version = _configuration["AiHubVersion"] ?? "v1.0.0"; - // 查询所有公告日志,按日期降序排列 var logs = await _announcementRepository._DbQueryable - .OrderByDescending(x => x.Date) + .OrderByDescending(x => x.StartTime) .ToListAsync(); // 转换为 DTO - var logDtos = logs.Select(log => new AnnouncementLogDto - { - Date = log.Date.ToString("yyyy-MM-dd"), - Title = log.Title, - Content = log.Content - }).ToList(); - + var logDtos = logs.Adapt>(); return new AnnouncementCacheDto { - Version = version, Logs = logDtos }; } 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 index d7bb6ba8..feefd4ba 100644 --- 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 @@ -51,9 +51,8 @@ public class CardFlipService : ApplicationService, ICardFlipService // 统计本周邀请人数 var invitedCount = await _inviteCodeManager.GetWeeklyInvitationCountAsync(userId, weekStart); - // 检查用户是否已被邀请 - var isInvited = await _inviteCodeManager.IsUserInvitedAsync(userId); - + //当前用户是否已填写过邀请码 + var isFilledInviteCode = await _inviteCodeManager.IsFilledInviteCodeAsync(userId); var output = new CardFlipStatusOutput { TotalFlips = task?.TotalFlips ?? 0, @@ -63,8 +62,8 @@ public class CardFlipService : ApplicationService, ICardFlipService CanFlip = _cardFlipManager.CanFlipCard(task), MyInviteCode = inviteCode?.Code, InvitedCount = invitedCount, - IsInvited = isInvited, - FlipRecords = BuildFlipRecords(task) + FlipRecords = BuildFlipRecords(task), + IsFilledInviteCode = isFilledInviteCode }; // 生成提示信息 @@ -87,7 +86,7 @@ public class CardFlipService : ApplicationService, ICardFlipService // 如果中奖,发放奖励 if (result.IsWin) { - await GrantRewardAsync(userId, result.RewardAmount, $"翻牌活动第{input.FlipNumber}次中奖"); + await GrantRewardAsync(userId, result.RewardAmount, $"翻牌活动-序号{input.FlipNumber}中奖"); } // 构建输出 @@ -147,7 +146,6 @@ public class CardFlipService : ApplicationService, ICardFlipService { MyInviteCode = inviteCode?.Code, InvitedCount = invitedCount, - IsInvited = inviteCode?.IsUserInvited ?? false, InvitationHistory = invitationHistory.Select(x => new InvitationHistoryItem { InvitedUserName = x.InvitedUserName, @@ -183,6 +181,9 @@ public class CardFlipService : ApplicationService, ICardFlipService var flippedOrder = task != null ? _cardFlipManager.GetFlippedOrder(task) : new List(); var flippedNumbers = new HashSet(flippedOrder); + // 获取中奖记录 + var winRecords = task?.WinRecords ?? new Dictionary(); + // 构建记录,按照原始序号1-10排列 for (int i = 1; i <= CardFlipManager.TOTAL_MAX_FLIPS; i++) { @@ -202,23 +203,11 @@ public class CardFlipService : ApplicationService, ICardFlipService { var flipOrderIndex = flippedOrder.IndexOf(i) + 1; // 第几次翻的(1-based) - // 第8次翻的卡中奖 - if (flipOrderIndex == 8) + // 检查这次翻牌是否中奖 + if (winRecords.TryGetValue(flipOrderIndex, out var rewardAmount)) { record.IsWin = true; - record.RewardAmount = task.EighthRewardAmount; - } - // 第9次翻的卡中奖 - else if (flipOrderIndex == 9) - { - record.IsWin = true; - record.RewardAmount = task.NinthRewardAmount; - } - // 第10次翻的卡中奖 - else if (flipOrderIndex == 10) - { - record.IsWin = true; - record.RewardAmount = task.TenthRewardAmount; + record.RewardAmount = rewardAmount; } } @@ -246,10 +235,10 @@ public class CardFlipService : ApplicationService, ICardFlipService { if (status.TotalFlips >= 7) { - return $"本周使用他人邀请码可解锁{status.RemainingInviteFlips}次翻牌,且必中大奖!每次中奖最大额度将翻倍!"; + return $"本周使用他人邀请码或他人使用你的邀请码,可解锁{status.RemainingInviteFlips}次翻牌,且必中大奖!每次中奖最大额度将翻倍!"; } - return $"本周使用他人邀请码可解锁{status.RemainingInviteFlips}次翻牌,必中大奖!每次中奖最大额度将翻倍!"; + return $"本周使用他人邀请码或他人使用你的邀请码,可解锁{status.RemainingInviteFlips}次翻牌,必中大奖!每次中奖最大额度将翻倍!"; } return "继续加油!"; diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs similarity index 100% rename from Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/AiChatService.cs rename to Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/AiChatService.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/MessageService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/MessageService.cs similarity index 100% rename from Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/MessageService.cs rename to Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/MessageService.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/SessionService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/SessionService.cs similarity index 100% rename from Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/SessionService.cs rename to Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/SessionService.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/TokenService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs similarity index 100% rename from Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/TokenService.cs rename to Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/Chat/TokenService.cs diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs index 05d6eaeb..feedf690 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/UsageStatisticsService.cs @@ -1,5 +1,8 @@ +using Mapster; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using SqlSugar; +using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics; @@ -7,6 +10,7 @@ using Yi.Framework.AiHub.Application.Contracts.IServices; using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities.Chat; using Yi.Framework.AiHub.Domain.Extensions; +using Yi.Framework.Ddd.Application.Contracts; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Application.Services; @@ -137,4 +141,27 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic return result; } + + /// + /// 获取当前用户尊享服务token用量统计列表 + /// + /// + [HttpGet("usage-statistics/premium-token-usage/list")] + public async Task> GetPremiumTokenUsageListAsync( + PremiumTokenUsageGetListInput input) + { + var userId = CurrentUser.GetId(); + RefAsync total = 0; + // 获取尊享包Token信息 + var entities = await _premiumPackageRepository._DbQueryable + .Where(x => x.UserId == userId) + .WhereIF(input.IsFree == false, x => x.PurchaseAmount > 0) + .WhereIF(input.IsFree == true, x => x.PurchaseAmount == 0) + .WhereIF(input.StartTime is not null && input.EndTime is not null, + x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime) + .OrderByDescending(x => x.CreationTime) + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + return new PagedResultDto(total, + entities.Adapt>()); + } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/AnnouncementTypeEnum.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/AnnouncementTypeEnum.cs new file mode 100644 index 00000000..fa898e18 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Enums/AnnouncementTypeEnum.cs @@ -0,0 +1,7 @@ +namespace Yi.Framework.AiHub.Domain.Shared.Enums; + +public enum AnnouncementTypeEnum +{ + Activity=1, + System=2 +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/AnnouncementAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/AnnouncementAggregateRoot.cs new file mode 100644 index 00000000..4488d985 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/AnnouncementAggregateRoot.cs @@ -0,0 +1,57 @@ +using SqlSugar; +using Volo.Abp.Domain.Entities.Auditing; +using Yi.Framework.AiHub.Domain.Shared.Enums; + +namespace Yi.Framework.AiHub.Domain.Entities; + +/// +/// 公告日志 +/// +[SugarTable("Ai_Announcement")] +public class AnnouncementAggregateRoot : FullAuditedAggregateRoot +{ + public AnnouncementAggregateRoot() + { + } + + /// + /// 标题 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 内容列表(JSON格式存储) + /// + [SugarColumn(IsJson = true, IsNullable = false)] + public List Content { get; set; } = new List(); + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 图片url + /// + public string? ImageUrl { get; set; } + + /// + /// 开始时间(系统公告时间、活动开始时间) + /// + public DateTime StartTime { get; set; } + + /// + /// 活动结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 公告类型(系统、活动) + /// + public AnnouncementTypeEnum Type{ get; set; } + + /// + /// 跳转链接 + /// + public string? Url { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/AnnouncementLogAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/AnnouncementLogAggregateRoot.cs deleted file mode 100644 index ec890394..00000000 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/AnnouncementLogAggregateRoot.cs +++ /dev/null @@ -1,44 +0,0 @@ -using SqlSugar; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Yi.Framework.AiHub.Domain.Entities; - -/// -/// 公告日志 -/// -[SugarTable("Ai_AnnouncementLog")] -[SugarIndex($"index_{nameof(Date)}", nameof(Date), OrderByType.Desc)] -public class AnnouncementLogAggregateRoot : FullAuditedAggregateRoot -{ - public AnnouncementLogAggregateRoot() - { - } - - public AnnouncementLogAggregateRoot(DateTime date, string title, List content) - { - Date = date; - Title = title; - Content = content; - } - - /// - /// 日期 - /// - public DateTime Date { get; set; } - - /// - /// 标题 - /// - public string Title { get; set; } = string.Empty; - - /// - /// 内容列表(JSON格式存储) - /// - [SugarColumn(IsJson = true, IsNullable = false)] - public List Content { get; set; } = new List(); - - /// - /// 备注 - /// - public string? Remark { get; set; } -} 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 index 3adaba7e..9f51870e 100644 --- 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 @@ -25,9 +25,8 @@ public class CardFlipTaskAggregateRoot : FullAuditedAggregateRoot BonusFlipsUsed = 0; InviteFlipsUsed = 0; IsFirstFlipDone = false; - HasNinthReward = false; - HasTenthReward = false; FlippedOrder = new List(); + WinRecords = new Dictionary(); } /// @@ -66,34 +65,10 @@ public class CardFlipTaskAggregateRoot : FullAuditedAggregateRoot public bool IsFirstFlipDone { get; set; } /// - /// 是否已获得第8次奖励 + /// 中奖记录(以翻牌顺序为key,例如第1次翻牌中奖则key为1,奖励金额为value) /// - public bool HasEighthReward { get; set; } - - /// - /// 第8次奖励金额(100-300w) - /// - public long? EighthRewardAmount { get; set; } - - /// - /// 是否已获得第9次奖励 - /// - public bool HasNinthReward { get; set; } - - /// - /// 第9次奖励金额(100-500w) - /// - public long? NinthRewardAmount { get; set; } - - /// - /// 是否已获得第10次奖励 - /// - public bool HasTenthReward { get; set; } - - /// - /// 第10次奖励金额(100-1000w) - /// - public long? TenthRewardAmount { get; set; } + [SugarColumn(IsJson = true, IsNullable = true)] + public Dictionary? WinRecords { get; set; } /// /// 备注信息 @@ -135,33 +110,17 @@ public class CardFlipTaskAggregateRoot : FullAuditedAggregateRoot } /// - /// 记录第8次奖励 + /// 记录中奖 /// + /// 第几次翻牌(1-10) /// 奖励金额 - public void SetEighthReward(long amount) + public void SetWinReward(int flipCount, long amount) { - HasEighthReward = true; - EighthRewardAmount = amount; - } - - /// - /// 记录第9次奖励 - /// - /// 奖励金额 - public void SetNinthReward(long amount) - { - HasNinthReward = true; - NinthRewardAmount = amount; - } - - /// - /// 记录第10次奖励 - /// - /// 奖励金额 - public void SetTenthReward(long amount) - { - HasTenthReward = true; - TenthRewardAmount = amount; + if (WinRecords == null) + { + WinRecords = new Dictionary(); + } + WinRecords[flipCount] = amount; } } 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 index de569b49..bcd310af 100644 --- 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 @@ -19,8 +19,6 @@ public class InviteCodeAggregateRoot : FullAuditedAggregateRoot { UserId = userId; Code = code; - IsUsed = false; - IsUserInvited = false; UsedCount = 0; } @@ -34,55 +32,16 @@ public class InviteCodeAggregateRoot : FullAuditedAggregateRoot /// [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/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/CardFlipManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/CardFlipManager.cs index 0eae034c..96ae5d1d 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/CardFlipManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/CardFlipManager.cs @@ -24,6 +24,11 @@ public class CardFlipManager : DomainService private const int NINTH_FLIP = 9; // 第9次翻牌 private const int TENTH_FLIP = 10; // 第10次翻牌 + // 前7次免费翻牌奖励配置 + private const long FREE_MIN_REWARD = 10000; // 前7次最小奖励 1w + private const long FREE_MAX_REWARD = 30000; // 前7次最大奖励 3w + private const double FREE_WIN_RATE = 0.5; // 前7次中奖概率 50% + private const long EIGHTH_MIN_REWARD = 1000000; // 第8次最小奖励 100w private const long EIGHTH_MAX_REWARD = 3000000; // 第8次最大奖励 300w private const long NINTH_MIN_REWARD = 1000000; // 第9次最小奖励 100w @@ -112,23 +117,23 @@ public class CardFlipManager : DomainService throw new UserFriendlyException(GetFlipTypeErrorMessage(flipType)); } - // 如果是邀请类型翻牌,必须验证用户本周填写的邀请码数量足够 + // 如果是邀请类型翻牌,必须验证用户本周的邀请记录数量足够(包括填写别人的邀请码和别人填写我的邀请码) if (flipType == FlipType.Invite) { - // 查询本周已使用的邀请码数量 - var weeklyInviteCodeUsedCount = await _invitationRecordRepository._DbQueryable - .Where(x => x.InvitedUserId == userId) + // 查询本周作为邀请人或被邀请人的记录数量(双方都会增加翻牌次数) + var weeklyInviteRecordCount = await _invitationRecordRepository._DbQueryable + .Where(x => x.InviterId == userId || x.InvitedUserId == userId) .Where(x => x.InvitationTime >= weekStart) .CountAsync(); - // 本周填写的邀请码数量必须 >= 即将使用的邀请翻牌次数 - // 例如: 要翻第8次(InviteFlipsUsed=0->1), 需要至少填写了1个邀请码 - // 要翻第9次(InviteFlipsUsed=1->2), 需要至少填写了2个邀请码 - // 要翻第10次(InviteFlipsUsed=2->3), 需要至少填写了3个邀请码 - var requiredInviteCodeCount = task.InviteFlipsUsed + 1; - if (weeklyInviteCodeUsedCount < requiredInviteCodeCount) + // 本周邀请记录数量必须 >= 即将使用的邀请翻牌次数 + // 例如: 要翻第8次(InviteFlipsUsed=0->1), 需要至少有1条邀请记录(我邀请别人或别人邀请我) + // 要翻第9次(InviteFlipsUsed=1->2), 需要至少有2条邀请记录 + // 要翻第10次(InviteFlipsUsed=2->3), 需要至少有3条邀请记录 + var requiredInviteRecordCount = task.InviteFlipsUsed + 1; + if (weeklyInviteRecordCount < requiredInviteRecordCount) { - throw new UserFriendlyException($"需本周累积使用{requiredInviteCodeCount}个他人邀请码才能解锁第{task.TotalFlips + 1}次翻牌,您还差一个~"); + throw new UserFriendlyException($"需本周累积{requiredInviteRecordCount}次邀请记录(填写别人的邀请码或别人填写你的邀请码)才能解锁第{task.TotalFlips + 1}次翻牌"); } } @@ -152,18 +157,7 @@ public class CardFlipManager : DomainService // 如果中奖,记录奖励金额(用于后续查询显示) if (result.IsWin) { - if (flipCount == EIGHTH_FLIP) - { - task.SetEighthReward(result.RewardAmount); - } - else if (flipCount == NINTH_FLIP) - { - task.SetNinthReward(result.RewardAmount); - } - else if (flipCount == TENTH_FLIP) - { - task.SetTenthReward(result.RewardAmount); - } + task.SetWinReward(flipCount, result.RewardAmount); } await _cardFlipTaskRepository.UpdateAsync(task); @@ -223,11 +217,24 @@ public class CardFlipManager : DomainService IsWin = false }; - // 前7次固定失败 + var random = new Random(); + + // 前7次: 50%概率中奖,奖励1w-3w if (flipCount <= 7) { - result.IsWin = false; - result.RewardDesc = "很遗憾,未中奖"; + // 50%概率中奖 + if (random.NextDouble() < FREE_WIN_RATE) + { + var rewardAmount = GenerateRandomReward(FREE_MIN_REWARD, FREE_MAX_REWARD); + result.IsWin = true; + result.RewardAmount = rewardAmount; + result.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000m:0.##}w tokens!"; + } + else + { + result.IsWin = false; + result.RewardDesc = "很遗憾,未中奖"; + } } // 第8次中奖 (邀请码解锁) else if (flipCount == EIGHTH_FLIP) @@ -271,21 +278,24 @@ public class CardFlipManager : DomainService } /// - /// 生成随机奖励金额 (最小单位100w) + /// 生成随机奖励金额 /// private long GenerateRandomReward(long min, long max) { var random = new Random(); - const long unit = 1000000; // 100w的单位 - // 将min和max转换为100w的倍数 + // 根据最小值判断单位 + // 如果min小于100000,则使用1w(10000)作为单位;否则使用100w(1000000)作为单位 + long unit = min < 100000 ? 10000 : 1000000; + + // 将min和max转换为单位的倍数 long minUnits = min / unit; long maxUnits = max / unit; // 在倍数范围内随机 long randomUnits = random.Next((int)minUnits, (int)maxUnits + 1); - // 返回100w的倍数 + // 返回单位的倍数 return randomUnits * unit; } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/InviteCodeManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/InviteCodeManager.cs index 1deed773..9fae5e81 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/InviteCodeManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/InviteCodeManager.cs @@ -63,14 +63,19 @@ public class InviteCodeManager : DomainService } /// - /// 统计用户本周邀请人数 + /// 统计用户本周邀请人数(别人填写我的邀请码的次数/或者我填写别人邀请码) /// public async Task GetWeeklyInvitationCountAsync(Guid userId, DateTime weekStart) { - return await _invitationRecordRepository._DbQueryable + var inviterCount= await _invitationRecordRepository._DbQueryable + .Where(x => x.InviterId == userId) + .Where(x => x.InvitationTime >= weekStart) + .CountAsync(); + var invitedUserIdCount= await _invitationRecordRepository._DbQueryable .Where(x => x.InvitedUserId == userId) .Where(x => x.InvitationTime >= weekStart) .CountAsync(); + return inviterCount + invitedUserIdCount; } /// @@ -91,7 +96,7 @@ public class InviteCodeManager : DomainService } /// - /// 使用邀请码 + /// 使用邀请码(双方都增加翻牌次数) /// /// 使用者ID /// 邀请码 @@ -118,75 +123,40 @@ public class InviteCodeManager : DomainService { throw new UserFriendlyException("不能使用自己的邀请码"); } + + // 检查当前用户是否已经填写过别人的邀请码(一辈子只能填写一次) + var hasUsedOthersCode = await IsFilledInviteCodeAsync(userId); - // 验证邀请码是否已被使用 - if (inviteCodeEntity.IsUsed) + if (hasUsedOthersCode) { - throw new UserFriendlyException("该邀请码已被使用"); + throw new UserFriendlyException("您已经填写过别人的邀请码了,每个账号只能填写一次"); } - - // 验证邀请码拥有者是否已被邀请 - if (inviteCodeEntity.IsUserInvited) - { - throw new UserFriendlyException("该用户已被邀请,邀请码无效"); - } - - // 验证本周邀请码使用次数 - var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now); - var weeklyUseCount = await _invitationRecordRepository._DbQueryable - .Where(x => x.InvitedUserId == userId) - .Where(x => x.InvitationTime >= weekStart) - .CountAsync(); - - if (weeklyUseCount >= CardFlipManager.MAX_INVITE_FLIPS) - { - throw new UserFriendlyException($"本周邀请码使用次数已达上限({CardFlipManager.MAX_INVITE_FLIPS}次),请下周再来"); - } - - // 检查当前用户的邀请码信息 - var myInviteCode = await _inviteCodeRepository._DbQueryable - .Where(x => x.UserId == userId) - .FirstAsync(); - - // 标记邀请码为已使用 - inviteCodeEntity.MarkAsUsed(userId); + + // 增加邀请码被使用次数 + inviteCodeEntity.UsedCount++; await _inviteCodeRepository.UpdateAsync(inviteCodeEntity); - - // 标记当前用户已被邀请(仅第一次使用邀请码时标记) - if (myInviteCode == null) - { - myInviteCode = new InviteCodeAggregateRoot(userId, GenerateUniqueInviteCode()); - myInviteCode.MarkUserAsInvited(); - await _inviteCodeRepository.InsertAsync(myInviteCode); - } - else if (!myInviteCode.IsUserInvited) - { - myInviteCode.MarkUserAsInvited(); - await _inviteCodeRepository.UpdateAsync(myInviteCode); - } - - // 创建邀请记录 + + // 创建邀请记录(双方都会因为这条记录增加一次翻牌机会) var invitationRecord = new InvitationRecordAggregateRoot( inviteCodeEntity.UserId, userId, inviteCode); await _invitationRecordRepository.InsertAsync(invitationRecord); - _logger.LogInformation($"用户 {userId} 使用邀请码 {inviteCode} 成功"); + _logger.LogInformation($"用户 {userId} 使用邀请码 {inviteCode} 成功,邀请人 {inviteCodeEntity.UserId} 和被邀请人 {userId} 都增加一次翻牌机会"); return inviteCodeEntity.UserId; } /// - /// 检查用户是否已被邀请 + /// 检查用户是否已填写过邀请码 /// - public async Task IsUserInvitedAsync(Guid userId) + public async Task IsFilledInviteCodeAsync(Guid userId) { - var inviteCode = await _inviteCodeRepository._DbQueryable - .Where(x => x.UserId == userId) - .FirstAsync(); - - return inviteCode?.IsUserInvited ?? false; + // 检查当前用户是否已经填写过别人的邀请码(一辈子只能填写一次) + return await _invitationRecordRepository._DbQueryable + .Where(x => x.InvitedUserId == userId) + .AnyAsync(); } /// diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Caches/FileCacheItem.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Caches/FileCacheItem.cs index 47a91d4f..a8bfe3ab 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Caches/FileCacheItem.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/Caches/FileCacheItem.cs @@ -1,4 +1,4 @@ -namespace Yi.Framework.Rbac.Domain.Shared.Caches; +namespace Yi.Framework.Rbac.Domain.Shared.Caches; public class FileCacheItem { diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index 1e701178..a7d136b7 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.Application.Services; using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AspNetCore; using Yi.Framework.AspNetCore.Authentication.OAuth; @@ -354,7 +355,10 @@ 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.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); + // app.ApplicationServices.GetRequiredService().SqlSugarClient.CodeFirst.InitTables(); //跨域 app.UseCors(DefaultCorsPolicyName); diff --git a/Yi.Ai.Vue3/.claude/settings.local.json b/Yi.Ai.Vue3/.claude/settings.local.json new file mode 100644 index 00000000..4477e4c3 --- /dev/null +++ b/Yi.Ai.Vue3/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(npx vue-tsc --noEmit)" + ], + "deny": [], + "ask": [] + } +} diff --git a/Yi.Ai.Vue3/index.html b/Yi.Ai.Vue3/index.html index 88c0e7d3..312c6934 100644 --- a/Yi.Ai.Vue3/index.html +++ b/Yi.Ai.Vue3/index.html @@ -112,7 +112,7 @@ - 意心Ai 2.2 + 意心Ai 2.3 海外地址,仅首次访问预计加载约10秒 diff --git a/Yi.Ai.Vue3/src/App.vue b/Yi.Ai.Vue3/src/App.vue index e1bab0c5..3ea0046e 100644 --- a/Yi.Ai.Vue3/src/App.vue +++ b/Yi.Ai.Vue3/src/App.vue @@ -1,37 +1,8 @@ - - diff --git a/Yi.Ai.Vue3/src/api/announcement/index.ts b/Yi.Ai.Vue3/src/api/announcement/index.ts index 25b7b76c..a8ae2870 100644 --- a/Yi.Ai.Vue3/src/api/announcement/index.ts +++ b/Yi.Ai.Vue3/src/api/announcement/index.ts @@ -1,31 +1,12 @@ -import { get } from '@/utils/request' -import type { - ActivityDetailResponse, - AnnouncementDetailResponse, - SystemAnnouncementResponse, -} from './types' +import type { AnnouncementLogDto } from './types'; +import { get } from '@/utils/request'; /** * 获取系统公告和活动数据 + * 后端接口: GET /api/app/announcement + * 返回格式: AnnouncementLogDto[] */ export function getSystemAnnouncements() { - return get('/announcement/system').json() + return get('/announcement').json(); } - -/** - * 获取活动详情 - * @param id 活动ID - */ -export function getActivityDetail(id: string | number) { - return get(`/announcement/activity/${id}`).json() -} - -/** - * 获取公告详情 - * @param id 公告ID - */ -export function getAnnouncementDetail(id: string | number) { - return get(`/announcement/detail/${id}`).json() -} - -export * from './types' +export * from './types'; diff --git a/Yi.Ai.Vue3/src/api/announcement/types.ts b/Yi.Ai.Vue3/src/api/announcement/types.ts index 576cee6f..c05a030a 100644 --- a/Yi.Ai.Vue3/src/api/announcement/types.ts +++ b/Yi.Ai.Vue3/src/api/announcement/types.ts @@ -1,51 +1,18 @@ -// 轮播图类型 -export interface CarouselItem { - id: number | string - imageUrl: string - link?: string - title?: string -} +// 公告类型(对应后端 AnnouncementTypeEnum) +export type AnnouncementType = 'Activity' | 'System' -// 活动类型 -export interface Activity { - id: number | string +// 公告DTO(对应后端 AnnouncementLogDto) +export interface AnnouncementLogDto { + /** 标题 */ title: string - description: string - content: string - coverImage?: string - startTime?: string - endTime?: string - status: 'active' | 'inactive' | 'expired' - createdAt: string - updatedAt?: string -} - -// 公告类型 -export interface Announcement { - id: number | string - title: string - content: string - type: 'latest' | 'history' - isImportant?: boolean - publishTime: string - createdAt: string - updatedAt?: string -} - -// 系统公告响应类型 -export interface SystemAnnouncementResponse { - carousels: CarouselItem[] - activities: Activity[] - announcements: Announcement[] -} - -// 公告详情响应类型 -export interface AnnouncementDetailResponse extends Announcement { - views?: number -} - -// 活动详情响应类型 -export interface ActivityDetailResponse extends Activity { - views?: number - participantCount?: number + /** 内容列表 */ + content: string[] + /** 图片url */ + imageUrl?: string | null + /** 开始时间(系统公告时间、活动开始时间) */ + startTime: string + /** 活动结束时间 */ + endTime?: string | null + /** 公告类型(系统、活动) */ + type: AnnouncementType } diff --git a/Yi.Ai.Vue3/src/api/cardFlip/types.ts b/Yi.Ai.Vue3/src/api/cardFlip/types.ts index 4683f9a3..6b0ceab0 100644 --- a/Yi.Ai.Vue3/src/api/cardFlip/types.ts +++ b/Yi.Ai.Vue3/src/api/cardFlip/types.ts @@ -7,9 +7,10 @@ export interface CardFlipStatusOutput { canFlip: boolean; // 是否可以翻牌 myInviteCode?: string; // 用户的邀请码 invitedCount: number; // 本周邀请人数 - isInvited: boolean; // 是否已被邀请 + // isInvited: boolean; // 是否已被邀请 flipRecords: CardFlipRecord[]; // 翻牌记录 nextFlipTip?: string; // 下次可翻牌提示 + isFilledInviteCode: boolean;// 当前用户是否已经填写过邀请码 } // 翻牌记录 diff --git a/Yi.Ai.Vue3/src/api/user/index.ts b/Yi.Ai.Vue3/src/api/user/index.ts index 2c1db1b7..6b9d6b8d 100644 --- a/Yi.Ai.Vue3/src/api/user/index.ts +++ b/Yi.Ai.Vue3/src/api/user/index.ts @@ -1,5 +1,61 @@ import { get, post } from '@/utils/request'; +// 尊享包用量明细DTO +export interface PremiumTokenUsageDto { + /** id */ + id: string; + /** 用户ID */ + userId: string; + /** 包名称 */ + packageName: string; + /** 总用量(总token数) */ + totalTokens: number; + /** 剩余用量(剩余token数) */ + remainingTokens: number; + /** 已使用token数 */ + usedTokens: number; + /** 到期时间 */ + expireDateTime?: string; + /** 是否激活 */ + isActive: boolean; + /** 购买金额 */ + purchaseAmount: number; + /** 备注 */ + remark?: string; + /** 创建时间 */ + creationTime?: string; + /** 创建者ID */ + creatorId?: string; +} + +// 查询参数接口 - 匹配后端 PagedAllResultRequestDto +export interface PremiumTokenUsageQueryParams { + /** 查询开始时间 */ + startTime?: string; + /** 查询结束时间 */ + endTime?: string; + /** 排序列名 */ + orderByColumn?: string; + /** 排序方向(ascending/descending) */ + isAsc?: string; + /** 跳过数量(分页) */ + skipCount?: number; + /** 最大返回数量(分页) */ + maxResultCount?: number; + /** 是否免费 */ + isFree?: boolean; + // 是否为升序排序 + isAscending?: boolean; +} + +// 分页响应接口 +export interface PagedResult { + /** 数据列表 */ + items: T[]; + /** 总数量 */ + totalCount: number; +} + // 获取用户信息 export function getUserInfo() { return get('/account/ai').json(); @@ -24,3 +80,63 @@ export function getWechatAuth(data: any) { export function getPremiumTokenPackage() { return get('/usage-statistics/premium-token-usage').json(); } + +// 获取尊享包用量明细列表 +export function getPremiumTokenUsageList(params?: PremiumTokenUsageQueryParams) { + return get>('/usage-statistics/premium-token-usage/list', params).json(); +} + +// 查询条件的后端dto,其他查询或者排序由前端自己实现: +// using Volo.Abp.Application.Dtos; +// +// namespace Yi.Framework.Ddd.Application.Contracts +// { +// /// +// /// 分页查询请求DTO,包含时间范围和自定义排序功能 +// /// +// public class PagedAllResultRequestDto : PagedAndSortedResultRequestDto, IPagedAllResultRequestDto +// { +// /// +// /// 查询开始时间 +// /// +// public DateTime? StartTime { get; set; } +// +// /// +// /// 查询结束时间 +// /// +// public DateTime? EndTime { get; set; } +// +// /// +// /// 排序列名 +// /// +// public string? OrderByColumn { get; set; } +// +// /// +// /// 排序方向(ascending/descending) +// /// +// public string? IsAsc { get; set; } +// +// /// +// /// 是否为升序排序 +// /// +// public bool IsAscending => string.Equals(IsAsc, "ascending", StringComparison.OrdinalIgnoreCase); +// +// private string? _sorting; +// +// /// +// /// 排序表达式 +// /// +// public override string? Sorting +// { +// get +// { +// if (!string.IsNullOrWhiteSpace(OrderByColumn)) +// { +// return $"{OrderByColumn} {(IsAscending ? "ASC" : "DESC")}"; +// } +// return _sorting; +// } +// set => _sorting = value; +// } +// } +// } diff --git a/Yi.Ai.Vue3/src/components/LoginDialog/components/QrCodeLogin/index.vue b/Yi.Ai.Vue3/src/components/LoginDialog/components/QrCodeLogin/index.vue index 601387d1..0031b25b 100644 --- a/Yi.Ai.Vue3/src/components/LoginDialog/components/QrCodeLogin/index.vue +++ b/Yi.Ai.Vue3/src/components/LoginDialog/components/QrCodeLogin/index.vue @@ -7,6 +7,7 @@ import { getQrCode, getQrCodeResult, getUserInfo } from '@/api'; import { useUserStore } from '@/stores'; import { useSessionStore } from '@/stores/modules/session.ts'; import { WECHAT_QRCODE_TYPE } from '@/utils/user.ts'; +import { useGuideTour } from '@/hooks/useGuideTour'; const props = defineProps({ type: { @@ -29,6 +30,7 @@ const userStore = useUserStore(); const router = useRouter(); const sessionStore = useSessionStore(); const isQrCodeError = ref(false); +const { startFullTour } = useGuideTour(); // 二维码倒计时实例 const { start: qrStart, stop: qrStop } = useCountdown(shallowRef(600), { interval: 1000, @@ -126,6 +128,11 @@ async function handleLoginSuccess(token: string, refreshToken: string) { stopPolling(); userStore.setToken(token, refreshToken); const resUserInfo = await getUserInfo(); + + // 判断是否为新用户(注册时间小于1小时) + const creationTime = resUserInfo.data.user.creationTime; // 格式: "2024-11-01 12:01:34" + const isNewUser = checkIsNewUser(creationTime); + userStore.setUserInfo(resUserInfo.data); // 提示用户 ElMessage.success('登录成功'); @@ -133,6 +140,34 @@ async function handleLoginSuccess(token: string, refreshToken: string) { await router.replace('/'); await sessionStore.requestSessionList(1, true); userStore.closeLoginDialog(); + + // 如果是新用户,延迟500ms后自动触发新手引导 + if (isNewUser) { + setTimeout(() => { + startFullTour(); + }, 500); + } +} + +// 判断是否为新用户(注册时间距离当前时间小于1小时) +function checkIsNewUser(creationTimeStr: string): boolean { + try { + // 解析注册时间字符串 "2024-11-01 12:01:34" + const creationTime = new Date(creationTimeStr.replace(' ', 'T')); + const currentTime = new Date(); + + // 计算时间差(毫秒) + const timeDiff = currentTime.getTime() - creationTime.getTime(); + + // 1小时 = 60分钟 * 60秒 * 1000毫秒 = 3600000毫秒 + const oneHourInMs = 60 * 60 * 1000; + + return timeDiff < oneHourInMs; + } + catch (error) { + console.error('解析注册时间失败:', error); + return false; + } } // 处理注册授权 diff --git a/Yi.Ai.Vue3/src/components/ModelSelect/index.vue b/Yi.Ai.Vue3/src/components/ModelSelect/index.vue index ddadb148..ce697df9 100644 --- a/Yi.Ai.Vue3/src/components/ModelSelect/index.vue +++ b/Yi.Ai.Vue3/src/components/ModelSelect/index.vue @@ -2,22 +2,17 @@ - + -import { storeToRefs } from 'pinia' -import { ElMessage } from 'element-plus' -import { useRouter } from 'vue-router' -import type { Activity, Announcement } from '@/api' -import { useAnnouncementStore } from '@/stores' -import type { CloseType } from '@/stores/modules/announcement' +import type { AnnouncementLogDto } from '@/api'; +import type { CloseType } from '@/stores/modules/announcement'; +import { getSystemAnnouncements } from '@/api'; +import { ElMessage } from 'element-plus'; +import { storeToRefs } from 'pinia'; +import { useAnnouncementStore } from '@/stores'; -const router = useRouter() -const announcementStore = useAnnouncementStore() +const announcementStore = useAnnouncementStore(); +const isLoadingData = ref(false); -const activeTab = ref('activity') +const activeTab = ref('activity'); // 窗口宽度响应式状态 -const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1920) +const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1920); // 监听窗口大小变化 onMounted(() => { const handleResize = () => { - windowWidth.value = window.innerWidth - } - window.addEventListener('resize', handleResize) + windowWidth.value = window.innerWidth; + }; + window.addEventListener('resize', handleResize); onUnmounted(() => { - window.removeEventListener('resize', handleResize) - }) -}) + window.removeEventListener('resize', handleResize); + }); +}); // 响应式弹窗宽度 const dialogWidth = computed(() => { - if (windowWidth.value < 768) return '95%' - if (windowWidth.value < 1024) return '90%' - return '700px' -}) + if (windowWidth.value < 768) + return '95%'; + if (windowWidth.value < 1024) + return '90%'; + return '700px'; +}); // 从store获取数据 -const { carousels, activities, announcements, isDialogVisible } = storeToRefs(announcementStore) +const { announcements, isDialogVisible } = storeToRefs(announcementStore); -// 分离最新公告和历史公告 -const latestAnnouncements = computed(() => - announcements.value.filter(a => a.type === 'latest'), -) -const historyAnnouncements = computed(() => - announcements.value.filter(a => a.type === 'history'), -) +// 分离活动和系统公告 +const activities = computed(() => { + if (!Array.isArray(announcements.value)) + return []; + return announcements.value.filter(a => a.type === 'Activity'); +}); +const systemAnnouncements = computed(() => { + if (!Array.isArray(announcements.value)) + return []; + return announcements.value.filter(a => a.type === 'System'); +}); // 处理关闭弹窗 function handleClose(type: CloseType) { - announcementStore.closeDialog(type) + announcementStore.closeDialog(type); const messages = { - today: '今日内不再显示', - week: '本周内不再显示', + today: '一周内不再显示', permanent: '公告已关闭', - } + }; - ElMessage.success(messages[type]) -} - -// 查看活动详情 -function viewActivityDetail(activity: Activity) { - router.push({ - name: 'activityDetail', - params: { id: activity.id }, - }) - announcementStore.isDialogVisible = false -} - -// 查看公告详情 -function viewAnnouncementDetail(announcement: Announcement) { - router.push({ - name: 'announcementDetail', - params: { id: announcement.id }, - }) - announcementStore.isDialogVisible = false + ElMessage.success(messages[type]); } // 格式化时间 function formatTime(time: string) { - const date = new Date(time) - return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` + const date = new Date(time); + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } + +// 判断活动状态 +function getActivityStatus(activity: AnnouncementLogDto): 'active' | 'expired' { + if (!activity.endTime) + return 'active'; + const now = new Date(); + const endTime = new Date(activity.endTime); + return now > endTime ? 'expired' : 'active'; +} + +// 监听弹窗显示状态,每次打开时从后端获取最新数据 +watch(isDialogVisible, async (newValue) => { + if (newValue) { + // 弹窗打开时,从后端获取最新数据 + isLoadingData.value = true; + try { + const res = await getSystemAnnouncements(); + if (res && res.data && Array.isArray(res.data)) { + announcementStore.setAnnouncementData(res.data); + } + else { + announcementStore.setAnnouncementData([]); + } + } + catch (error) { + console.error('获取系统公告失败:', error); + ElMessage.error('获取公告数据失败,请稍后重试'); + announcementStore.setAnnouncementData([]); + } + finally { + isLoadingData.value = false; + } + } +}); @@ -86,70 +107,80 @@ function formatTime(time: string) { v-model="isDialogVisible" title="系统公告" :width="dialogWidth" - :close-on-click-modal="false" + :close-on-click-modal="true" class="announcement-dialog" > - + - - - - - - {{ item.title }} - - - - - + - - {{ activity.title }} - - 进行中 - - - 已结束 - + + + + + + + 进行中 + + + 已结束 + + - {{ activity.description }} - + @@ -158,70 +189,45 @@ function formatTime(time: string) { - - - 最新公告 - - - - - - - {{ announcement.title }} - - {{ formatTime(announcement.publishTime) }} - - {{ announcement.content.substring(0, 100) }}... - - 查看详情 - - - - - - - 历史公告 + + - - {{ announcement.title }} - {{ announcement.content.substring(0, 80) }}... - - 查看详情 - + + + + {{ announcement.title }} + + + + + + + {{ line }} + + + - +
{{ activity.description }}
{{ announcement.content.substring(0, 100) }}...
{{ announcement.content.substring(0, 80) }}...
+ {{ line }} +