Pass rewind service and account rewind service

This commit is contained in:
2025-12-25 22:26:57 +08:00
parent 24836fc606
commit f0d6772dca
10 changed files with 168 additions and 28 deletions

View File

@@ -1,6 +0,0 @@
namespace DysonNetwork.Pass.Account.Rewind;
public class AccountRewindService(AppDatabase db)
{
}

View File

@@ -1,4 +1,4 @@
namespace DysonNetwork.Pass.Account.Rewind;
namespace DysonNetwork.Pass.Rewind;
public class AccountRewindController
{

View File

@@ -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<SnRewindPoint> CreateRewindPoint(Guid accountId)
{
var currentYear = DateTime.UtcNow.Year;
var rewindRequest = new RequestRewindEvent { AccountId = accountId.ToString(), Year = currentYear};
var rewindEventTasks = new List<Task<RewindEvent>>
{
passRewindSrv.CreateRewindEvent(accountId, currentYear),
CreateRewindServiceClient("sphere").GetRewindEventAsync(rewindRequest).ResponseAsync
};
var rewindEvents = await Task.WhenAll(rewindEventTasks);
var rewindData = rewindEvents.ToDictionary<RewindEvent, string, Dictionary<string, object?>>(
rewindEvent => rewindEvent.ServiceId,
rewindEvent => GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object?>>(rewindEvent.Data) ??
new Dictionary<string, object?>()
);
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<SnRewindPoint> 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);
}
}

View File

@@ -0,0 +1,49 @@
using System.Linq;
using DysonNetwork.Shared.Proto;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Pass.Rewind;
/// <summary>
/// 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.
/// </summary>
public class PassRewindService(AppDatabase db)
{
public async Task<RewindEvent> 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<string, object?>
{
["max_check_in_strike"] = maxCheckInStrike,
};
return new RewindEvent
{
ServiceId = "pass",
AccountId = accountId.ToString(),
Data = GrpcTypeHelper.ConvertObjectToByteString(data)
};
}
}

View File

@@ -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<OidcProviderOptions>(configuration.GetSection("OidcProvider"));
services.AddScoped<OidcProviderService>();
services.AddScoped<PassRewindService>();
services.AddScoped<AccountRewindService>();
services.AddHostedService<BroadcastEventHandler>();

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public int SchemaVersion { get; set; } = 1;
[Column(TypeName = "jsonb")] public Dictionary<string, object?> Data { get; set; } = new();
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
}

View File

@@ -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<string, string> Data { get; set; } = new();
public Guid AccountId { get; set; }
public SnAccount Account { get; set; } = null!;
}

View File

@@ -12,6 +12,7 @@ message RewindEvent {
message RequestRewindEvent {
string account_id = 1;
int32 year = 2;
}
service RewindService {

View File

@@ -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<RewindEvent> 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)
};
}
}
}

View File

@@ -37,6 +37,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbFunctionsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc1c46ed28c61e1caa79185e4375a8ae7cd11cd5ba8853dcb37577f93f2ca8d5_003FDbFunctionsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultInterpolatedStringHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F3e6f03b324974d80b20d1999c4a43881f83400_003F87_003F5967abaf_003FDefaultInterpolatedStringHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADiagnosticServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F47e01f36dea14a23aaea6e0391c1347ace00_003F3c_003F140e6d8b_003FDiagnosticServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADictionary_00602_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F19674998a88c4c42be3692b32f3379d21046510_003F40_003F495b0a36_003FDictionary_00602_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADirectory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fde_003F94973e27_003FDirectory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADockerComposeServiceExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4768773ea5864bf8b6fc7a1a3c6f6f311fc38_003F2d_003F5f83b17e_003FDockerComposeServiceExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEndpointConventionBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8bb08a178b5b43c5bac20a5a54159a5b2a800_003F8a_003F101938e3_003FEndpointConventionBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -66,6 +67,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFirebaseSender_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6aadc2cf048f477d8636fb2def7b73648200_003F5c_003F1f5bca3f_003FFirebaseSender_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AForwardedHeaders_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcfe5737f9bb84738979cbfedd11822a8ea00_003F50_003F9a335f87_003FForwardedHeaders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AForwardedTransformExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf3f51607a3e4e76b5b91640cd7409195c430_003F03_003F36e779df_003FForwardedTransformExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGrpcChannel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F33b697967214455ca048862a59bf98a457c60_003Fc0_003Fd99cb5be_003FGrpcChannel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fc181aff8c6ec418494a7efcfec578fc154e00_003Fd0_003Fcc905531_003FHttpContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpRequestHeaders_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb904f9896c4049fabd596decf1be9c381dc400_003F32_003F906beb77_003FHttpRequestHeaders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpStatusCode_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb3f2e07d4b3f4b42a41fbcf3137e534f3be00_003Fe2_003F215f9441_003FHttpStatusCode_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>