Marking views

This commit is contained in:
LittleSheep 2025-06-19 23:54:25 +08:00
parent eb1c283971
commit cfa63c7c93
6 changed files with 130 additions and 1 deletions

View File

@ -69,9 +69,14 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
var postsId = posts.Select(e => e.Id).ToList();
var reactionMaps = await ps.GetPostReactionMapBatch(postsId);
foreach (var post in posts)
{
post.ReactionsCount =
reactionMaps.TryGetValue(post.Id, out var count) ? count : new Dictionary<string, int>();
// Track view for each post in the feed
await ps.IncreaseViewCount(post.Id, currentUser.Id.ToString());
}
// Formatting data
foreach (var post in posts)
activities.Add(post.ToActivity());

View File

@ -58,9 +58,21 @@ public class PostController(
var postsId = posts.Select(e => e.Id).ToList();
var reactionMaps = await ps.GetPostReactionMapBatch(postsId);
foreach (var post in posts)
{
post.ReactionsCount =
reactionMaps.TryGetValue(post.Id, out var count) ? count : new Dictionary<string, int>();
// Track view for each post in the list
if (currentUser != null)
{
await ps.IncreaseViewCount(post.Id, currentUser.Id.ToString());
}
else
{
await ps.IncreaseViewCount(post.Id);
}
}
Response.Headers["X-Total"] = totalCount.ToString();
return Ok(posts);
@ -85,6 +97,9 @@ public class PostController(
post.ReactionsCount = await ps.GetPostReactionMap(post.Id);
// Track view - use the account ID as viewer ID if user is logged in
await ps.IncreaseViewCount(post.Id, currentUser?.Id.ToString());
return Ok(post);
}

View File

@ -13,7 +13,9 @@ public class PostService(
AppDatabase db,
FileReferenceService fileRefService,
IStringLocalizer<NotificationResource> localizer,
IServiceScopeFactory factory
IServiceScopeFactory factory,
FlushBufferService flushBuffer,
ICacheService cacheService
)
{
private const string PostFileUsageIdentifier = "post";
@ -365,6 +367,40 @@ public class PostService(
);
}
/// <summary>
/// Increases the view count for a post.
/// Uses the flush buffer service to batch database updates for better performance.
/// </summary>
/// <param name="postId">The ID of the post to mark as viewed</param>
/// <param name="viewerId">Optional viewer ID for unique view counting (anonymous if null)</param>
/// <returns>Task representing the asynchronous operation</returns>
public async Task IncreaseViewCount(Guid postId, string? viewerId = null)
{
// Check if this view is already counted in cache to prevent duplicate counting
if (!string.IsNullOrEmpty(viewerId))
{
var cacheKey = $"post:view:{postId}:{viewerId}";
var (found, _) = await cacheService.GetAsyncWithStatus<bool>(cacheKey);
if (found)
{
// Already viewed by this user recently, don't count again
return;
}
// Mark as viewed in cache for 1 hour to prevent duplicate counting
await cacheService.SetAsync(cacheKey, true, TimeSpan.FromHours(1));
}
// Add view info to flush buffer
flushBuffer.Enqueue(new PostViewInfo
{
PostId = postId,
ViewerId = viewerId,
ViewedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
});
}
public async Task<List<Post>> LoadPublishers(List<Post> posts)
{
var publisherIds = posts

View File

@ -0,0 +1,10 @@
using NodaTime;
namespace DysonNetwork.Sphere.Post;
public class PostViewInfo
{
public Guid PostId { get; set; }
public string? ViewerId { get; set; }
public Instant ViewedAt { get; set; }
}

View File

@ -195,6 +195,7 @@ builder.Services.AddSingleton<FlushBufferService>();
builder.Services.AddScoped<ActionLogFlushHandler>();
builder.Services.AddScoped<MessageReadReceiptFlushHandler>();
builder.Services.AddScoped<LastActiveFlushHandler>();
builder.Services.AddScoped<PostViewFlushHandler>();
// The handlers for websocket
builder.Services.AddScoped<IWebSocketPacketHandler, MessageReadHandler>();
@ -282,6 +283,16 @@ builder.Services.AddQuartz(q =>
.WithIntervalInMinutes(5)
.RepeatForever())
);
var postViewFlushJob = new JobKey("PostViewFlush");
q.AddJob<PostViewFlushJob>(opts => opts.WithIdentity(postViewFlushJob));
q.AddTrigger(opts => opts
.ForJob(postViewFlushJob)
.WithIdentity("PostViewFlushTrigger")
.WithSimpleSchedule(o => o
.WithIntervalInMinutes(1)
.RepeatForever())
);
});
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);

View File

@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Quartz;
namespace DysonNetwork.Sphere.Storage.Handlers;
public class PostViewFlushHandler(IServiceProvider serviceProvider) : IFlushHandler<Post.PostViewInfo>
{
public async Task FlushAsync(IReadOnlyList<Post.PostViewInfo> items)
{
using var scope = serviceProvider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
// Group views by post
var postViews = items
.GroupBy(x => x.PostId)
.ToDictionary(g => g.Key, g => g.ToList());
// Calculate total views and unique views per post
foreach (var postId in postViews.Keys)
{
// Calculate unique views by distinct viewer IDs (not null)
var uniqueViews = postViews[postId]
.Where(v => !string.IsNullOrEmpty(v.ViewerId))
.Select(v => v.ViewerId)
.Distinct()
.Count();
// Total views is just the count of all items for this post
var totalViews = postViews[postId].Count;
// Update the post in the database
await db.Posts
.Where(p => p.Id == postId)
.ExecuteUpdateAsync(p => p
.SetProperty(x => x.ViewsTotal, x => x.ViewsTotal + totalViews)
.SetProperty(x => x.ViewsUnique, x => x.ViewsUnique + uniqueViews));
// Invalidate any cache entries for this post
await cache.RemoveAsync($"post:{postId}");
}
}
}
public class PostViewFlushJob(FlushBufferService fbs, PostViewFlushHandler hdl) : IJob
{
public async Task Execute(IJobExecutionContext context)
{
await fbs.FlushAsync(hdl);
}
}