Handler.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Application.Features.Api.Feed.Shared;
  4. using Microsoft.EntityFrameworkCore;
  5. namespace Application.Features.Api.Feed.GetForYou;
  6. public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
  7. {
  8. public async Task<Response> Handle(Query request, CancellationToken ct)
  9. {
  10. var followingIDs = await db.MemberFollow.AsNoTracking()
  11. .Where(f => f.FollowerMemberID == request.MemberID && !f.IsMuted)
  12. .Select(f => f.FolloweeMemberID)
  13. .ToListAsync(ct);
  14. var since = DateTime.UtcNow.AddDays(-14);
  15. var take = Math.Max(20, request.PerPage * 3);
  16. var followingRows = followingIDs.Count > 0
  17. ? await QueryRowsAsync(since, take, p => followingIDs.Contains(p.MemberID), byRecency: true, ct)
  18. : [];
  19. var trendingRows = await QueryRowsAsync(since, take, p => !followingIDs.Contains(p.MemberID), byRecency: false, ct);
  20. var merged = new List<Row>();
  21. var seen = new HashSet<int>();
  22. var followTake = (int)Math.Round((followingRows.Count + trendingRows.Count) * 0.7);
  23. foreach (var r in followingRows.Take(followTake))
  24. {
  25. if (seen.Add(r.ID))
  26. {
  27. merged.Add(r);
  28. }
  29. }
  30. foreach (var r in trendingRows)
  31. {
  32. if (seen.Add(r.ID))
  33. {
  34. merged.Add(r);
  35. }
  36. }
  37. merged = [..merged.OrderByDescending(r => r.CreatedAt)];
  38. var total = merged.Count;
  39. var skip = (request.Page - 1) * request.PerPage;
  40. var list = merged.Skip(skip).Take(request.PerPage).Select(r => new FeedCardRow
  41. {
  42. PostID = r.ID,
  43. AuthorSID = r.AuthorSID,
  44. AuthorName = r.AuthorName,
  45. AuthorThumb = r.AuthorThumb,
  46. AuthorSummary = r.AuthorSummary,
  47. IsCreator = r.IsCreator,
  48. ChannelSID = r.ChannelSID,
  49. Subject = FeedContent.Excerpt(r.Content, 60),
  50. ContentExcerpt = FeedContent.Excerpt(r.Content, 200),
  51. Thumbnail = r.Thumbnail,
  52. Images = r.Images,
  53. Medias = r.Medias,
  54. Likes = r.Likes,
  55. Comments = r.Comments,
  56. Bookmarks = r.Bookmarks,
  57. Views = r.Views,
  58. Tags = [..r.Tags.Select(t => new FeedCardRow.TagRef { Name = t.Name, Slug = t.Slug })],
  59. CreatedAt = r.CreatedAt
  60. }).ToList();
  61. return new Response { Total = total, List = list };
  62. }
  63. private async Task<List<Row>> QueryRowsAsync(
  64. DateTime since,
  65. int take,
  66. System.Linq.Expressions.Expression<Func<Domain.Entities.Feed.FeedPost, bool>> additionalFilter,
  67. bool byRecency,
  68. CancellationToken ct
  69. ) {
  70. var query = db.FeedPost.AsNoTracking()
  71. .Where(p => !p.IsDeleted && p.CreatedAt >= since)
  72. .Where(additionalFilter);
  73. var ordered = byRecency
  74. ? query.OrderByDescending(p => p.CreatedAt)
  75. : query.OrderByDescending(p => p.Likes + p.Comments * 2 + p.Views * 0.1).ThenByDescending(p => p.CreatedAt);
  76. return await ordered.Take(take).Select(p => new Row
  77. {
  78. ID = p.ID,
  79. AuthorSID = p.Member.SID,
  80. AuthorName = p.Member.Name,
  81. AuthorThumb = p.Member.Thumb,
  82. AuthorSummary = p.Member.Summary,
  83. IsCreator = p.Member.IsCreator,
  84. ChannelSID = p.Member.Channel != null ? p.Member.Channel.SID : null,
  85. Content = p.Content,
  86. Thumbnail = p.Thumbnail,
  87. Images = p.Images,
  88. Medias = p.Medias,
  89. Likes = p.Likes,
  90. Comments = p.Comments,
  91. Bookmarks = p.Bookmarks,
  92. Views = p.Views,
  93. Tags = p.FeedPostTag.Select(pt => new TagRow { Name = pt.FeedTag.Name, Slug = pt.FeedTag.Slug }).ToList(),
  94. CreatedAt = p.CreatedAt
  95. }).ToListAsync(ct);
  96. }
  97. private sealed class Row
  98. {
  99. public int ID { get; set; }
  100. public string? AuthorSID { get; set; }
  101. public string? AuthorName { get; set; }
  102. public string? AuthorThumb { get; set; }
  103. public string? AuthorSummary { get; set; }
  104. public bool IsCreator { get; set; }
  105. public string? ChannelSID { get; set; }
  106. public string Content { get; set; } = default!;
  107. public string? Thumbnail { get; set; }
  108. public byte Images { get; set; }
  109. public byte Medias { get; set; }
  110. public int Likes { get; set; }
  111. public int Comments { get; set; }
  112. public int Bookmarks { get; set; }
  113. public int Views { get; set; }
  114. public DateTime CreatedAt { get; set; }
  115. public List<TagRow> Tags { get; set; } = [];
  116. }
  117. private sealed class TagRow
  118. {
  119. public string Name { get; set; } = default!;
  120. public string Slug { get; set; } = default!;
  121. }
  122. }