RewardChatHook.cs 2.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. using Application.Abstractions.Cache;
  2. using Application.Abstractions.Chat;
  3. using Application.Abstractions.Data;
  4. using Application.Helpers;
  5. using Microsoft.EntityFrameworkCore;
  6. using Microsoft.Extensions.DependencyInjection;
  7. using Microsoft.Extensions.Logging;
  8. namespace Infrastructure.Chat;
  9. /// <summary>
  10. /// 채팅 보상 훅 실구현 (d0 §2.2 / d3 M2). SendMessage 성공 시 ChatExpConfig.ChatXpPerMessage 를
  11. /// RewardService 로 지급(ActionType=Chat, 일일 캡 적용, XP 전용)한다.
  12. /// Singleton 등록(허브 계약 유지)이므로 scoped DbContext/Cache 는 스코프를 직접 생성해 사용한다.
  13. /// 예외는 밖으로 던지지 않는다 — 채팅 브로드캐스트에 영향 금지.
  14. /// </summary>
  15. internal sealed class RewardChatHook(IServiceScopeFactory scopeFactory, ILogger<RewardChatHook> logger) : IChatRewardHook
  16. {
  17. public async Task OnMessageSentAsync(int memberID, string roomKey, string content, CancellationToken ct = default)
  18. {
  19. if (memberID <= 0)
  20. {
  21. return;
  22. }
  23. try
  24. {
  25. await using var scope = scopeFactory.CreateAsyncScope();
  26. var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
  27. var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
  28. // ChatExpConfig 는 Config.Get.Response 에 노출되지 않으므로 수치만 DB 에서 직접 조회.
  29. var chatExp = await db.Config.AsNoTracking().OrderByDescending(c => c.ID).Select(c => c.ChatExp.ChatXpPerMessage).FirstOrDefaultAsync(ct);
  30. if (chatExp <= 0)
  31. {
  32. return;
  33. }
  34. await RewardService.GrantForChatAsync(db, cache, memberID, chatExp, DateTime.UtcNow, ct);
  35. await db.SaveChangesAsync(ct);
  36. }
  37. catch (Exception ex)
  38. {
  39. // 채팅 흐름을 막지 않는다 — 지급 실패만 로깅.
  40. logger.LogWarning(ex, "채팅 보상 지급 실패 (memberID={MemberID}, roomKey={RoomKey})", memberID, roomKey);
  41. }
  42. }
  43. }