diff --git a/src/CloudGaming/Api/CloudGaming.Api/Base/CloudGamingControllerBase.cs b/src/CloudGaming/Api/CloudGaming.Api/Base/CloudGamingControllerBase.cs index 6e76bfc..f688d9c 100644 --- a/src/CloudGaming/Api/CloudGaming.Api/Base/CloudGamingControllerBase.cs +++ b/src/CloudGaming/Api/CloudGaming.Api/Base/CloudGamingControllerBase.cs @@ -20,6 +20,5 @@ namespace CloudGaming.Api.Base /// 数据库使用 /// public IServiceProvider ServiceProvider { get; set; } = _serviceProvider; - } } diff --git a/src/CloudGaming/Api/CloudGaming.ExtApi/Base/CloudGamingControllerBase.cs b/src/CloudGaming/Api/CloudGaming.ExtApi/Base/CloudGamingControllerBase.cs new file mode 100644 index 0000000..5c0a2ab --- /dev/null +++ b/src/CloudGaming/Api/CloudGaming.ExtApi/Base/CloudGamingControllerBase.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; + +namespace CloudGaming.ExtApi.Base; + +/// +/// 基础父类 +/// +/// +[ApiController] +[Route("api/[controller]/[action]")] + +public class CloudGamingControllerBase(IServiceProvider _serviceProvider) : ControllerBase +{ + /// + /// 数据库使用 + /// + public IServiceProvider ServiceProvider { get; set; } = _serviceProvider; +} diff --git a/src/CloudGaming/Api/CloudGaming.ExtApi/CloudGaming.ExtApi.csproj b/src/CloudGaming/Api/CloudGaming.ExtApi/CloudGaming.ExtApi.csproj index c2219de..81b2116 100644 --- a/src/CloudGaming/Api/CloudGaming.ExtApi/CloudGaming.ExtApi.csproj +++ b/src/CloudGaming/Api/CloudGaming.ExtApi/CloudGaming.ExtApi.csproj @@ -6,6 +6,7 @@ enable Linux ..\.. + True diff --git a/src/CloudGaming/Api/CloudGaming.ExtApi/Controllers/MonitorController.cs b/src/CloudGaming/Api/CloudGaming.ExtApi/Controllers/MonitorController.cs index 790d167..6df0653 100644 --- a/src/CloudGaming/Api/CloudGaming.ExtApi/Controllers/MonitorController.cs +++ b/src/CloudGaming/Api/CloudGaming.ExtApi/Controllers/MonitorController.cs @@ -1,11 +1,29 @@ +using CloudGaming.AppConfigModel; +using CloudGaming.Code.Monitor; +using CloudGaming.DtoModel.Other; +using CloudGaming.ExtApi.Base; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace CloudGaming.ExtApi.Controllers +namespace CloudGaming.ExtApi.Controllers; + + +/// +/// 监控数据 +/// +public class MonitorController : CloudGamingControllerBase { - [Route("api/[controller]")] - [ApiController] - public class MonitorController : ControllerBase + public MonitorController(IServiceProvider _serviceProvider) : base(_serviceProvider) { } + /// + /// 获取监控数据 + /// + /// + [HttpGet] + public AppBasicStatistics GetAppMonitorInfo() + { + return new MonitorBLL(ServiceProvider).GetAppMonitorInfo(); + } } diff --git a/src/CloudGaming/Code/CloudGaming.Code/Account/AccountExtend.cs b/src/CloudGaming/Code/CloudGaming.Code/Account/AccountExtend.cs index 17243ab..390d879 100644 --- a/src/CloudGaming/Code/CloudGaming.Code/Account/AccountExtend.cs +++ b/src/CloudGaming/Code/CloudGaming.Code/Account/AccountExtend.cs @@ -82,6 +82,11 @@ namespace CloudGaming.Code.Account return 0; } + /// + /// + /// + public static string UserInfoRedisKeyPrefix { get => "user:userInfo"; } + /// /// 用户缓存的key /// @@ -89,7 +94,7 @@ namespace CloudGaming.Code.Account /// public static string GetUserInfoRedisKey(int userId) { - return $"user:userInfo:{userId}"; + return $"{UserInfoRedisKeyPrefix}:{userId}"; } /// diff --git a/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AppJobBase.cs b/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AppJobBase.cs new file mode 100644 index 0000000..1b09940 --- /dev/null +++ b/src/CloudGaming/Code/CloudGaming.Code/AppExtend/AppJobBase.cs @@ -0,0 +1,76 @@ +using CloudGaming.Code.Monitor; +using CloudGaming.DtoModel.Other; + +using Quartz; + +using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CloudGaming.Code.AppExtend +{ + + /// + /// 定时使用 + /// + public abstract class AppJobBase : IJob + { + /// + /// + /// + protected readonly IServiceScopeFactory scopeFactory; + public AppJobBase(IServiceScopeFactory scopeFactory) + { + this.scopeFactory = scopeFactory; + } + + public Task Execute(IJobExecutionContext context) + { + if (AppConfigurationExtend.AppConfigs != null && AppConfigurationExtend.AppConfigs.Count > 0) + { + var appConfigs = AppConfigurationExtend.AppConfigs.Where(it => it.Key != "default") + .Select(it => it.Value) + .ToList(); + + // 为每个项目启动一个独立的任务 + foreach (var appConfig in appConfigs) + { + + + _ = Task.Run(() => AppConfigProcessLoop(appConfig)); + } + } + return Task.CompletedTask; + } + /// + /// + /// + /// + /// + public async Task AppConfigProcessLoop(AppConfig appConfig) + { + AppMonitorInfo appMonitorInfo = MonitorExtend.AppMonitorConfigs.GetOrAdd(appConfig.Identifier, new AppMonitorInfo()); + using var scope = scopeFactory.CreateScope(); + var scopedProvider = scope.ServiceProvider; + var app = scopedProvider.GetRequiredService(); + appConfig.ToAppConfig(app); + CloudGamingBase cloudGamingBase = new CloudGamingBase(scopedProvider); + await AppConfigProcessLoop(appConfig, appMonitorInfo, scopedProvider, cloudGamingBase); + } + + /// + /// + /// + /// + /// + /// + /// + /// + public abstract Task AppConfigProcessLoop(AppConfig appConfig, AppMonitorInfo appMonitorInfo, IServiceProvider serviceProvider, CloudGamingBase cloudGamingBase); + + } +} diff --git a/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorBLL.cs b/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorBLL.cs new file mode 100644 index 0000000..fc6cc6f --- /dev/null +++ b/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorBLL.cs @@ -0,0 +1,28 @@ +using CloudGaming.DtoModel.Other; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CloudGaming.Code.Monitor; + +/// +/// 监控类 +/// +public class MonitorBLL : CloudGamingBase +{ + public MonitorBLL(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + /// + /// 获取监控数据 + /// + /// + public AppBasicStatistics GetAppMonitorInfo() + { + return AppConfig.GetAppMonitorInfo().ToAppBasicStatistics(); + } +} diff --git a/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorExtend.cs b/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorExtend.cs index c33db25..9307d83 100644 --- a/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorExtend.cs +++ b/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorExtend.cs @@ -13,6 +13,8 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.CompilerServices; +using static SKIT.FlurlHttpClient.Wechat.TenpayV3.ExtendedSDK.Global.Models.QueryPartnerRefundsResponse.Types; +using HuanMeng.DotNetCore.QuartzExtend; namespace CloudGaming.Code.Monitor; @@ -27,27 +29,28 @@ public static class MonitorExtend public static ConcurrentDictionary AppMonitorConfigs { get; set; } = new ConcurrentDictionary(); + /// + /// 添加监控服务 + /// + /// + /// public static WebApplicationBuilder AddMonitorConfig(this WebApplicationBuilder builder) { - // 添加 Quartz 配置 - builder.Services.AddQuartz(q => - { - // 注册作业和触发器 - var jobKey = new JobKey("MonitorProcessor"); - q.AddJob(opts => opts.WithIdentity(jobKey)); - - q.AddTrigger(opts => opts - .ForJob(jobKey) - .WithIdentity("monitorTrigger") - .StartNow() - .WithSimpleSchedule(x => x.WithIntervalInSeconds(5).RepeatForever())); - }); - - // 添加 Quartz 托管服务 - builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); - + builder.Services.AddQuartzWithAttributes(typeof(MonitorExtend).Assembly); return builder; } + + /// + /// 获取app监控数据 + /// + /// + /// + public static AppMonitorInfo GetAppMonitorInfo(this AppConfig appConfig) + { + AppMonitorInfo appMonitorInfo = MonitorExtend.AppMonitorConfigs.GetOrAdd(appConfig.Identifier, new AppMonitorInfo()); + return appMonitorInfo; + } + /// /// /// @@ -57,12 +60,12 @@ public static class MonitorExtend { return new AppBasicStatistics() { - CurrentOnlineUsers= appMonitorInfo.CurrentOnlineUsers, + CurrentOnlineUsers = appMonitorInfo.CurrentOnlineUsers, CurrentPlayingUsers = appMonitorInfo.CurrentPlayingUsers, CurrentQueuedUsers = appMonitorInfo.CurrentQueuedUsers, TodayIntendedOrders = appMonitorInfo.TodayIntendedOrders, TodayLoggedInUsers = appMonitorInfo.TodayLoggedInUsers, - TodayPaidOrders= appMonitorInfo.TodayPaidOrders, + TodayPaidOrders = appMonitorInfo.TodayPaidOrders, TodayRechargeAmount = appMonitorInfo.TodayRechargeAmount, TodayRegisteredUsers = appMonitorInfo.TodayRegisteredUsers }; diff --git a/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorProcessor.cs b/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorProcessor.cs index af5c609..3a242cd 100644 --- a/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorProcessor.cs +++ b/src/CloudGaming/Code/CloudGaming.Code/Monitor/MonitorProcessor.cs @@ -1,6 +1,9 @@ +using CloudGaming.Code.Account; using CloudGaming.DtoModel.Other; using CloudGaming.DtoModel.PlayGame; +using HuanMeng.DotNetCore.QuartzExtend; + using Newtonsoft.Json; using Quartz; @@ -15,53 +18,22 @@ namespace CloudGaming.Code.Monitor; /// -/// 定时监控 +/// 基础信息定时监控 /// -public class MonitorProcessor : IJob +[QuartzTrigger("MonitorProcessor", "3/10 * * * * ?")] +public class MonitorProcessor : AppJobBase { - private readonly IServiceProvider serviceProvider; - private readonly IServiceScopeFactory scopeFactory; - public MonitorProcessor(IServiceScopeFactory scopeFactory) + public MonitorProcessor(IServiceScopeFactory scopeFactory) : base(scopeFactory) { - this.scopeFactory = scopeFactory; } - public Task Execute(IJobExecutionContext context) - { - if (AppConfigurationExtend.AppConfigs != null && AppConfigurationExtend.AppConfigs.Count > 0) - { - var appConfigs = AppConfigurationExtend.AppConfigs.Where(it => it.Key != "default") - .Select(it => it.Value) - .ToList(); - - // 为每个项目启动一个独立的任务 - foreach (var appConfig in appConfigs) - { - - AppMonitorInfo appMonitorInfo = MonitorExtend.AppMonitorConfigs.GetOrAdd(appConfig.Identifier, new AppMonitorInfo()); - _ = Task.Run(() => ProcessAppConfigLoop(appConfig, appMonitorInfo)); - } - } - - //throw new NotImplementedException(); - return Task.CompletedTask; - } /// /// 单个项目的处理循环 /// - public async Task ProcessAppConfigLoop(AppConfig appConfig, AppMonitorInfo appMonitorInfo) + public override async Task AppConfigProcessLoop(AppConfig appConfig, AppMonitorInfo appMonitorInfo, IServiceProvider serviceProvider, CloudGamingBase cloudGamingBase) { - var sw = Stopwatch.StartNew(); - using var scope = scopeFactory.CreateScope(); - var scopedProvider = scope.ServiceProvider; int day = int.Parse(DateTime.Now.ToString("yyyyMMdd")); var nowDay = DateOnly.FromDateTime(DateTime.Now); - - - var app = scopedProvider.GetRequiredService(); - appConfig.ToAppConfig(app); - CloudGamingBase cloudGamingBase = new CloudGamingBase(scopedProvider); - //今日登录人数 var todayLoggedInUsers = await cloudGamingBase.Dao.DaoExt.Context.T_User_LoginDay_Log.Where(it => it.CreateTimeDay == day).CountAsync(); //今日注册人数 @@ -71,39 +43,24 @@ public class MonitorProcessor : IJob //今日支付订单 var todayPaidOrders = await cloudGamingBase.Dao.DaoUser.Context.T_User_Order.Where(it => it.PaymentDay == nowDay).CountAsync(); //今日支付金额 - var todayRechargeAmount =await cloudGamingBase.Dao.DaoUser.Context.T_User_Order.Where(it => it.PaymentDay == nowDay).SumAsync(it => (decimal?)it.TotalPrice); + var todayRechargeAmount = await cloudGamingBase.Dao.DaoUser.Context.T_User_Order.Where(it => it.PaymentDay == nowDay).SumAsync(it => (decimal?)it.TotalPrice); // 查询游戏数据 var playGameStatusStatistics = appMonitorInfo.PlayGameStatusStatistics(); //排队数据 - int currentQueuedUsers = 0; - if (playGameStatusStatistics.TryGetValue(PlayGameStatus.排队中, out var _currentQueuedUsers)) - { - currentQueuedUsers += _currentQueuedUsers; - } + int currentQueuedUsers = playGameStatusStatistics.GetValueOrDefault(PlayGameStatus.排队中); //开始游戏 - int currentPlayingUsers = 0; - if (playGameStatusStatistics.TryGetValue(PlayGameStatus.开始游戏, out var _currentPlayingUsers)) - { - currentPlayingUsers += _currentPlayingUsers; - } - if (playGameStatusStatistics.TryGetValue(PlayGameStatus.游戏中, out var _currentPlayingUsers1)) - { - currentPlayingUsers += _currentPlayingUsers1; - } - //appMonitorInfo.CurrentQueuedUsers= - sw.Stop(); + int currentPlayingUsers = playGameStatusStatistics.GetValueOrDefault(PlayGameStatus.开始游戏) + playGameStatusStatistics.GetValueOrDefault(PlayGameStatus.游戏中); + //查询在线人数 + //ScanKeysCount user:userInfo:* + var currentOnlineUsers = await cloudGamingBase.RedisServerCache.ScanKeysCountAsync($"{AccountExtend.UserInfoRedisKeyPrefix}:*"); + appMonitorInfo.TodayLoggedInUsers = todayLoggedInUsers; appMonitorInfo.TodayRegisteredUsers = todayRegisteredUsers; appMonitorInfo.TodayIntendedOrders = todayIntendedOrders; appMonitorInfo.TodayPaidOrders = todayPaidOrders; appMonitorInfo.TodayRechargeAmount = todayRechargeAmount ?? 0; - appMonitorInfo.CurrentOnlineUsers = 0; + appMonitorInfo.CurrentOnlineUsers = currentOnlineUsers; appMonitorInfo.CurrentQueuedUsers = currentQueuedUsers; appMonitorInfo.CurrentPlayingUsers = currentPlayingUsers; - - var t = appMonitorInfo.ToAppBasicStatistics(); - Console.WriteLine(JsonConvert.SerializeObject(t)); - Console.WriteLine($"消耗{sw.Elapsed.TotalMilliseconds.ToString("0.###")}"); - } } \ No newline at end of file diff --git a/src/CloudGaming/Code/CloudGaming.Code/Monitor/UserMonitorProcessor.cs b/src/CloudGaming/Code/CloudGaming.Code/Monitor/UserMonitorProcessor.cs new file mode 100644 index 0000000..291511b --- /dev/null +++ b/src/CloudGaming/Code/CloudGaming.Code/Monitor/UserMonitorProcessor.cs @@ -0,0 +1,119 @@ + +using CloudGaming.DtoModel.Other; + +using HuanMeng.DotNetCore.QuartzExtend; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CloudGaming.Code.Monitor; + +/// +/// 用户定时类 +/// +[QuartzTrigger("UserMonitorProcessor", "* 15 4 * * ?")] +public class UserMonitorProcessor : AppJobBase +{ + public UserMonitorProcessor(IServiceScopeFactory scopeFactory) : base(scopeFactory) + { + } + + public override async Task AppConfigProcessLoop(AppConfig appConfig, AppMonitorInfo appMonitorInfo, IServiceProvider serviceProvider, CloudGamingBase cloudGamingBase) + { + var dao = cloudGamingBase.Dao; + var now = DateTime.Now.AddDays(-1); + int day = int.Parse(now.ToString("yyyyMMdd")); + var nowDay = DateOnly.FromDateTime(now); + + //今日登录人数 + var todayLoggedInUsers = await cloudGamingBase.Dao.DaoExt.Context.T_User_LoginDay_Log.Where(it => it.CreateTimeDay == day) + .GroupBy(it => it.Channel ?? "") + .ToDictionaryAsync(it => it.Key, it => it.Count()); + //今日注册人数 + var todayRegisteredUsers = await cloudGamingBase.Dao.DaoExt.Context.T_User_LoginDay_Log.Where(it => it.CreateTimeDay == day && it.IsNew) + .GroupBy(it => it.Channel ?? "") + .ToDictionaryAsync(it => it.Key, it => it.Count()); + //今日活跃人数 + var todayActionUsers = await cloudGamingBase.Dao.DaoExt.Context.T_User_LoginDay_Log.Where(it => it.CreateTimeDay == day && it.LogInDay > 1) + .GroupBy(it => it.Channel ?? "") + .ToDictionaryAsync(it => it.Key, it => it.Count()); + //数据库中的数据 + var userStatistics = await dao.DaoExt.Context.T_Statistics_User.Where(it => it.LoginDay == day).ToDictionaryAsync(it => it.Channel ?? "");//.ToListAsync(); + //登录 + if (todayLoggedInUsers != null) + { + foreach (var item in todayLoggedInUsers) + { + if (!userStatistics.TryGetValue(item.Key, out var statisticsUser)) + { + statisticsUser = new T_Statistics_User() + { + ActiveCount = 0, + Channel = item.Key, + CreatedAt = DateTime.Now, + LoginCount = 0, + RegistrCount = 0, + LoginDate = nowDay, + LoginDay = day, + UpdatedAt = DateTime.Now, + }; + await dao.DaoExt.Context.T_Statistics_User.AddAsync(statisticsUser); + userStatistics.Add(item.Key, statisticsUser); + } + statisticsUser.LoginDay = item.Value; + } + } + //注册 + if (todayRegisteredUsers != null) + { + foreach (var item in todayRegisteredUsers) + { + if (!userStatistics.TryGetValue(item.Key, out var statisticsUser)) + { + statisticsUser = new T_Statistics_User() + { + ActiveCount = 0, + Channel = item.Key, + CreatedAt = DateTime.Now, + LoginCount = 0, + RegistrCount = 0, + LoginDate = nowDay, + LoginDay = day, + UpdatedAt = DateTime.Now, + }; + await dao.DaoExt.Context.T_Statistics_User.AddAsync(statisticsUser); + userStatistics.Add(item.Key, statisticsUser); + } + statisticsUser.RegistrCount = item.Value; + } + } + //活跃 + if (todayActionUsers != null) + { + foreach (var item in todayActionUsers) + { + if (!userStatistics.TryGetValue(item.Key, out var statisticsUser)) + { + statisticsUser = new T_Statistics_User() + { + ActiveCount = 0, + Channel = item.Key, + CreatedAt = DateTime.Now, + LoginCount = 0, + RegistrCount = 0, + LoginDate = nowDay, + LoginDay = day, + UpdatedAt = DateTime.Now, + }; + await dao.DaoExt.Context.T_Statistics_User.AddAsync(statisticsUser); + userStatistics.Add(item.Key, statisticsUser); + } + statisticsUser.ActiveCount = item.Value; + } + } + await dao.DaoExt.Context.SaveChangesAsync(); + } +} diff --git a/src/CloudGaming/Code/CloudGaming.Code/Other/MonitorBLL.cs b/src/CloudGaming/Code/CloudGaming.Code/Other/MonitorBLL.cs deleted file mode 100644 index 1582713..0000000 --- a/src/CloudGaming/Code/CloudGaming.Code/Other/MonitorBLL.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace CloudGaming.Code.Other -{ - /// - /// 监控类 - /// - public class MonitorBLL : CloudGamingBase - { - public MonitorBLL(IServiceProvider serviceProvider) : base(serviceProvider) - { - } - } -} diff --git a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/CloudGamingCBTContext.cs b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/CloudGamingCBTContext.cs index c9da070..40aedb5 100644 --- a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/CloudGamingCBTContext.cs +++ b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/CloudGamingCBTContext.cs @@ -59,6 +59,21 @@ public partial class CloudGamingCBTContext : DbContext /// public virtual DbSet T_Sms_Log { get; set; } + /// + /// 游戏统计数据 + /// + public virtual DbSet T_Statistics_Game { get; set; } + + /// + /// 订单统计表 + /// + public virtual DbSet T_Statistics_Order { get; set; } + + /// + /// 用户统计表 + /// + public virtual DbSet T_Statistics_User { get; set; } + /// /// 用户登录日志表,每天每个用户只有一条数据 /// @@ -185,6 +200,75 @@ public partial class CloudGamingCBTContext : DbContext }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("_copy_1"); + + entity.ToTable(tb => tb.HasComment("游戏统计数据")); + + entity.Property(e => e.Channel) + .HasMaxLength(10) + .HasComment("渠道号"); + entity.Property(e => e.CreatedAt) + .HasComment("创建时间") + .HasColumnType("datetime"); + entity.Property(e => e.LoginDate).HasComment("日期"); + entity.Property(e => e.LoginDay).HasComment("天"); + entity.Property(e => e.PlayGameCount).HasComment("用户玩游戏人数"); + entity.Property(e => e.PlayGameTimeCount).HasComment("用户玩游戏时长"); + entity.Property(e => e.StartGameCount).HasComment("启动游戏次数"); + entity.Property(e => e.UpdatedAt) + .HasComment("修改时间") + .HasColumnType("datetime"); + + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("_copy_3"); + + entity.ToTable(tb => tb.HasComment("订单统计表")); + + entity.Property(e => e.Channel) + .HasMaxLength(10) + .HasComment("渠道号"); + entity.Property(e => e.CreatedAt) + .HasComment("创建时间") + .HasColumnType("datetime"); + entity.Property(e => e.IntendedOrderCount).HasComment("意向订单次数"); + entity.Property(e => e.LoginDate).HasComment("日期"); + entity.Property(e => e.LoginDay).HasComment("天"); + entity.Property(e => e.PaidOrders).HasComment("支付订单次数"); + entity.Property(e => e.RechargeAmount).HasComment("支付订单金额"); + entity.Property(e => e.UpdatedAt) + .HasComment("修改时间") + .HasColumnType("datetime"); + + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__T_Statis__3214EC07FE2CF78F"); + + entity.ToTable(tb => tb.HasComment("用户统计表")); + + entity.Property(e => e.ActiveCount).HasComment("用户活跃数"); + entity.Property(e => e.Channel) + .HasMaxLength(10) + .HasComment("渠道号"); + entity.Property(e => e.CreatedAt) + .HasComment("创建时间") + .HasColumnType("datetime"); + entity.Property(e => e.LoginCount).HasComment("用户登录数"); + entity.Property(e => e.LoginDate).HasComment("日期"); + entity.Property(e => e.LoginDay).HasComment("天"); + entity.Property(e => e.RegistrCount).HasComment("用户注册数"); + entity.Property(e => e.UpdatedAt) + .HasComment("修改时间") + .HasColumnType("datetime"); + + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.Id).HasName("PK__T_User_L__3214EC0786EBC26A"); diff --git a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Statistics_Game.cs b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Statistics_Game.cs new file mode 100644 index 0000000..2bec20e --- /dev/null +++ b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Statistics_Game.cs @@ -0,0 +1,53 @@ +using System; + +namespace CloudGaming.GameModel.Db.Db_Ext; + +/// +/// 游戏统计数据 +/// +public partial class T_Statistics_Game +{ + public T_Statistics_Game() { } + + public virtual int Id { get; set; } + + /// + /// 日期 + /// + public virtual DateOnly LoginDate { get; set; } + + /// + /// 天 + /// + public virtual int LoginDay { get; set; } + + /// + /// 用户玩游戏时长 + /// + public virtual int PlayGameTimeCount { get; set; } + + /// + /// 用户玩游戏人数 + /// + public virtual int PlayGameCount { get; set; } + + /// + /// 启动游戏次数 + /// + public virtual int StartGameCount { get; set; } + + /// + /// 渠道号 + /// + public virtual string Channel { get; set; } = null!; + + /// + /// 创建时间 + /// + public virtual DateTime CreatedAt { get; set; } + + /// + /// 修改时间 + /// + public virtual DateTime UpdatedAt { get; set; } +} diff --git a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Statistics_Order.cs b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Statistics_Order.cs new file mode 100644 index 0000000..e3adf0a --- /dev/null +++ b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Statistics_Order.cs @@ -0,0 +1,53 @@ +using System; + +namespace CloudGaming.GameModel.Db.Db_Ext; + +/// +/// 订单统计表 +/// +public partial class T_Statistics_Order +{ + public T_Statistics_Order() { } + + public virtual int Id { get; set; } + + /// + /// 日期 + /// + public virtual DateOnly LoginDate { get; set; } + + /// + /// 天 + /// + public virtual int LoginDay { get; set; } + + /// + /// 意向订单次数 + /// + public virtual int IntendedOrderCount { get; set; } + + /// + /// 支付订单次数 + /// + public virtual int PaidOrders { get; set; } + + /// + /// 支付订单金额 + /// + public virtual int RechargeAmount { get; set; } + + /// + /// 渠道号 + /// + public virtual string Channel { get; set; } = null!; + + /// + /// 创建时间 + /// + public virtual DateTime CreatedAt { get; set; } + + /// + /// 修改时间 + /// + public virtual DateTime UpdatedAt { get; set; } +} diff --git a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Statistics_User.cs b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Statistics_User.cs new file mode 100644 index 0000000..b63adbe --- /dev/null +++ b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_Statistics_User.cs @@ -0,0 +1,53 @@ +using System; + +namespace CloudGaming.GameModel.Db.Db_Ext; + +/// +/// 用户统计表 +/// +public partial class T_Statistics_User +{ + public T_Statistics_User() { } + + public virtual int Id { get; set; } + + /// + /// 日期 + /// + public virtual DateOnly LoginDate { get; set; } + + /// + /// 天 + /// + public virtual int LoginDay { get; set; } + + /// + /// 用户登录数 + /// + public virtual int LoginCount { get; set; } + + /// + /// 用户注册数 + /// + public virtual int RegistrCount { get; set; } + + /// + /// 用户活跃数 + /// + public virtual int ActiveCount { get; set; } + + /// + /// 渠道号 + /// + public virtual string Channel { get; set; } = null!; + + /// + /// 创建时间 + /// + public virtual DateTime CreatedAt { get; set; } + + /// + /// 修改时间 + /// + public virtual DateTime UpdatedAt { get; set; } +} diff --git a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_User_LoginDay_Log.cs b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_User_LoginDay_Log.cs index 6bc9a82..07fea84 100644 --- a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_User_LoginDay_Log.cs +++ b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Ext/T_User_LoginDay_Log.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace CloudGaming.GameModel.Db.Db_Ext; diff --git a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Game/CloudGamingGameContext.cs b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Game/CloudGamingGameContext.cs index 25cbf26..d64c728 100644 --- a/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Game/CloudGamingGameContext.cs +++ b/src/CloudGaming/Model/CloudGaming.GameModel/Db/Db_Game/CloudGamingGameContext.cs @@ -70,7 +70,7 @@ public partial class CloudGamingGameContext : DbContext public virtual DbSet T_Game_UserShare { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - {// => optionsBuilder.UseSqlServer("Server=192.168.195.6;Database=CloudGamingGame;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=true;"); + {// => optionsBuilder.UseSqlServer("Server=192.168.195.8;Database=CloudGamingGame;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=true;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/CloudGaming/Model/CloudGaming.GameModel/Db/efcore-gen.md b/src/CloudGaming/Model/CloudGaming.GameModel/Db/efcore-gen.md index 1de538a..acea14c 100644 --- a/src/CloudGaming/Model/CloudGaming.GameModel/Db/efcore-gen.md +++ b/src/CloudGaming/Model/CloudGaming.GameModel/Db/efcore-gen.md @@ -5,7 +5,7 @@ 内网 dotnet ef dbcontext scaffold "Server=192.168.1.17;Database=CloudGamingGame;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=true;" Microsoft.EntityFrameworkCore.SqlServer -o Db/Db_Game/ --use-database-names --no-pluralize --force 内网穿透 -dotnet ef dbcontext scaffold "Server=192.168.195.6;Database=CloudGamingGame;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=true;" Microsoft.EntityFrameworkCore.SqlServer -o Db/Db_Game/ --use-database-names --no-pluralize --force +dotnet ef dbcontext scaffold "Server=192.168.195.8;Database=CloudGamingGame;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=true;" Microsoft.EntityFrameworkCore.SqlServer -o Db/Db_Game/ --use-database-names --no-pluralize --force --Ext dotnet ef dbcontext scaffold "Server=192.168.1.17;Database=CloudGamingCBT;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=true;" Microsoft.EntityFrameworkCore.SqlServer -o Db/Db_Ext/ --use-database-names --no-pluralize --force 内网穿透 diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/HuanMeng.DotNetCore.csproj b/src/CloudGaming/Utile/HuanMeng.DotNetCore/HuanMeng.DotNetCore.csproj index e95534e..91484a0 100644 --- a/src/CloudGaming/Utile/HuanMeng.DotNetCore/HuanMeng.DotNetCore.csproj +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/HuanMeng.DotNetCore.csproj @@ -23,6 +23,9 @@ + + + diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/QuartzExtend/QuartzExtensions.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/QuartzExtend/QuartzExtensions.cs new file mode 100644 index 0000000..cdf318f --- /dev/null +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/QuartzExtend/QuartzExtensions.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.DependencyInjection; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Quartz; +using Microsoft.IdentityModel.Tokens; +namespace HuanMeng.DotNetCore.QuartzExtend; + +/// +/// 提供扩展方法,用于基于 QuartzTriggerAttribute 注册 Quartz 作业和触发器。 +/// +public static class QuartzExtensions +{ + /// + /// 扫描指定程序集中的所有带有 QuartzTriggerAttribute 的作业类,并将它们注册到 Quartz 调度器中。 + /// + /// 依赖注入服务集合。 + /// 要扫描的程序集。 + /// 更新后的服务集合。 + public static IServiceCollection AddQuartzWithAttributes(this IServiceCollection services, Assembly assembly) + { + services.AddQuartz(q => + { + // 查找实现了 IJob 接口并带有 QuartzTriggerAttribute 特性的所有类 + var jobTypes = assembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && typeof(IJob).IsAssignableFrom(t)); + + foreach (var jobType in jobTypes) + { + // 获取 QuartzTriggerAttribute 特性实例 + var attr = jobType.GetCustomAttribute(); + if (attr != null) + { + // 注册作业 + var jobKey = new JobKey(jobType.Name); // 使用类名作为作业的唯一标识 + q.AddJob(jobType, jobKey, opts => opts.WithIdentity(jobKey)); // 将作业注册到调度器 + + // 根据特性中定义的调度配置注册触发器 + if (!string.IsNullOrEmpty(attr.CronExpression)) + { + // 如果定义了 Cron 表达式,使用 Cron 调度 + q.AddTrigger(opts => opts + .ForJob(jobKey) + .WithIdentity(attr.TriggerName) + .WithCronSchedule(attr.CronExpression)); + } + else if (attr.IntervalInSeconds.HasValue && attr.IntervalInSeconds > 0) + { + // 如果定义了简单时间间隔,使用简单调度 + q.AddTrigger(opts => opts + .ForJob(jobKey) + .WithIdentity(attr.TriggerName) + .StartNow() + .WithSimpleSchedule(x => x.WithIntervalInSeconds(attr.IntervalInSeconds.Value).RepeatForever())); + } + } + } + }); + + // 注册 Quartz 的托管服务 + services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); + return services; + } +} diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/QuartzExtend/QuartzTriggerAttribute.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/QuartzExtend/QuartzTriggerAttribute.cs new file mode 100644 index 0000000..3516b9c --- /dev/null +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/QuartzExtend/QuartzTriggerAttribute.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HuanMeng.DotNetCore.QuartzExtend; + +/// +/// 自定义特性,用于标识 Quartz 作业的触发器配置。 +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class QuartzTriggerAttribute : Attribute +{ + /// + /// 获取触发器的名称。 + /// + public string TriggerName { get; } + + /// + /// 获取触发器的时间间隔(秒)。 + /// + public int? IntervalInSeconds { get; } + + /// + /// 获取 Cron 表达式。 + /// + public string CronExpression { get; } + + /// + /// 使用 Cron 表达式初始化特性。 + /// + /// 触发器名称。 + /// Cron 表达式。 + public QuartzTriggerAttribute(string triggerName, string cronExpression = "0/1 * * * * ?") + { + TriggerName = triggerName; + IntervalInSeconds = null; + CronExpression = cronExpression; + } + + /// + /// 使用时间间隔初始化特性。 + /// + /// 触发器名称。 + /// 时间间隔(秒)。 + public QuartzTriggerAttribute(string triggerName, int intervalInSeconds) + { + TriggerName = triggerName; + IntervalInSeconds = intervalInSeconds; + CronExpression = ""; + } +} diff --git a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Redis/RedisConnection.cs b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Redis/RedisConnection.cs index 13ca5de..4ef6490 100644 --- a/src/CloudGaming/Utile/HuanMeng.DotNetCore/Redis/RedisConnection.cs +++ b/src/CloudGaming/Utile/HuanMeng.DotNetCore/Redis/RedisConnection.cs @@ -50,7 +50,7 @@ namespace HuanMeng.DotNetCore.Redis return database; } - + /// /// @@ -195,6 +195,7 @@ namespace HuanMeng.DotNetCore.Redis /// public static async Task StringGetAsync(this IDatabase database, string key) { + var value = await database.StringGetAsync(key); // 检查值是否为空 if (!value.HasValue) @@ -232,21 +233,80 @@ namespace HuanMeng.DotNetCore.Redis /// /// 异步模糊查询key /// - /// - /// - /// - /// + /// Redis 服务器实例 + /// 匹配的模式(支持通配符) + /// 每次扫描的页大小 + /// 数据库编号,默认值为 -1 表示全部数据库 /// 匹配的键的字符串列表 public static async Task> ScanKeysAsync(this IServer server, string pattern, int pageSize = 100, int database = -1) { var matchingKeys = new List(); - await foreach (var key in server.KeysAsync(database: database, pattern: pattern, pageSize: pageSize)) + try { - matchingKeys.Add(key.ToString()); + await foreach (var key in server.KeysAsync(database: database, pattern: pattern, pageSize: pageSize)) + { + matchingKeys.Add(key.ToString()); + } + } + catch (Exception ex) + { + // 可以记录日志或者处理异常 + Console.WriteLine($"Error while scanning keys: {ex.Message}"); } return matchingKeys; } + + + /// + /// 模糊查询所有匹配的键的数量 + /// + /// Redis 服务器实例 + /// 匹配的模式(支持通配符) + /// 匹配的键的数量 + public static int ScanKeysCount(this IServer server, string pattern,int database = -1) + { + int count = 0; + long cursor = 0; // 游标,用于记录扫描进度 + + do + { + // 使用 Keys 方法进行分页查询 + var keys = server.Keys(database: database, cursor: cursor, pattern: pattern, pageSize: 1000).ToArray(); + + // 累计匹配到的键的数量 + count += keys.Length; + + // 如果还有更多键,则获取新的游标;否则,循环终止 + cursor = keys.Length == 1000 ? 1 : 0; + + } while (cursor != 0); + + return count; + } + + /// + /// 异步模糊查询所有匹配的键的数量 + /// + /// Redis 服务器实例 + /// 匹配的模式(支持通配符) + /// 每次扫描的页大小 + /// 数据库编号,默认为 -1 表示当前数据库 + /// 匹配的键的数量 + public static async Task ScanKeysCountAsync(this IServer server, string pattern, int pageSize = 1000, int database = -1) + { + int count = 0; + + // 异步遍历 KeysAsync + await foreach (var key in server.KeysAsync(database: database, pattern: pattern, pageSize: pageSize)) + { + count++; + } + + return count; + } + + } }