diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Controller/RoleCombatController.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Controller/RoleCombatController.cs new file mode 100644 index 0000000..87b00a7 --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Controller/RoleCombatController.cs @@ -0,0 +1,120 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.EntityFrameworkCore.Storage; +using Snap.Hutao.Server.Controller.Filter; +using Snap.Hutao.Server.Extension; +using Snap.Hutao.Server.Model.Context; +using Snap.Hutao.Server.Model.Entity.RoleCombat; +using Snap.Hutao.Server.Model.Response; +using Snap.Hutao.Server.Model.RoleCombat; +using Snap.Hutao.Server.Service.RoleCombat; +using System.Collections.Concurrent; + +namespace Snap.Hutao.Server.Controller; + +[ApiController] +[Route("[controller]")] +[ServiceFilter(typeof(CountRequests))] +[ApiExplorerSettings(GroupName = "RoleCombat")] +public class RoleCombatController +{ + private static readonly ConcurrentDictionary UploadingUids = new(); + + private readonly AppDbContext appDbContext; + private readonly IMemoryCache memoryCache; + + public RoleCombatController(IServiceProvider serviceProvider) + { + appDbContext = serviceProvider.GetRequiredService(); + memoryCache = serviceProvider.GetRequiredService(); + } + + [HttpPost("Upload")] + public async Task Upload([FromBody] SimpleRoleCombatRecord record) + { + if (memoryCache.TryGetValue(RoleCombatService.Working, out _)) + { + return Response.Fail(ReturnCode.ComputingStatistics, "正在计算统计数据", ServerKeys.ServerRecordComputingStatistics); + } + + if (record.ScheduleId != RoleCombatScheduleId.GetForNow()) + { + return Response.Fail(ReturnCode.NotCurrentSchedule, "非当前剧演数据", ServerKeys.ServerRecordNotCurrentSchedule); + } + + if (!record.Validate()) + { + return Response.Fail(ReturnCode.InvalidUploadData, "无效的提交数据", ServerKeys.ServerRecordInvalidData); + } + + if (UploadingUids.TryGetValue(record.Uid, out _) || !UploadingUids.TryAdd(record.Uid, default)) + { + return Response.Fail(ReturnCode.PreviousRequestNotCompleted, "该UID的请求尚在处理", ServerKeys.ServerRecordPreviousRequestNotCompleted); + } + + using (IDbContextTransaction transaction = await appDbContext.Database.BeginTransactionAsync().ConfigureAwait(false)) + { + RoleCombatRecord? exist = await appDbContext.RoleCombatRecords.SingleOrDefaultAsync(r => r.Uid == record.Uid).ConfigureAwait(false); + + if (exist is not null) + { + await appDbContext.RoleCombatAvatars.Where(a => a.RecordId == exist.PrimaryId).ExecuteDeleteAsync().ConfigureAwait(false); + } + + await appDbContext.RoleCombatRecords.Where(r => r.Uid == record.Uid).ExecuteDeleteAsync().ConfigureAwait(false); + + RoleCombatRecord newRecord = new() + { + Uid = record.Uid, + Uploader = record.Identity, + UploadTime = DateTimeOffset.Now.ToUnixTimeSeconds(), + }; + await appDbContext.RoleCombatRecords.AddAndSaveAsync(newRecord).ConfigureAwait(false); + + long recordId = newRecord.PrimaryId; + List avatars = record.BackupAvatars.Select(id => new RoleCombatAvatar() { AvatarId = id, RecordId = recordId }).ToList(); + await appDbContext.RoleCombatAvatars.AddRangeAndSaveAsync(avatars).ConfigureAwait(false); + + await transaction.CommitAsync().ConfigureAwait(false); + } + + if (!UploadingUids.TryRemove(record.Uid, out _)) + { + return Response.Fail(ReturnCode.InternalStateException, "提交状态异常", ServerKeys.ServerRecordInternalException); + } + + return Response.Success("数据提交成功"); + } + + [HttpGet("Statistics")] + public IActionResult GetStatistics([FromQuery(Name = "Last")] bool last = false) + { + int scheduleId = RoleCombatScheduleId.GetForNow(); + if (last) + { + scheduleId--; + } + + string key = $"RoleCombatStatistics:{scheduleId}"; + if (memoryCache.TryGetValue(key, out RoleCombatStatisticsItem? data)) + { + return Response.Success("获取剧演统计数据成功", data!); + } + + RoleCombatStatistics? statistics = appDbContext.RoleCombatStatistics + .SingleOrDefault(s => s.ScheduleId == scheduleId); + + if (statistics is null) + { + return Response.Success("获取剧演统计数据成功", default!); + } + + RoleCombatStatisticsItem? typedData = JsonSerializer.Deserialize(statistics.Data); + memoryCache.Set(key, typedData, TimeSpan.FromDays(1)); + + return Response.Success("获取深渊统计数据成功", typedData!); + } + + private readonly struct UploadToken; +} \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Discord/HutaoServerCommands.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Discord/HutaoServerCommands.cs index d3e960a..e889a92 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Discord/HutaoServerCommands.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Discord/HutaoServerCommands.cs @@ -7,6 +7,7 @@ using Snap.Hutao.Server.Service.GachaLog; using Snap.Hutao.Server.Service.GachaLog.Statistics; using Snap.Hutao.Server.Service.Legacy; +using Snap.Hutao.Server.Service.RoleCombat; namespace Snap.Hutao.Server.Discord; @@ -38,6 +39,19 @@ public sealed class HutaoServerCommands : DiscordApplicationModuleBase return Response(response); } + [OwnerOnly] + [SlashCommand("run-statistics-rolecombat")] + [Description("运行剧演记录统计")] + public async ValueTask RunRoleCombatStatisticsAsync() + { + await Context.Services.GetRequiredService().RunAsync().ConfigureAwait(false); + LocalEmbed embed = Embed.CreateStandardEmbed("幻想真境剧诗统计", Embed.GachaLogIcon); + embed.WithDescription("幻想真境剧诗完成"); + LocalInteractionMessageResponse response = new LocalInteractionMessageResponse() + .WithEmbeds(embed); + return Response(response); + } + [OwnerOnly] [SlashCommand("extend-gachalog-all")] [Description("延长祈愿记录时间")] diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/GachaLogStatisticsRefreshJob.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/GachaLogStatisticsRefreshJob.cs index fa9dcec..72e0cd3 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/GachaLogStatisticsRefreshJob.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/GachaLogStatisticsRefreshJob.cs @@ -6,7 +6,7 @@ namespace Snap.Hutao.Server.Job; -public class GachaLogStatisticsRefreshJob : IJob +public sealed class GachaLogStatisticsRefreshJob : IJob { private readonly GachaLogStatisticsService statisticsService; diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/RoleCombatStatisticsRefreshJob.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/RoleCombatStatisticsRefreshJob.cs new file mode 100644 index 0000000..ff50672 --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/RoleCombatStatisticsRefreshJob.cs @@ -0,0 +1,23 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Quartz; +using Snap.Hutao.Server.Service.GachaLog.Statistics; +using Snap.Hutao.Server.Service.RoleCombat; + +namespace Snap.Hutao.Server.Job; + +public sealed class RoleCombatStatisticsRefreshJob : IJob +{ + private readonly RoleCombatService statisticsService; + + public RoleCombatStatisticsRefreshJob(RoleCombatService statisticsService) + { + this.statisticsService = statisticsService; + } + + public Task Execute(IJobExecutionContext context) + { + return statisticsService.RunAsync(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/SpiralAbyssRecordCleanJob.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/SpiralAbyssRecordCleanJob.cs index 08077d3..ec3e672 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/SpiralAbyssRecordCleanJob.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/SpiralAbyssRecordCleanJob.cs @@ -57,4 +57,38 @@ public async Task Execute(IJobExecutionContext context) memoryCache.Remove(GachaLogStatisticsService.Working); } } +} + +public sealed class RoleCombatRecordCleanJob : IJob +{ + private readonly DiscordService discordService; + private readonly AppDbContext appDbContext; + private readonly IMemoryCache memoryCache; + + public RoleCombatRecordCleanJob(IServiceProvider serviceProvider) + { + memoryCache = serviceProvider.GetRequiredService(); + appDbContext = serviceProvider.GetRequiredService(); + discordService = serviceProvider.GetRequiredService(); + } + + /// + public async Task Execute(IJobExecutionContext context) + { + try + { + memoryCache.Set(GachaLogStatisticsService.Working, true); + + await appDbContext.RoleCombatAvatars.ExecuteDeleteAsync().ConfigureAwait(false); + + int deletedRecordsCount = await appDbContext.RoleCombatRecords.ExecuteDeleteAsync().ConfigureAwait(false); + + RoleCombatRecordCleanResult result = new(deletedRecordsCount); + await discordService.ReportRoleCombatCleanResultAsync(result).ConfigureAwait(false); + } + finally + { + memoryCache.Remove(GachaLogStatisticsService.Working); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/SpiralAbyssRecordCleanResult.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/SpiralAbyssRecordCleanResult.cs index 6a237a4..791c928 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/SpiralAbyssRecordCleanResult.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Job/SpiralAbyssRecordCleanResult.cs @@ -15,4 +15,14 @@ public SpiralAbyssRecordCleanResult(int records, int spiralAbyss, long redis) DeletedNumberOfSpiralAbysses = spiralAbyss; RemovedNumberOfRedisKeys = redis; } +} + +public readonly struct RoleCombatRecordCleanResult +{ + public readonly int DeletedNumberOfRecords; + + public RoleCombatRecordCleanResult(int records) + { + DeletedNumberOfRecords = records; + } } \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/20241111021636_AddRoleCombatModels.Designer.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/20241111021636_AddRoleCombatModels.Designer.cs new file mode 100644 index 0000000..6997d15 --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/20241111021636_AddRoleCombatModels.Designer.cs @@ -0,0 +1,1041 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Snap.Hutao.Server.Model.Context; + +#nullable disable + +namespace Snap.Hutao.Server.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20241111021636_AddRoleCombatModels")] + partial class AddRoleCombatModels + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Achievement.EntityAchievement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArchiveId") + .HasColumnType("bigint"); + + b.Property("Current") + .HasColumnType("int unsigned"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Time") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("ArchiveId"); + + b.ToTable("achievements"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Achievement.EntityAchievementArchive", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("achievement_archives"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.AllowedVersion", b => + { + b.Property("Header") + .HasColumnType("varchar(255)"); + + b.HasKey("Header"); + + b.ToTable("allowed_versions"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Announcement.EntityAnnouncement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Locale") + .HasColumnType("varchar(255)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LastUpdateTime") + .HasColumnType("bigint"); + + b.Property("Link") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxPresentVersion") + .HasColumnType("longtext"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id", "Locale"); + + b.ToTable("announcements"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.GachaLog.EntityGachaItem", b => + { + b.Property("Uid") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("GachaType") + .HasColumnType("int"); + + b.Property("IsTrusted") + .HasColumnType("tinyint(1)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("QueryType") + .HasColumnType("int"); + + b.Property("Time") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Uid", "Id"); + + b.HasIndex("UserId"); + + b.ToTable("gacha_items"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.GachaLog.GachaStatistics", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("PrimaryId"); + + b.ToTable("gacha_statistics"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.GachaLog.InvalidGachaUid", b => + { + b.Property("Uid") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.HasKey("Uid"); + + b.ToTable("invalid_gacha_uids"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.LicenseApplicationRecord", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("ApprovalCode") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsApproved") + .HasColumnType("tinyint(1)"); + + b.Property("ProjectUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("PrimaryId"); + + b.HasIndex("UserId"); + + b.ToTable("license_application_records"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Passport.GithubIdentity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ExipresAt") + .HasColumnType("bigint"); + + b.Property("NodeId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("GithubIdentities"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Passport.HutaoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("GachaLogExpireAt") + .HasColumnType("bigint"); + + b.Property("IsLicensedDeveloper") + .HasColumnType("tinyint(1)"); + + b.Property("IsMaintainer") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Passport.PassportVerification", b => + { + b.Property("NormalizedUserName") + .HasColumnType("varchar(255)"); + + b.Property("ExpireTimestamp") + .HasColumnType("bigint"); + + b.Property("GeneratedTimestamp") + .HasColumnType("bigint"); + + b.Property("VerifyCode") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("NormalizedUserName"); + + b.ToTable("passport_verifications"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RequestStatistics", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Count") + .HasColumnType("bigint"); + + b.Property("Path") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserAgent") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("PrimaryId"); + + b.ToTable("request_statistics"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatAvatar", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("AvatarId") + .HasColumnType("int unsigned"); + + b.Property("RecordId") + .HasColumnType("bigint"); + + b.HasKey("PrimaryId"); + + b.HasIndex("RecordId"); + + b.ToTable("role_combat_avatars"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatRecord", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Uid") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("UploadTime") + .HasColumnType("bigint"); + + b.Property("Uploader") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.HasKey("PrimaryId"); + + b.ToTable("role_combat_records"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatStatistics", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ScheduleId") + .HasColumnType("int"); + + b.HasKey("PrimaryId"); + + b.ToTable("role_combat_statistics"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.Banned", b => + { + b.Property("Uid") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.HasKey("Uid"); + + b.ToTable("banned"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityAvatar", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("ActivedConstellationNumber") + .HasColumnType("int"); + + b.Property("AvatarId") + .HasColumnType("int"); + + b.Property("RecordId") + .HasColumnType("bigint"); + + b.Property("ReliquarySet") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("PrimaryId"); + + b.HasIndex("RecordId"); + + b.ToTable("avatars"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityDamageRank", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("AvatarId") + .HasColumnType("int"); + + b.Property("SpiralAbyssId") + .HasColumnType("bigint"); + + b.Property("Uid") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Value") + .HasColumnType("int"); + + b.HasKey("PrimaryId"); + + b.HasIndex("SpiralAbyssId") + .IsUnique(); + + b.HasIndex("Value"); + + b.ToTable("damage_ranks"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityFloor", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Index") + .HasColumnType("int"); + + b.Property("Levels") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SpiralAbyssId") + .HasColumnType("bigint"); + + b.Property("Star") + .HasColumnType("int"); + + b.HasKey("PrimaryId"); + + b.HasIndex("SpiralAbyssId"); + + b.ToTable("spiral_abysses_floors"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityRecord", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Uid") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("UploadTime") + .HasColumnType("bigint"); + + b.Property("Uploader") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.HasKey("PrimaryId"); + + b.ToTable("records"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntitySpiralAbyss", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("RecordId") + .HasColumnType("bigint"); + + b.Property("TotalBattleTimes") + .HasColumnType("int"); + + b.Property("TotalWinTimes") + .HasColumnType("int"); + + b.HasKey("PrimaryId"); + + b.HasIndex("RecordId") + .IsUnique(); + + b.ToTable("spiral_abysses"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityTakeDamageRank", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("AvatarId") + .HasColumnType("int"); + + b.Property("SpiralAbyssId") + .HasColumnType("bigint"); + + b.Property("Uid") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Value") + .HasColumnType("int"); + + b.HasKey("PrimaryId"); + + b.HasIndex("SpiralAbyssId") + .IsUnique(); + + b.HasIndex("Value"); + + b.ToTable("take_damage_ranks"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.LegacyStatistics", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ScheduleId") + .HasColumnType("int"); + + b.HasKey("PrimaryId"); + + b.ToTable("spiral_abysses_statistics"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Telemetry.HutaoLog", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("Info") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Resolved") + .HasColumnType("tinyint(1)"); + + b.Property("Version") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("PrimaryId"); + + b.ToTable("hutao_logs"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Telemetry.HutaoLogSingleItem", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LogId") + .HasColumnType("bigint"); + + b.Property("Time") + .HasColumnType("bigint"); + + b.HasKey("PrimaryId"); + + b.HasIndex("LogId"); + + b.ToTable("hutao_log_items"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.Passport.HutaoUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.Passport.HutaoUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Snap.Hutao.Server.Model.Entity.Passport.HutaoUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.Passport.HutaoUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Achievement.EntityAchievement", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.Achievement.EntityAchievementArchive", "Archive") + .WithMany() + .HasForeignKey("ArchiveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Achievement.EntityAchievementArchive", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.Passport.HutaoUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.GachaLog.EntityGachaItem", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.Passport.HutaoUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.LicenseApplicationRecord", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.Passport.HutaoUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Passport.GithubIdentity", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.Passport.HutaoUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatAvatar", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatRecord", "Record") + .WithMany("Avatars") + .HasForeignKey("RecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Record"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityAvatar", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityRecord", "Record") + .WithMany("Avatars") + .HasForeignKey("RecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Record"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityDamageRank", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntitySpiralAbyss", "SpiralAbyss") + .WithOne("Damage") + .HasForeignKey("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityDamageRank", "SpiralAbyssId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SpiralAbyss"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityFloor", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntitySpiralAbyss", "SpiralAbyss") + .WithMany("Floors") + .HasForeignKey("SpiralAbyssId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SpiralAbyss"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntitySpiralAbyss", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityRecord", "Record") + .WithOne("SpiralAbyss") + .HasForeignKey("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntitySpiralAbyss", "RecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Record"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityTakeDamageRank", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntitySpiralAbyss", "SpiralAbyss") + .WithOne("TakeDamage") + .HasForeignKey("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityTakeDamageRank", "SpiralAbyssId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SpiralAbyss"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.Telemetry.HutaoLogSingleItem", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.Telemetry.HutaoLog", "Log") + .WithMany() + .HasForeignKey("LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Log"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatRecord", b => + { + b.Navigation("Avatars"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityRecord", b => + { + b.Navigation("Avatars"); + + b.Navigation("SpiralAbyss"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntitySpiralAbyss", b => + { + b.Navigation("Damage") + .IsRequired(); + + b.Navigation("Floors"); + + b.Navigation("TakeDamage") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/20241111021636_AddRoleCombatModels.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/20241111021636_AddRoleCombatModels.cs new file mode 100644 index 0000000..ff76793 --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/20241111021636_AddRoleCombatModels.cs @@ -0,0 +1,89 @@ +// +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Server.Migrations +{ + /// + public partial class AddRoleCombatModels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "role_combat_records", + columns: table => new + { + PrimaryId = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Uid = table.Column(type: "varchar(10)", maxLength: 10, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Uploader = table.Column(type: "varchar(64)", maxLength: 64, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + UploadTime = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_role_combat_records", x => x.PrimaryId); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "role_combat_statistics", + columns: table => new + { + PrimaryId = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + ScheduleId = table.Column(type: "int", nullable: false), + Data = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_role_combat_statistics", x => x.PrimaryId); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "role_combat_avatars", + columns: table => new + { + PrimaryId = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + RecordId = table.Column(type: "bigint", nullable: false), + AvatarId = table.Column(type: "int unsigned", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_role_combat_avatars", x => x.PrimaryId); + table.ForeignKey( + name: "FK_role_combat_avatars_role_combat_records_RecordId", + column: x => x.RecordId, + principalTable: "role_combat_records", + principalColumn: "PrimaryId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_role_combat_avatars_RecordId", + table: "role_combat_avatars", + column: "RecordId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "role_combat_avatars"); + + migrationBuilder.DropTable( + name: "role_combat_statistics"); + + migrationBuilder.DropTable( + name: "role_combat_records"); + } + } +} diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/AppDbContextModelSnapshot.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/AppDbContextModelSnapshot.cs index 53a4ce1..db954f6 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Migrations/AppDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("ProductVersion", "8.0.10") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -497,6 +497,73 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("request_statistics"); }); + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatAvatar", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("AvatarId") + .HasColumnType("int unsigned"); + + b.Property("RecordId") + .HasColumnType("bigint"); + + b.HasKey("PrimaryId"); + + b.HasIndex("RecordId"); + + b.ToTable("role_combat_avatars"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatRecord", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Uid") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("UploadTime") + .HasColumnType("bigint"); + + b.Property("Uploader") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.HasKey("PrimaryId"); + + b.ToTable("role_combat_records"); + }); + + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatStatistics", b => + { + b.Property("PrimaryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrimaryId")); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ScheduleId") + .HasColumnType("int"); + + b.HasKey("PrimaryId"); + + b.ToTable("role_combat_statistics"); + }); + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.Banned", b => { b.Property("Uid") @@ -866,6 +933,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatAvatar", b => + { + b.HasOne("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatRecord", "Record") + .WithMany("Avatars") + .HasForeignKey("RecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Record"); + }); + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityAvatar", b => { b.HasOne("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityRecord", "Record") @@ -932,6 +1010,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Log"); }); + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.RoleCombat.RoleCombatRecord", b => + { + b.Navigation("Avatars"); + }); + modelBuilder.Entity("Snap.Hutao.Server.Model.Entity.SpiralAbyss.EntityRecord", b => { b.Navigation("Avatars"); diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Context/AppDbContext.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Context/AppDbContext.cs index 44c37b7..7616815 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Context/AppDbContext.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Context/AppDbContext.cs @@ -8,8 +8,10 @@ using Snap.Hutao.Server.Model.Entity.Announcement; using Snap.Hutao.Server.Model.Entity.GachaLog; using Snap.Hutao.Server.Model.Entity.Passport; +using Snap.Hutao.Server.Model.Entity.RoleCombat; using Snap.Hutao.Server.Model.Entity.SpiralAbyss; using Snap.Hutao.Server.Model.Entity.Telemetry; +using Snap.Hutao.Server.Model.RoleCombat; namespace Snap.Hutao.Server.Model.Context; @@ -72,6 +74,14 @@ public AppDbContext(DbContextOptions options) public DbSet Achievements { get; set; } = default!; #endregion + #region RoleCombat + public DbSet RoleCombatRecords { get; set; } = default!; + + public DbSet RoleCombatAvatars { get; set; } = default!; + + public DbSet RoleCombatStatistics { get; set; } = default!; + #endregion + /// protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/RoleCombat/RoleCombatAvatar.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/RoleCombat/RoleCombatAvatar.cs new file mode 100644 index 0000000..502024e --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/RoleCombat/RoleCombatAvatar.cs @@ -0,0 +1,19 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Server.Model.Entity.RoleCombat; + +[Table("role_combat_avatars")] +public sealed class RoleCombatAvatar +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long PrimaryId { get; set; } + + public long RecordId { get; set; } + + [ForeignKey(nameof(RecordId))] + public RoleCombatRecord Record { get; set; } = null!; + + public uint AvatarId { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/RoleCombat/RoleCombatRecord.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/RoleCombat/RoleCombatRecord.cs new file mode 100644 index 0000000..4133631 --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/RoleCombat/RoleCombatRecord.cs @@ -0,0 +1,22 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Server.Model.Entity.RoleCombat; + +[Table("role_combat_records")] +public sealed class RoleCombatRecord +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long PrimaryId { get; set; } + + [StringLength(10, MinimumLength = 9)] + public string Uid { get; set; } = null!; + + [StringLength(64)] + public string Uploader { get; set; } = null!; + + public long UploadTime { get; set; } + + public List Avatars { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/RoleCombat/RoleCombatStatistics.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/RoleCombat/RoleCombatStatistics.cs new file mode 100644 index 0000000..55701d5 --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/RoleCombat/RoleCombatStatistics.cs @@ -0,0 +1,16 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Server.Model.Entity.RoleCombat; + +[Table("role_combat_statistics")] +public class RoleCombatStatistics +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long PrimaryId { get; set; } + + public int ScheduleId { get; set; } = default!; + + public string Data { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/Telemetry/HutaoLog.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/Telemetry/HutaoLog.cs index cf98649..c2e854c 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/Telemetry/HutaoLog.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Entity/Telemetry/HutaoLog.cs @@ -17,4 +17,7 @@ public class HutaoLog public bool Resolved { get; set; } public string Version { get; set; } = default!; + + [NotMapped] + public long Time { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/RoleCombat/RoleCombatStatisticsItem.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/RoleCombat/RoleCombatStatisticsItem.cs new file mode 100644 index 0000000..8749c82 --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/RoleCombat/RoleCombatStatisticsItem.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Server.Model.Legacy; + +namespace Snap.Hutao.Server.Model.RoleCombat; + +public sealed class RoleCombatStatisticsItem +{ + public int RecordTotal { get; set; } + + public List> BackupAvatarRates { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/RoleCombat/SimpleRoleCombatRecord.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/RoleCombat/SimpleRoleCombatRecord.cs new file mode 100644 index 0000000..564818c --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/RoleCombat/SimpleRoleCombatRecord.cs @@ -0,0 +1,39 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Server.Model.RoleCombat; + +public sealed class SimpleRoleCombatRecord +{ + public uint Version { get; set; } + + #region V1 + public string Uid { get; set; } = default!; + + public string Identity { get; set; } = default!; + + public List BackupAvatars { get; set; } = default!; + + public int ScheduleId { get; set; } + #endregion + + public bool Validate() + { + if (Version != 1) + { + return false; + } + + if (Uid is null || Uid.Length < 9) + { + return false; + } + + if (BackupAvatars is not { Count: >= 8 }) + { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Upload/SimpleRecord.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Upload/SimpleRecord.cs index 9822e0e..57712af 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Upload/SimpleRecord.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Model/Upload/SimpleRecord.cs @@ -25,7 +25,7 @@ public bool Validate() return false; } - if (Uid == null || Uid.Length < 9) + if (Uid is null || Uid.Length < 9) { return false; } diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Program.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Program.cs index dae16d7..f6cfeab 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Program.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Program.cs @@ -26,6 +26,7 @@ using Snap.Hutao.Server.Service.Licensing; using Snap.Hutao.Server.Service.Ranking; using Snap.Hutao.Server.Service.ReCaptcha; +using Snap.Hutao.Server.Service.RoleCombat; using Snap.Hutao.Server.Service.Telemetry; using Swashbuckle.AspNetCore.SwaggerUI; @@ -83,6 +84,8 @@ public static void Main(string[] args) config.ScheduleJob(t => t.StartNow().WithCronSchedule("0 30 */1 * * ?")); config.ScheduleJob(t => t.StartNow().WithCronSchedule("0 5 */1 * * ?")); config.ScheduleJob(t => t.StartNow().WithCronSchedule("0 0 4 16 * ?")); + config.ScheduleJob(t => t.StartNow().WithCronSchedule("0 10 */1 * * ?")); + config.ScheduleJob(t => t.StartNow().WithCronSchedule("0 0 4 1 * ?")); }) .AddQuartzServer(options => options.WaitForJobsToComplete = true) .AddResponseCompression() @@ -106,6 +109,7 @@ public static void Main(string[] args) .AddSwaggerGen(options => { options.SwaggerDoc("SpiralAbyss", new() { Version = "1.0", Title = "深渊统计", Description = "深渊统计数据" }); + options.SwaggerDoc("RoleCombat", new() { Version = "1.0", Title = "剧演统计", Description = "幻想真境剧诗" }); options.SwaggerDoc("Passport", new() { Version = "1.0", Title = "胡桃账户", Description = "胡桃通行证" }); options.SwaggerDoc("GachaLog", new() { Version = "1.0", Title = "祈愿记录", Description = "账户祈愿记录管理" }); options.SwaggerDoc("Services", new() { Version = "1.0", Title = "服务管理", Description = "维护专用管理接口,调用需要维护权限" }); @@ -119,6 +123,9 @@ public static void Main(string[] args) .AddTransient() .AddTransient() .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient(); @@ -225,6 +232,7 @@ public static void Main(string[] args) option.InjectStylesheet("/css/style.css"); option.SwaggerEndpoint("/swagger/SpiralAbyss/swagger.json", "深渊统计"); + option.SwaggerEndpoint("/swagger/RoleCombat/swagger.json", "剧演统计"); option.SwaggerEndpoint("/swagger/Passport/swagger.json", "胡桃账户"); option.SwaggerEndpoint("/swagger/GachaLog/swagger.json", "祈愿记录"); diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Discord/DiscordService.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Discord/DiscordService.cs index 74022c4..8979585 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Discord/DiscordService.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Discord/DiscordService.cs @@ -7,6 +7,7 @@ using Snap.Hutao.Server.Job; using Snap.Hutao.Server.Model.GachaLog; using Snap.Hutao.Server.Model.Legacy; +using Snap.Hutao.Server.Model.RoleCombat; using Snap.Hutao.Server.Option; using Snap.Hutao.Server.Service.Afdian; using Snap.Hutao.Server.Service.Github; @@ -52,6 +53,17 @@ public async ValueTask ReportSpiralAbyssCleanResultAsync(SpiralAbyssRecordCleanR await hutaoServerBot.SendMessageAsync(discordOptions.KnownChannels.PublicStatus, new LocalMessage().WithEmbeds(embed)); } + internal async Task ReportRoleCombatCleanResultAsync(RoleCombatRecordCleanResult result) + { + LocalEmbed embed = Embed.CreateStandardEmbed("Role Combat Record Cleanup", Embed.SpiralAbyssIcon); + + embed.WithDescription($"In this cleanup, we cleanned:"); + + embed.AddField("Records", result.DeletedNumberOfRecords); + + await hutaoServerBot.SendMessageAsync(discordOptions.KnownChannels.PublicStatus, new LocalMessage().WithEmbeds(embed)); + } + public async ValueTask ReportGachaEventStatisticsAsync(GachaEventStatistics statistics) { LocalEmbed embed = Embed.CreateStandardEmbed("Gacha Event Statistics", Embed.GachaLogIcon); @@ -91,6 +103,17 @@ public async ValueTask ReportSpiralAbyssStatisticsAsync(Overview overview) await hutaoServerBot.SendMessageAsync(discordOptions.KnownChannels.PublicStatus, new LocalMessage().WithEmbeds(embed)); } + public async ValueTask ReportRoleCombatStatisticsAsync(RoleCombatStatisticsItem statistics) + { + LocalEmbed embed = Embed.CreateStandardEmbed("Role Combat Statistics", Embed.GachaLogIcon); + + embed.WithDescription("Statistics run completed"); + + embed.AddField("Record Total", statistics.RecordTotal, true); + + await hutaoServerBot.SendMessageAsync(discordOptions.KnownChannels.PublicStatus, new LocalMessage().WithEmbeds(embed)); + } + public async ValueTask ReportGithubWebhookAsync(GithubWebhookResult githubMessage) { ulong channelId = githubMessage.Event switch diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Legacy/RecordService.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Legacy/RecordService.cs index 331f44d..04122b2 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Legacy/RecordService.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Legacy/RecordService.cs @@ -205,7 +205,7 @@ private async Task SaveRecordAsync(SimpleRecord record) (bool haveUploaded, bool recordExists) = await HaveUploadedForCurrentScheduleAsync(record.Uid).ConfigureAwait(false); RecordUploadResult result = await GetNonErrorRecordUploadResultAsync(record, haveUploaded).ConfigureAwait(false); - using (IDbContextTransaction transaction = await appDbContext.Database.BeginTransactionAsync()) + using (IDbContextTransaction transaction = await appDbContext.Database.BeginTransactionAsync().ConfigureAwait(false)) { if (recordExists) { @@ -253,7 +253,7 @@ private async Task SaveRecordAsync(SimpleRecord record) await appDbContext.TakeDamageRanks.AddAndSaveAsync(entityTakeDamageRank).ConfigureAwait(false); } - transaction.Commit(); + await transaction.CommitAsync().ConfigureAwait(false); } // Redis rank sync diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/RoleCombat/RoleCombatScheduleId.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/RoleCombat/RoleCombatScheduleId.cs new file mode 100644 index 0000000..7445855 --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/RoleCombat/RoleCombatScheduleId.cs @@ -0,0 +1,33 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Server.Service.RoleCombat; + +public static class RoleCombatScheduleId +{ + private static readonly TimeSpan Utc8 = new(8, 0, 0); + + public static int GetForNow() + { + return GetForDateTimeOffset(DateTimeOffset.UtcNow); + } + + public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset) + { + // Force time in UTC+08 + dateTimeOffset = dateTimeOffset.ToOffset(Utc8); + + ((int year, int mouth, int day), (int hour, _), _) = dateTimeOffset; + + // 2024-07-01 04:00:00 为第 3 期 + int periodNum = ((year - 2024) * 12) + (mouth - 6) + 2; + + // 上个月:1 日 00:00-04:00 + if (day is 1 && hour < 4) + { + periodNum--; + } + + return periodNum; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/RoleCombat/RoleCombatService.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/RoleCombat/RoleCombatService.cs new file mode 100644 index 0000000..884b8ba --- /dev/null +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/RoleCombat/RoleCombatService.cs @@ -0,0 +1,96 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Server.Extension; +using Snap.Hutao.Server.Model.Context; +using Snap.Hutao.Server.Model.Entity.RoleCombat; +using Snap.Hutao.Server.Model.RoleCombat; +using Snap.Hutao.Server.Service.Discord; +using Snap.Hutao.Server.Service.Legacy.Primitive; +using System.Runtime.InteropServices; + +namespace Snap.Hutao.Server.Service.RoleCombat; + +public sealed class RoleCombatService +{ + public const string Working = "RoleCombatService.Working"; + + private static readonly Func> RecordsQuery = EF.CompileQuery((AppDbContext context) => + context.RoleCombatRecords.AsNoTracking()); + + private static readonly Func> AvatarsQuery = EF.CompileQuery((AppDbContext context, long recordId) => + context.RoleCombatAvatars.AsNoTracking().Where(g => g.RecordId == recordId).AsQueryable()); + + private readonly DiscordService discordService; + private readonly AppDbContext appDbContext; + private readonly IMemoryCache memoryCache; + + public RoleCombatService(IServiceProvider serviceProvider) + { + discordService = serviceProvider.GetRequiredService(); + appDbContext = serviceProvider.GetRequiredService(); + memoryCache = serviceProvider.GetRequiredService(); + } + + public async Task RunAsync() + { + if (memoryCache.TryGetValue(Working, out _)) + { + return; + } + + try + { + memoryCache.Set(Working, true); + Map resultMap = []; + + int total = await Task.Run(() => RunCore(resultMap)).ConfigureAwait(false); + RoleCombatStatisticsItem item = new() + { + RecordTotal = total, + BackupAvatarRates = resultMap.Select(kvp => new Model.Legacy.ItemRate(kvp.Key, kvp.Value / (double)total)).ToList(), + }; + + int scheduleId = RoleCombatScheduleId.GetForNow(); + RoleCombatStatistics? statistics = appDbContext.RoleCombatStatistics.SingleOrDefault(s => s.ScheduleId == scheduleId); + + if (statistics == null) + { + statistics = new() + { + ScheduleId = scheduleId, + }; + appDbContext.RoleCombatStatistics.Add(statistics); + } + + memoryCache.Set($"RoleCombatStatistics:{scheduleId}", item); + statistics.Data = JsonSerializer.Serialize(item); + + appDbContext.SaveChanges(); + + await discordService.ReportRoleCombatStatisticsAsync(item).ConfigureAwait(false); + } + finally + { + memoryCache.Remove(Working); + } + } + + private int RunCore(Map resultMap) + { + int count = 0; + List reocrds = RecordsQuery(appDbContext).ToList(); + foreach (ref readonly RoleCombatRecord record in CollectionsMarshal.AsSpan(reocrds)) + { + count++; + List avatars = AvatarsQuery(appDbContext, record.PrimaryId).ToList(); + + foreach (ref readonly RoleCombatAvatar avatar in CollectionsMarshal.AsSpan(avatars)) + { + resultMap.IncreaseOne(avatar.AvatarId); + } + } + + return count; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Telemetry/TelemetryService.cs b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Telemetry/TelemetryService.cs index 4a98333..f5a77bd 100644 --- a/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Telemetry/TelemetryService.cs +++ b/src/Snap.Hutao.Server/Snap.Hutao.Server/Service/Telemetry/TelemetryService.cs @@ -74,7 +74,13 @@ public async ValueTask> GetLogsByDeviceId(string deviceId) .ToListAsync() .ConfigureAwait(false); - List logs = items.SelectList(item => appDbContext.HutaoLogs.AsNoTracking().Single(x => x.PrimaryId == item.LogId)); + List logs = items.SelectList(item => + { + HutaoLog log = appDbContext.HutaoLogs.AsNoTracking().Single(x => x.PrimaryId == item.LogId); + log.Time = item.Time; + return log; + }); + return logs; } } \ No newline at end of file