| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Messaging;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Features.Api.Ranking.GetRising;
- /// <summary>
- /// 최근 7일 vs 이전 7일 후원 증가율 기준 급상승 크리에이터.
- /// 집계는 실시간 Donation 테이블로 계산 (최근성이 중요하므로 스냅샷 아님).
- /// </summary>
- internal sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
- {
- public async Task<Response> Handle(Query request, CancellationToken ct)
- {
- var limit = request.Limit is < 1 or > 100 ? 20 : request.Limit;
- var now = DateTime.UtcNow;
- var recent7End = now;
- var recent7Start = now.AddDays(-7);
- var prev7Start = now.AddDays(-14);
- // 최근 7일 집계
- var recent = await db.Donation.AsNoTracking()
- .Where(d => d.CreatedAt >= recent7Start && d.CreatedAt < recent7End && !d.IsTest)
- .GroupBy(d => d.ReceiverMemberID)
- .Select(g => new
- {
- MemberID = g.Key,
- Amount = g.Sum(d => (long)d.NetAmount),
- DonorCount = g.Select(d => d.SponsorMemberID).Distinct().Count()
- })
- .ToListAsync(ct);
- if (recent.Count == 0)
- {
- return new Response([], now);
- }
- // 이전 7일 집계
- var memberIds = recent.Select(r => r.MemberID).ToList();
- var previous = await db.Donation.AsNoTracking()
- .Where(d => d.CreatedAt >= prev7Start && d.CreatedAt < recent7Start && !d.IsTest && memberIds.Contains(d.ReceiverMemberID))
- .GroupBy(d => d.ReceiverMemberID)
- .Select(g => new { MemberID = g.Key, Amount = g.Sum(d => (long)d.NetAmount) })
- .ToDictionaryAsync(g => g.MemberID, g => g.Amount, ct);
- var rising = recent.Select(r => {
- var prevAmount = previous.GetValueOrDefault(r.MemberID);
- double rate;
- if (prevAmount <= 0)
- {
- rate = r.Amount > 0 ? 1000.0 : 0.0; // 신규 유입 — 1000% 취급
- }
- else
- {
- rate = ((double)(r.Amount - prevAmount) / prevAmount) * 100.0;
- }
- return new { r.MemberID, r.Amount, r.DonorCount, PreviousAmount = prevAmount, Rate = rate };
- })
- .Where(r => r.Rate > 0)
- .OrderByDescending(r => r.Rate).ThenByDescending(r => r.Amount)
- .Take(limit)
- .ToList();
- var memberInfo = await db.Member.AsNoTracking()
- .Where(m => rising.Select(r => r.MemberID).Contains(m.ID))
- .Select(m => new { m.ID, m.Name, m.Thumb })
- .ToListAsync(ct);
- var memberDict = memberInfo.ToDictionary(m => m.ID, m => m);
- var channels = await db.Channel.AsNoTracking()
- .Where(c => rising.Select(r => r.MemberID).Contains(c.MemberID))
- .Select(c => new
- {
- c.MemberID,
- c.Name,
- c.ThumbnailUrl
- })
- .ToListAsync(ct);
- var channelDict = channels.GroupBy(c => c.MemberID).ToDictionary(g => g.Key, g => g.First());
- var entries = rising.Select(r => {
- var m = memberDict.GetValueOrDefault(r.MemberID);
- var ch = channelDict.GetValueOrDefault(r.MemberID);
- // Member.Thumb 우선, 없으면 Channel.ThumbnailUrl(YouTube 채널 썸네일) 폴백
- return new RisingEntry(
- r.MemberID,
- m?.Name ?? "",
- m?.Thumb ?? ch?.ThumbnailUrl,
- ch?.Name,
- r.Amount,
- r.PreviousAmount,
- r.Rate,
- r.DonorCount
- );
- }).ToList();
- return new Response(entries, now);
- }
- }
|