From f0d6772dcab01a5c51913206c6650cd16d173730 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 25 Dec 2025 22:26:57 +0800 Subject: [PATCH] :sparkles: Pass rewind service and account rewind service --- .../Account/Rewind/AccountRewindService.cs | 6 -- .../Rewind/AccountRewindController.cs | 2 +- .../Rewind/AccountRewindService.cs | 75 +++++++++++++++++++ DysonNetwork.Pass/Rewind/PassRewindService.cs | 49 ++++++++++++ .../Startup/ServiceCollectionExtensions.cs | 4 + DysonNetwork.Shared/Models/RewindPoint.cs | 21 ++++++ DysonNetwork.Shared/Models/SnRewindPoint.cs | 14 ---- DysonNetwork.Shared/Proto/rewind.proto | 1 + .../Rewind/SphereRewindServiceGrpc.cs | 22 ++++-- DysonNetwork.sln.DotSettings.user | 2 + 10 files changed, 168 insertions(+), 28 deletions(-) delete mode 100644 DysonNetwork.Pass/Account/Rewind/AccountRewindService.cs rename DysonNetwork.Pass/{Account => }/Rewind/AccountRewindController.cs (51%) create mode 100644 DysonNetwork.Pass/Rewind/AccountRewindService.cs create mode 100644 DysonNetwork.Pass/Rewind/PassRewindService.cs create mode 100644 DysonNetwork.Shared/Models/RewindPoint.cs delete mode 100644 DysonNetwork.Shared/Models/SnRewindPoint.cs diff --git a/DysonNetwork.Pass/Account/Rewind/AccountRewindService.cs b/DysonNetwork.Pass/Account/Rewind/AccountRewindService.cs deleted file mode 100644 index 1bf9954..0000000 --- a/DysonNetwork.Pass/Account/Rewind/AccountRewindService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DysonNetwork.Pass.Account.Rewind; - -public class AccountRewindService(AppDatabase db) -{ - -} \ No newline at end of file diff --git a/DysonNetwork.Pass/Account/Rewind/AccountRewindController.cs b/DysonNetwork.Pass/Rewind/AccountRewindController.cs similarity index 51% rename from DysonNetwork.Pass/Account/Rewind/AccountRewindController.cs rename to DysonNetwork.Pass/Rewind/AccountRewindController.cs index 71fe5f8..2b5bb53 100644 --- a/DysonNetwork.Pass/Account/Rewind/AccountRewindController.cs +++ b/DysonNetwork.Pass/Rewind/AccountRewindController.cs @@ -1,4 +1,4 @@ -namespace DysonNetwork.Pass.Account.Rewind; +namespace DysonNetwork.Pass.Rewind; public class AccountRewindController { diff --git a/DysonNetwork.Pass/Rewind/AccountRewindService.cs b/DysonNetwork.Pass/Rewind/AccountRewindService.cs new file mode 100644 index 0000000..33fa9f9 --- /dev/null +++ b/DysonNetwork.Pass/Rewind/AccountRewindService.cs @@ -0,0 +1,75 @@ +using DysonNetwork.Shared.Models; +using DysonNetwork.Shared.Proto; +using Grpc.Net.Client; +using Microsoft.EntityFrameworkCore; + +namespace DysonNetwork.Pass.Rewind; + +public class AccountRewindService( + IHttpClientFactory httpClientFactory, + AppDatabase db, + PassRewindService passRewindSrv +) +{ + private static string CapitalizeFirstLetter(string str) + { + if (string.IsNullOrEmpty(str)) + return str; + + // Capitalize the first character and append the rest of the string in lowercase + return char.ToUpper(str[0]) + str[1..].ToLower(); + } + + private RewindService.RewindServiceClient CreateRewindServiceClient(string serviceId) + { + var httpClient = httpClientFactory.CreateClient( + $"{nameof(AccountRewindService)}+{CapitalizeFirstLetter(serviceId)}" + ); + var channel = GrpcChannel.ForAddress($"http://{serviceId}", new GrpcChannelOptions { HttpClient = httpClient }); + return new RewindService.RewindServiceClient(channel); + } + + private async Task CreateRewindPoint(Guid accountId) + { + var currentYear = DateTime.UtcNow.Year; + var rewindRequest = new RequestRewindEvent { AccountId = accountId.ToString(), Year = currentYear}; + + var rewindEventTasks = new List> + { + passRewindSrv.CreateRewindEvent(accountId, currentYear), + CreateRewindServiceClient("sphere").GetRewindEventAsync(rewindRequest).ResponseAsync + }; + var rewindEvents = await Task.WhenAll(rewindEventTasks); + + var rewindData = rewindEvents.ToDictionary>( + rewindEvent => rewindEvent.ServiceId, + rewindEvent => GrpcTypeHelper.ConvertByteStringToObject>(rewindEvent.Data) ?? + new Dictionary() + ); + + var point = new SnRewindPoint + { + SchemaVersion = 1, + AccountId = accountId, + Data = rewindData.ToDictionary(kvp => kvp.Key, object? (kvp) => kvp.Value) + }; + + db.RewindPoints.Add(point); + await db.SaveChangesAsync(); + + return point; + } + + public async Task GetOrCreateRewindPoint(Guid accountId) + { + var currentYear = DateTime.UtcNow.Year; + + var existingRewind = await db.RewindPoints + .Where(p => p.AccountId == accountId && p.Year == currentYear) + .OrderBy(p => p.CreatedAt) + .FirstOrDefaultAsync(); + if (existingRewind is not null) return existingRewind; + + return await CreateRewindPoint(accountId); + } +} diff --git a/DysonNetwork.Pass/Rewind/PassRewindService.cs b/DysonNetwork.Pass/Rewind/PassRewindService.cs new file mode 100644 index 0000000..7570cc9 --- /dev/null +++ b/DysonNetwork.Pass/Rewind/PassRewindService.cs @@ -0,0 +1,49 @@ +using System.Linq; +using DysonNetwork.Shared.Proto; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace DysonNetwork.Pass.Rewind; + +/// +/// Although the pass uses the rewind service call internally, no need for grpc. +/// But we created a service that produce the grpc type for consistency. +/// +public class PassRewindService(AppDatabase db) +{ + public async Task CreateRewindEvent(Guid accountId, int year) + { + var startDate = Instant.FromDateTimeUtc(new DateTime(year - 1, 12, 26)); + var endDate = Instant.FromDateTimeUtc(new DateTime(year, 12, 26)); + + var checkInDates = await db.AccountCheckInResults + .Where(a => a.CreatedAt >= startDate && a.CreatedAt < endDate) + .Where(a => a.AccountId == accountId) + .Select(a => a.CreatedAt.ToDateTimeUtc().Date) + .Distinct() + .OrderBy(d => d) + .ToListAsync(); + + var maxCheckInStrike = 0; + if (checkInDates.Count != 0) + { + maxCheckInStrike = checkInDates + .Select((d, i) => new { Date = d, Index = i }) + .GroupBy(x => x.Date.Subtract(new TimeSpan(x.Index, 0, 0, 0))) + .Select(g => g.Count()) + .Max(); + } + + var data = new Dictionary + { + ["max_check_in_strike"] = maxCheckInStrike, + }; + + return new RewindEvent + { + ServiceId = "pass", + AccountId = accountId.ToString(), + Data = GrpcTypeHelper.ConvertObjectToByteString(data) + }; + } +} diff --git a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs index d7f73dc..0eb3a8a 100644 --- a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs @@ -20,6 +20,7 @@ using DysonNetwork.Pass.Leveling; using DysonNetwork.Pass.Lotteries; using DysonNetwork.Pass.Mailer; using DysonNetwork.Pass.Realm; +using DysonNetwork.Pass.Rewind; using DysonNetwork.Pass.Safety; using DysonNetwork.Pass.Wallet.PaymentHandlers; using DysonNetwork.Shared.Cache; @@ -167,6 +168,9 @@ public static class ServiceCollectionExtensions services.Configure(configuration.GetSection("OidcProvider")); services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); services.AddHostedService(); diff --git a/DysonNetwork.Shared/Models/RewindPoint.cs b/DysonNetwork.Shared/Models/RewindPoint.cs new file mode 100644 index 0000000..2446fcb --- /dev/null +++ b/DysonNetwork.Shared/Models/RewindPoint.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace DysonNetwork.Shared.Models; + +public class SnRewindPoint : ModelBase +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public int Year { get; set; } = DateTime.UtcNow.Year; + + /// + /// Due to every year the Solar Network upgrade become better and better. + /// The rewind data might be incompatible at that time, + /// this field provide the clues for the client to parsing the data correctly. + /// + public int SchemaVersion { get; set; } = 1; + + [Column(TypeName = "jsonb")] public Dictionary Data { get; set; } = new(); + + public Guid AccountId { get; set; } + public SnAccount Account { get; set; } = null!; +} diff --git a/DysonNetwork.Shared/Models/SnRewindPoint.cs b/DysonNetwork.Shared/Models/SnRewindPoint.cs deleted file mode 100644 index 69ce03b..0000000 --- a/DysonNetwork.Shared/Models/SnRewindPoint.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.ComponentModel.DataAnnotations.Schema; - -namespace DysonNetwork.Shared.Models; - -public class SnRewindPoint -{ - public Guid Id { get; set; } = Guid.NewGuid(); - public int Year { get; set; } = DateTime.UtcNow.Year; - - [Column(TypeName = "jsonb")] public Dictionary Data { get; set; } = new(); - - public Guid AccountId { get; set; } - public SnAccount Account { get; set; } = null!; -} diff --git a/DysonNetwork.Shared/Proto/rewind.proto b/DysonNetwork.Shared/Proto/rewind.proto index fc14080..d70260e 100644 --- a/DysonNetwork.Shared/Proto/rewind.proto +++ b/DysonNetwork.Shared/Proto/rewind.proto @@ -12,6 +12,7 @@ message RewindEvent { message RequestRewindEvent { string account_id = 1; + int32 year = 2; } service RewindService { diff --git a/DysonNetwork.Sphere/Rewind/SphereRewindServiceGrpc.cs b/DysonNetwork.Sphere/Rewind/SphereRewindServiceGrpc.cs index 7307bb0..d93b1d4 100644 --- a/DysonNetwork.Sphere/Rewind/SphereRewindServiceGrpc.cs +++ b/DysonNetwork.Sphere/Rewind/SphereRewindServiceGrpc.cs @@ -3,6 +3,7 @@ using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Registry; using Grpc.Core; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace DysonNetwork.Sphere.Rewind; @@ -15,10 +16,15 @@ public class SphereRewindServiceGrpc( public override async Task GetRewindEvent(RequestRewindEvent request, ServerCallContext context) { var accountId = Guid.Parse(request.AccountId); + var year = request.Year; + + var startDate = Instant.FromDateTimeUtc(new DateTime(year - 1, 12, 26)); + var endDate = Instant.FromDateTimeUtc(new DateTime(year, 12, 26)); // Audience data var mostLovedPublisherClue = await db.PostReactions + .Where(a => a.CreatedAt >= startDate && a.CreatedAt < endDate) .Where(p => p.AccountId == accountId && p.Attitude == Shared.Models.PostReactionAttitude.Positive) .GroupBy(p => p.Post.PublisherId) .OrderByDescending(g => g.Count()) @@ -36,6 +42,7 @@ public class SphereRewindServiceGrpc( var mostLovedAudienceClue = await db.PostReactions + .Where(a => a.CreatedAt >= startDate && a.CreatedAt < endDate) .Where(pr => pr.Attitude == Shared.Models.PostReactionAttitude.Positive && publishers.Contains(pr.Post.PublisherId)) @@ -47,14 +54,15 @@ public class SphereRewindServiceGrpc( ? await remoteAccounts.GetAccount(mostLovedAudienceClue.AccountId) : null; - var posts = await db.Posts + var posts = db.Posts + .Where(a => a.CreatedAt >= startDate && a.CreatedAt < endDate) .Where(p => publishers.Contains(p.PublisherId)) - .ToListAsync(); - var postTotalCount = posts.Count; - var mostPopularPost = posts + .AsQueryable(); + var postTotalCount = await posts.CountAsync(); + var mostPopularPost = await posts .OrderByDescending(p => p.Upvotes - p.Downvotes) - .FirstOrDefault(); - var mostProductiveDay = posts + .FirstOrDefaultAsync(); + var mostProductiveDay = (await posts.Select(p => new { p.CreatedAt }).ToListAsync()) .GroupBy(p => p.CreatedAt.ToDateTimeUtc().Date) .OrderByDescending(g => g.Count()) .Select(g => new { Date = g.Key, PostCount = g.Count() }) @@ -94,4 +102,4 @@ public class SphereRewindServiceGrpc( Data = GrpcTypeHelper.ConvertObjectToByteString(data) }; } -} +} \ No newline at end of file diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index 528df8e..d25500b 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -37,6 +37,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -66,6 +67,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded