| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 |
- using Application.Abstractions.Cache;
- using Application.Abstractions.Chat;
- using Application.Abstractions.Data;
- using Application.Helpers;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Logging;
- namespace Infrastructure.Chat;
- /// <summary>
- /// 채팅 보상 훅 실구현 (d0 §2.2 / d3 M2). SendMessage 성공 시 ChatExpConfig.ChatXpPerMessage 를
- /// RewardService 로 지급(ActionType=Chat, 일일 캡 적용, XP 전용)한다.
- /// Singleton 등록(허브 계약 유지)이므로 scoped DbContext/Cache 는 스코프를 직접 생성해 사용한다.
- /// 예외는 밖으로 던지지 않는다 — 채팅 브로드캐스트에 영향 금지.
- /// </summary>
- internal sealed class RewardChatHook(IServiceScopeFactory scopeFactory, ILogger<RewardChatHook> logger) : IChatRewardHook
- {
- public async Task OnMessageSentAsync(int memberID, string roomKey, string content, CancellationToken ct = default)
- {
- if (memberID <= 0)
- {
- return;
- }
- try
- {
- await using var scope = scopeFactory.CreateAsyncScope();
- var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
- var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
- // ChatExpConfig 는 Config.Get.Response 에 노출되지 않으므로 수치만 DB 에서 직접 조회.
- var chatExp = await db.Config.AsNoTracking().OrderByDescending(c => c.ID).Select(c => c.ChatExp.ChatXpPerMessage).FirstOrDefaultAsync(ct);
- if (chatExp <= 0)
- {
- return;
- }
- await RewardService.GrantForChatAsync(db, cache, memberID, chatExp, DateTime.UtcNow, ct);
- await db.SaveChangesAsync(ct);
- }
- catch (Exception ex)
- {
- // 채팅 흐름을 막지 않는다 — 지급 실패만 로깅.
- logger.LogWarning(ex, "채팅 보상 지급 실패 (memberID={MemberID}, roomKey={RoomKey})", memberID, roomKey);
- }
- }
- }
|