diff --git a/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Extensions/AccessLogExtensions.cs b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Extensions/AccessLogExtensions.cs new file mode 100644 index 00000000..54387205 --- /dev/null +++ b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Extensions/AccessLogExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Yi.Framework.Bbs.Application.Extensions; + +public static class AccessLogExtensions +{ + public static IApplicationBuilder UseAccessLog(this IApplicationBuilder app) + { + app.UseMiddleware(); + return app; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Extensions/AccessLogMiddleware.cs b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Extensions/AccessLogMiddleware.cs new file mode 100644 index 00000000..373dcf7b --- /dev/null +++ b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Extensions/AccessLogMiddleware.cs @@ -0,0 +1,109 @@ +using Microsoft.AspNetCore.Http; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Yi.Framework.Bbs.Domain.Entities; +using Yi.Framework.Bbs.Domain.Shared.Caches; +using Yi.Framework.Bbs.Domain.Shared.Enums; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace Yi.Framework.Bbs.Application.Extensions; + +/// +/// 访问日志中间件 +/// 并发最高,采用缓存,默认10分钟才会真正操作一次数据库 +/// +/// 需考虑一致性问题,又不能上锁影响性能 +/// +public class AccessLogMiddleware : IMiddleware, ITransientDependency +{ + private readonly IDistributedCache _accessLogCache; + private readonly ISqlSugarRepository _repository; + + public AccessLogMiddleware(IDistributedCache accessLogCache, + ISqlSugarRepository repository) + { + _accessLogCache = accessLogCache; + _repository = repository; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + await next(context); + + //获取缓存 + var cache = await _accessLogCache.GetAsync(AccessLogCacheConst.Key); + + //当前缓存需赋的值 + long currentNumber = 1; + //最后插入的时间 + DateTime lastInsertTime = DateTime.Now; + //cahce是空,创建缓存,当前数量从数据库中获取 + //不等于空,如果大于当前天,插入数据库(间隔超过10分钟,也插入) + + //获取当前访问数量 + //没有缓存 + if (cache is null) + { + //获取数据库今天最后最后一条数据 + var currentDayEntity = await GetDateAccessLogEntityAsync(); + + //数据库有数据,以数据库数据为准+1 + if (currentDayEntity is not null) + { + currentNumber = currentDayEntity.Number + 1; + } + //数据库没有数据,就是默认的1,重新开始 + } + else + { + //有缓存,根据根据缓存的值来 + currentNumber = cache.Number + 1; + lastInsertTime = cache.LastInsertTime; + + //数据库持久化 + //缓存的日期大于当天的日期,插入到数据库,同时缓存变成0,重新开始统计 + if (cache.LastModificationTime.Date > DateTime.Today) + { + await _repository.InsertAsync(new AccessLogAggregateRoot() + { AccessLogType = AccessLogTypeEnum.Request, Number = currentNumber }); + currentNumber = 0; + } + else + { + if (cache.LastInsertTime <= DateTime.Now - TimeSpan.FromMinutes(10)) + { + //这里还需要判断数据库是否有值,不能之间更新 + + //缓存的时间大于当前数据库时间10分钟之后,更新(减少数据库交互,10分钟才更新一次) + var currentDayEntity = await GetDateAccessLogEntityAsync(); + if (currentDayEntity is null) + { + await _repository.InsertAsync(new AccessLogAggregateRoot() + { AccessLogType = AccessLogTypeEnum.Request, Number = currentNumber }); + } + await _repository.UpdateAsync(currentDayEntity); + } + } + } + + + //覆盖缓存 + var newCache = new AccessLogCacheItem(currentNumber) { LastInsertTime = lastInsertTime }; + await _accessLogCache.SetAsync(AccessLogCacheConst.Key, newCache); + } + + /// + /// 获取今天的统计 + /// + /// + private async Task GetDateAccessLogEntityAsync() + { + //获取数据库今天最后最后一条数据 + var currentDayEntity = await _repository._DbQueryable + .Where(x => x.AccessLogType == AccessLogTypeEnum.Request) + .Where(x => x.CreationTime == DateTime.Today) + .OrderByDescending(x => x.CreationTime) + .FirstAsync(); + return currentDayEntity; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Services/Analyses/AccessLogService.cs b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Services/Analyses/AccessLogService.cs index b99e2980..53dd4a07 100644 --- a/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Services/Analyses/AccessLogService.cs +++ b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Application/Services/Analyses/AccessLogService.cs @@ -4,6 +4,7 @@ using Volo.Abp.Application.Services; using Yi.Framework.Bbs.Application.Contracts.Dtos.AccessLog; using Yi.Framework.Bbs.Application.Contracts.IServices; using Yi.Framework.Bbs.Domain.Entities; +using Yi.Framework.Bbs.Domain.Shared.Enums; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.Bbs.Application.Services @@ -11,7 +12,11 @@ namespace Yi.Framework.Bbs.Application.Services public class AccessLogService : ApplicationService, IAccessLogService { private readonly ISqlSugarRepository _repository; - public AccessLogService(ISqlSugarRepository repository) { _repository = repository; } + + public AccessLogService(ISqlSugarRepository repository) + { + _repository = repository; + } public DateTime GetWeekFirst() { @@ -45,16 +50,15 @@ namespace Yi.Framework.Bbs.Application.Services } - - /// /// 获取全部访问流量(3个月) /// + /// /// - public async Task> Get() + public async Task> GetListAsync([FromQuery] AccessLogTypeEnum AccessLogType) { - var entities = await _repository._DbQueryable - .Where(x=>x.CreationTime>=DateTime.Now.AddMonths(-3)) + var entities = await _repository._DbQueryable.Where(x => x.AccessLogType == AccessLogType) + .Where(x => x.CreationTime >= DateTime.Now.AddMonths(-3)) .OrderBy(x => x.CreationTime).ToListAsync(); var output = entities.Adapt>(); output?.ForEach(x => x.CreationTime = x.CreationTime.Date); @@ -62,7 +66,7 @@ namespace Yi.Framework.Bbs.Application.Services } /// - /// 触发 + /// 首页点击触发 /// /// [HttpPost("access-log")] @@ -77,17 +81,20 @@ namespace Yi.Framework.Bbs.Application.Services } else { - await _repository._Db.Updateable().SetColumns(it => it.Number == it.Number + 1).Where(it => it.Id == last.Id).ExecuteCommandAsync(); + await _repository._Db.Updateable().SetColumns(it => it.Number == it.Number + 1) + .Where(it => it.Id == last.Id).ExecuteCommandAsync(); } } /// - /// 获取当前周数据 + /// 获取当前周首页点击数据 /// /// public async Task GetWeekAsync() { - var lastSeven = await _repository._DbQueryable.OrderByDescending(x => x.CreationTime).ToPageListAsync(1, 7); + var lastSeven = await _repository._DbQueryable + .Where(x => x.AccessLogType == AccessLogTypeEnum.HomeClick) + .OrderByDescending(x => x.CreationTime).ToPageListAsync(1, 7); return WeekTimeHandler(lastSeven.ToArray()); } @@ -99,7 +106,8 @@ namespace Yi.Framework.Bbs.Application.Services /// private AccessLogDto[] WeekTimeHandler(AccessLogAggregateRoot[] data) { - data = data.Where(x => x.CreationTime >= GetWeekFirst()).OrderByDescending(x => x.CreationTime).DistinctBy(x => x.CreationTime.DayOfWeek).ToArray(); + data = data.Where(x => x.CreationTime >= GetWeekFirst()).OrderByDescending(x => x.CreationTime) + .DistinctBy(x => x.CreationTime.DayOfWeek).ToArray(); Dictionary processedData = new Dictionary(); @@ -117,8 +125,8 @@ namespace Yi.Framework.Bbs.Application.Services // 如果当天有数据,则更新字典中的值为对应的Number var sss = data.Adapt(); processedData[dayOfWeek] = item.Adapt(); - } + var result = processedData.Values.ToList(); //此时的时间是周日-周一-周二,需要处理 @@ -128,8 +136,5 @@ namespace Yi.Framework.Bbs.Application.Services return result.ToArray(); } - - - } -} +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain.Shared/Caches/AccessLogCacheItem.cs b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain.Shared/Caches/AccessLogCacheItem.cs new file mode 100644 index 00000000..2c2f61ad --- /dev/null +++ b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain.Shared/Caches/AccessLogCacheItem.cs @@ -0,0 +1,20 @@ +namespace Yi.Framework.Bbs.Domain.Shared.Caches; + +public class AccessLogCacheItem +{ + public AccessLogCacheItem(long number) + { + Number = number; + } + + public long Number { get; set; } + public DateTime LastModificationTime { get; set; }=DateTime.Now; + + public DateTime LastInsertTime { get; set; }=DateTime.Now; +} + +public class AccessLogCacheConst +{ + + public const string Key = "AccessLog"; +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain.Shared/Enums/AccessLogTypeEnum.cs b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain.Shared/Enums/AccessLogTypeEnum.cs new file mode 100644 index 00000000..04344ebd --- /dev/null +++ b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain.Shared/Enums/AccessLogTypeEnum.cs @@ -0,0 +1,14 @@ +namespace Yi.Framework.Bbs.Domain.Shared.Enums; + +public enum AccessLogTypeEnum +{ + /// + /// 首页点击量 + /// + HomeClick, + + /// + /// 接口请求量 + /// + Request +} \ No newline at end of file diff --git a/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain/Entities/AccessLogAggregateRoot.cs b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain/Entities/AccessLogAggregateRoot.cs index 4eb55a89..b0a0c0ee 100644 --- a/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain/Entities/AccessLogAggregateRoot.cs +++ b/Yi.Abp.Net8/module/bbs/Yi.Framework.Bbs.Domain/Entities/AccessLogAggregateRoot.cs @@ -1,16 +1,19 @@ using SqlSugar; using Volo.Abp.Auditing; using Volo.Abp.Domain.Entities; +using Yi.Framework.Bbs.Domain.Shared.Enums; namespace Yi.Framework.Bbs.Domain.Entities { [SugarTable("AccessLog")] - public class AccessLogAggregateRoot : AggregateRoot, IHasModificationTime, IHasCreationTime + public class AccessLogAggregateRoot : AggregateRoot, IHasCreationTime,IHasModificationTime { [SugarColumn(ColumnName = "Id", IsPrimaryKey = true)] public override Guid Id { get; protected set; } public long Number { get; set; } - public DateTime? LastModificationTime { get; set; } + + public AccessLogTypeEnum AccessLogType { get; set; } public DateTime CreationTime { get; set; } + public DateTime? LastModificationTime { get; set; } } } diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index f9557819..71b21ed9 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -26,6 +26,7 @@ using Yi.Framework.AspNetCore.Authentication.OAuth.QQ; using Yi.Framework.AspNetCore.Microsoft.AspNetCore.Builder; using Yi.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection; using Yi.Framework.Bbs.Application; +using Yi.Framework.Bbs.Application.Extensions; using Yi.Framework.ChatHub.Application; using Yi.Framework.CodeGen.Application; using Yi.Framework.Rbac.Application; @@ -39,8 +40,6 @@ namespace Yi.Abp.Web [DependsOn( typeof(YiAbpSqlSugarCoreModule), typeof(YiAbpApplicationModule), - - typeof(AbpAspNetCoreMultiTenancyModule), typeof(AbpAspNetCoreMvcModule), typeof(AbpAutofacModule), @@ -50,11 +49,11 @@ namespace Yi.Abp.Web typeof(AbpAspNetCoreAuthenticationJwtBearerModule), typeof(YiFrameworkAspNetCoreModule), typeof(YiFrameworkAspNetCoreAuthenticationOAuthModule) - - )] + )] public class YiAbpWebModule : AbpModule { private const string DefaultCorsPolicyName = "Default"; + public override Task ConfigureServicesAsync(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); @@ -70,20 +69,23 @@ namespace Yi.Abp.Web }); //配置错误处理显示详情 - Configure(options => - { - options.SendExceptionsDetailsToClients = true; - }); - + Configure(options => { options.SendExceptionsDetailsToClients = true; }); + //动态Api Configure(options => { - options.ConventionalControllers.Create(typeof(YiAbpApplicationModule).Assembly, options => options.RemoteServiceName = "default"); - options.ConventionalControllers.Create(typeof(YiFrameworkRbacApplicationModule).Assembly, options => options.RemoteServiceName = "rbac"); - options.ConventionalControllers.Create(typeof(YiFrameworkBbsApplicationModule).Assembly, options => options.RemoteServiceName = "bbs"); - options.ConventionalControllers.Create(typeof(YiFrameworkChatHubApplicationModule).Assembly, options => options.RemoteServiceName = "chat-hub"); - options.ConventionalControllers.Create(typeof(YiFrameworkTenantManagementApplicationModule).Assembly, options => options.RemoteServiceName = "tenant-management"); - options.ConventionalControllers.Create(typeof(YiFrameworkCodeGenApplicationModule).Assembly, options => options.RemoteServiceName = "code-gen"); + options.ConventionalControllers.Create(typeof(YiAbpApplicationModule).Assembly, + options => options.RemoteServiceName = "default"); + options.ConventionalControllers.Create(typeof(YiFrameworkRbacApplicationModule).Assembly, + options => options.RemoteServiceName = "rbac"); + options.ConventionalControllers.Create(typeof(YiFrameworkBbsApplicationModule).Assembly, + options => options.RemoteServiceName = "bbs"); + options.ConventionalControllers.Create(typeof(YiFrameworkChatHubApplicationModule).Assembly, + options => options.RemoteServiceName = "chat-hub"); + options.ConventionalControllers.Create(typeof(YiFrameworkTenantManagementApplicationModule).Assembly, + options => options.RemoteServiceName = "tenant-management"); + options.ConventionalControllers.Create(typeof(YiFrameworkCodeGenApplicationModule).Assembly, + options => options.RemoteServiceName = "code-gen"); //统一前缀 options.ConventionalControllers.ConventionalControllerSettings.ForEach(x => x.RootPath = "api/app"); @@ -98,22 +100,20 @@ namespace Yi.Abp.Web //设置缓存不要过期,默认滑动20分钟 Configure(cacheOptions => - { - cacheOptions.GlobalCacheEntryOptions.SlidingExpiration = null; - //缓存key前缀 - cacheOptions.KeyPrefix = "Yi:"; - }); - - - Configure(options => { - options.AutoValidate = false; + cacheOptions.GlobalCacheEntryOptions.SlidingExpiration = null; + //缓存key前缀 + cacheOptions.KeyPrefix = "Yi:"; }); + + Configure(options => { options.AutoValidate = false; }); + //Swagger context.Services.AddYiSwaggerGen(options => { - options.SwaggerDoc("default", new OpenApiInfo { Title = "Yi.Framework.Abp", Version = "v1", Description = "集大成者" }); + options.SwaggerDoc("default", + new OpenApiInfo { Title = "Yi.Framework.Abp", Version = "v1", Description = "集大成者" }); }); //跨域 @@ -152,7 +152,7 @@ namespace Yi.Abp.Web //每60秒限制100个请求,滑块添加,分6段 service.AddRateLimiter(_ => { - _.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + _.RejectionStatusCode = StatusCodes.Status429TooManyRequests; _.OnRejected = (context, _) => { if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) @@ -160,6 +160,7 @@ namespace Yi.Abp.Web context.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo); } + context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; context.HttpContext.Response.WriteAsync("Too many requests. Please try again later."); @@ -168,20 +169,20 @@ namespace Yi.Abp.Web //全局使用,链式表达式 _.GlobalLimiter = PartitionedRateLimiter.CreateChained( - PartitionedRateLimiter.Create(httpContext => - { - var userAgent = httpContext.Request.Headers.UserAgent.ToString(); + PartitionedRateLimiter.Create(httpContext => + { + var userAgent = httpContext.Request.Headers.UserAgent.ToString(); - return RateLimitPartition.GetSlidingWindowLimiter - (userAgent, _ => - new SlidingWindowRateLimiterOptions - { - PermitLimit = 1000, - Window = TimeSpan.FromSeconds(60), - SegmentsPerWindow = 6, - QueueProcessingOrder = QueueProcessingOrder.OldestFirst - }); - })); + return RateLimitPartition.GetSlidingWindowLimiter + (userAgent, _ => + new SlidingWindowRateLimiterOptions + { + PermitLimit = 1000, + Window = TimeSpan.FromSeconds(60), + SegmentsPerWindow = 6, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst + }); + })); }); @@ -203,14 +204,15 @@ namespace Yi.Abp.Web options.Events = new JwtBearerEvents { OnMessageReceived = context => - { - var accessToken = context.Request.Query["access_token"]; - if (!string.IsNullOrEmpty(accessToken)) { - context.Token = accessToken; + var accessToken = context.Request.Query["access_token"]; + if (!string.IsNullOrEmpty(accessToken)) + { + context.Token = accessToken; + } + + return Task.CompletedTask; } - return Task.CompletedTask; - } }; }) .AddJwtBearer(TokenTypeConst.Refresh, options => @@ -221,37 +223,32 @@ namespace Yi.Abp.Web ValidateIssuerSigningKey = true, ValidIssuer = refreshJwtOptions.Issuer, ValidAudience = refreshJwtOptions.Audience, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(refreshJwtOptions.SecurityKey)) + IssuerSigningKey = + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(refreshJwtOptions.SecurityKey)) }; options.Events = new JwtBearerEvents { OnMessageReceived = context => - { - var refresh_token = context.Request.Headers["refresh_token"]; - if (!string.IsNullOrEmpty(refresh_token)) { - context.Token = refresh_token; + var refresh_token = context.Request.Headers["refresh_token"]; + if (!string.IsNullOrEmpty(refresh_token)) + { + context.Token = refresh_token; + return Task.CompletedTask; + } + + var refreshToken = context.Request.Query["refresh_token"]; + if (!string.IsNullOrEmpty(refreshToken)) + { + context.Token = refreshToken; + } + return Task.CompletedTask; } - var refreshToken = context.Request.Query["refresh_token"]; - if (!string.IsNullOrEmpty(refreshToken)) - { - context.Token = refreshToken; - } - - return Task.CompletedTask; - } }; - }) - .AddQQ(options => - { - configuration.GetSection("OAuth:QQ").Bind(options); - }) - .AddGitee(options => - { - configuration.GetSection("OAuth:Gitee").Bind(options); - }); + .AddQQ(options => { configuration.GetSection("OAuth:QQ").Bind(options); }) + .AddGitee(options => { configuration.GetSection("OAuth:Gitee").Bind(options); }); //授权 context.Services.AddAuthorization(); @@ -291,6 +288,9 @@ namespace Yi.Abp.Web //swagger app.UseYiSwagger(); + //流量访问统计,不启用 + //app.UseAccessLog(); + //请求处理 app.UseYiApiHandlinge(); @@ -317,4 +317,4 @@ namespace Yi.Abp.Web return Task.CompletedTask; } } -} +} \ No newline at end of file