feat(chat): 글로벌/방 쿼터 정책 개편, 결제/조회/차단/이관 로직 반영
글로벌: 무료 40, UTC 20:00 lazy refill(유료 제거) 방: 무료 10, 무료 0 순간 now+6h, 경과 시 lazy refill(무료=10, next=null) 전송: 유료 우선, 무료 사용 시 글로벌/룸 동시 차감, 조건 불충족 예외 API: 방 쿼터 조회/구매 추가(구매 시 30캔, UseCan에 roomId:characterId 기록) next 계산: enter/send에서 경계 케이스 처리(max(room, global)) 대화 초기화: 유료 쿼터 새 방으로 이관
This commit is contained in:
@@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||
import kr.co.vividnext.sodalive.chat.character.image.CharacterImage
|
||||
import kr.co.vividnext.sodalive.chat.character.image.CharacterImageService
|
||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||
import kr.co.vividnext.sodalive.chat.quota.room.ChatRoomQuotaService
|
||||
import kr.co.vividnext.sodalive.chat.room.ChatMessage
|
||||
import kr.co.vividnext.sodalive.chat.room.ChatMessageType
|
||||
import kr.co.vividnext.sodalive.chat.room.ChatParticipant
|
||||
@@ -52,6 +53,7 @@ class ChatRoomService(
|
||||
private val canPaymentService: kr.co.vividnext.sodalive.can.payment.CanPaymentService,
|
||||
private val imageCloudFront: kr.co.vividnext.sodalive.aws.cloudfront.ImageContentCloudFront,
|
||||
private val chatQuotaService: kr.co.vividnext.sodalive.chat.quota.ChatQuotaService,
|
||||
private val chatRoomQuotaService: ChatRoomQuotaService,
|
||||
|
||||
@Value("\${weraser.api-key}")
|
||||
private val apiKey: String,
|
||||
@@ -376,8 +378,15 @@ class ChatRoomService(
|
||||
val messagesAsc = fetched.sortedBy { it.createdAt }
|
||||
val items = messagesAsc.map { toChatMessageItemDto(it, member) }
|
||||
|
||||
// 입장 시 Lazy refill 적용 후 상태 반환
|
||||
val quotaStatus = chatQuotaService.applyRefillOnEnterAndGetStatus(member.id!!)
|
||||
// 5-1) 글로벌 쿼터 Lazy refill
|
||||
val globalStatus = chatQuotaService.applyRefillOnEnterAndGetStatus(member.id!!)
|
||||
// 5-2) 룸 쿼터 Lazy refill + 상태
|
||||
val roomStatus = chatRoomQuotaService.applyRefillOnEnterAndGetStatus(
|
||||
memberId = member.id!!,
|
||||
chatRoomId = effectiveRoom.id!!,
|
||||
characterId = character.id!!,
|
||||
globalFree = globalStatus.totalRemaining
|
||||
)
|
||||
|
||||
// 선택적 캐릭터 이미지 서명 URL 생성 처리
|
||||
// 요구사항: baseRoom이 조건 불만족으로 동일 캐릭터의 내 활성 방으로 라우팅된 경우(bg 이미지 요청 무시)에는 null로 처리
|
||||
@@ -419,13 +428,42 @@ class ChatRoomService(
|
||||
}
|
||||
}
|
||||
|
||||
// 권고안에 따른 next 계산
|
||||
val nextForEnter: Long? = when {
|
||||
// roomPaid==0 && roomFree>0 && global<=0 → 글로벌 next
|
||||
roomStatus.remainingPaid == 0 && roomStatus.remainingFree > 0 && globalStatus.totalRemaining <= 0 ->
|
||||
globalStatus.nextRechargeAtEpochMillis
|
||||
// roomPaid==0 && roomFree==0 → (global<=0) ? max(roomNext, globalNext) : roomNext
|
||||
roomStatus.remainingPaid == 0 && roomStatus.remainingFree == 0 -> {
|
||||
val roomNext = roomStatus.nextRechargeAtEpochMillis
|
||||
val globalNext = globalStatus.nextRechargeAtEpochMillis
|
||||
if (globalStatus.totalRemaining <= 0) {
|
||||
if (roomNext == null) {
|
||||
globalNext
|
||||
} else if (globalNext == null) {
|
||||
roomNext
|
||||
} else {
|
||||
maxOf(
|
||||
roomNext,
|
||||
globalNext
|
||||
)
|
||||
}
|
||||
} else {
|
||||
roomNext
|
||||
}
|
||||
}
|
||||
// 그 외 기존 규칙: room total==0 → room next, else if global<=0 → global next, else null
|
||||
roomStatus.totalRemaining == 0 -> roomStatus.nextRechargeAtEpochMillis
|
||||
globalStatus.totalRemaining <= 0 -> globalStatus.nextRechargeAtEpochMillis
|
||||
else -> null
|
||||
}
|
||||
return ChatRoomEnterResponse(
|
||||
roomId = effectiveRoom.id!!,
|
||||
character = characterDto,
|
||||
messages = items,
|
||||
hasMoreMessages = hasMore,
|
||||
totalRemaining = quotaStatus.totalRemaining,
|
||||
nextRechargeAtEpoch = quotaStatus.nextRechargeAtEpochMillis,
|
||||
totalRemaining = roomStatus.totalRemaining,
|
||||
nextRechargeAtEpoch = nextForEnter,
|
||||
bgImageUrl = signedUrl
|
||||
)
|
||||
}
|
||||
@@ -602,8 +640,13 @@ class ChatRoomService(
|
||||
val sessionId = room.sessionId
|
||||
val characterUUID = character.characterUUID
|
||||
|
||||
// 5) 쿼터 확인 및 차감
|
||||
chatQuotaService.consumeOne(member.id!!)
|
||||
// 5) 쿼터 확인 및 차감 (유료 우선, 무료 사용 시 글로벌과 룸 동시 차감)
|
||||
val roomQuotaAfterConsume = chatRoomQuotaService.consumeOneForSend(
|
||||
memberId = member.id!!,
|
||||
chatRoomId = room.id!!,
|
||||
globalFreeProvider = { chatQuotaService.getStatus(member.id!!).totalRemaining },
|
||||
consumeGlobalFree = { chatQuotaService.consumeOneFree(member.id!!) }
|
||||
)
|
||||
|
||||
// 6) 외부 API 호출 (최대 3회 재시도)
|
||||
val characterReply = callExternalApiForChatSendWithRetry(userId, characterUUID, message, sessionId)
|
||||
@@ -646,7 +689,21 @@ class ChatRoomService(
|
||||
hasAccess = true
|
||||
)
|
||||
|
||||
val status = chatQuotaService.getStatus(member.id!!)
|
||||
// 발송 후 최신 잔여 수량 및 next 계산 규칙 적용
|
||||
val statusTotalRemaining = roomQuotaAfterConsume.totalRemaining
|
||||
val globalAfter = chatQuotaService.getStatus(member.id!!)
|
||||
val statusNextRechargeAt: Long? = when {
|
||||
// totalRemaining==0이고 (global<=0) → max(roomNext, globalNext)
|
||||
statusTotalRemaining == 0 && globalAfter.totalRemaining <= 0 -> {
|
||||
val roomNext = roomQuotaAfterConsume.nextRechargeAtEpochMillis
|
||||
val globalNext = globalAfter.nextRechargeAtEpochMillis
|
||||
if (roomNext == null) globalNext else if (globalNext == null) roomNext else maxOf(roomNext, globalNext)
|
||||
}
|
||||
|
||||
statusTotalRemaining == 0 -> roomQuotaAfterConsume.nextRechargeAtEpochMillis
|
||||
globalAfter.totalRemaining <= 0 -> globalAfter.nextRechargeAtEpochMillis
|
||||
else -> null
|
||||
}
|
||||
|
||||
// 8) 트리거 매칭 → 이미지 메시지 추가 저장(있을 경우)
|
||||
val matchedImage = findTriggeredCharacterImage(character.id!!, characterReply)
|
||||
@@ -676,15 +733,15 @@ class ChatRoomService(
|
||||
val imageDto = toChatMessageItemDto(imageMsg, member)
|
||||
return SendChatMessageResponse(
|
||||
messages = listOf(textDto, imageDto),
|
||||
totalRemaining = status.totalRemaining,
|
||||
nextRechargeAtEpoch = status.nextRechargeAtEpochMillis
|
||||
totalRemaining = statusTotalRemaining,
|
||||
nextRechargeAtEpoch = statusNextRechargeAt
|
||||
)
|
||||
}
|
||||
|
||||
return SendChatMessageResponse(
|
||||
messages = listOf(textDto),
|
||||
totalRemaining = status.totalRemaining,
|
||||
nextRechargeAtEpoch = status.nextRechargeAtEpochMillis
|
||||
totalRemaining = statusTotalRemaining,
|
||||
nextRechargeAtEpoch = statusNextRechargeAt
|
||||
)
|
||||
}
|
||||
|
||||
@@ -846,6 +903,8 @@ class ChatRoomService(
|
||||
memberId = member.id!!,
|
||||
needCan = 30,
|
||||
canUsage = CanUsage.CHAT_ROOM_RESET,
|
||||
chatRoomId = chatRoomId,
|
||||
characterId = character.id!!,
|
||||
container = container
|
||||
)
|
||||
|
||||
@@ -855,10 +914,14 @@ class ChatRoomService(
|
||||
// 4) 동일한 캐릭터와 새로운 채팅방 생성
|
||||
val created = createOrGetChatRoom(member, character.id!!)
|
||||
|
||||
// 5) 신규 채팅방 생성 성공 시 무료 채팅 횟수 10으로 설정
|
||||
chatQuotaService.resetFreeToDefault(member.id!!)
|
||||
|
||||
// 6) 생성된 채팅방 데이터 반환
|
||||
// 5) 신규 채팅방 생성 성공 시: 기존 방의 유료 쿼터를 새 방으로 이관
|
||||
chatRoomQuotaService.transferPaid(
|
||||
memberId = member.id!!,
|
||||
fromChatRoomId = chatRoomId,
|
||||
toChatRoomId = created.chatRoomId,
|
||||
toCharacterId = character.id!!
|
||||
)
|
||||
// 글로벌 무료 쿼터는 UTC 20:00 기준 lazy 충전이므로 별도의 초기화 불필요
|
||||
return created
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user