feat(admin-chat-calculate): 캐릭터별 정산 조회 API 추가
This commit is contained in:
parent
3dc9dd1f35
commit
eec63cc7b2
|
@ -0,0 +1,32 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat.calculate
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@RequestMapping("/admin/chat/calculate")
|
||||
class AdminChatCalculateController(
|
||||
private val service: AdminChatCalculateService
|
||||
) {
|
||||
@GetMapping("/characters")
|
||||
fun getCharacterCalculate(
|
||||
@RequestParam startDateStr: String,
|
||||
@RequestParam endDateStr: String,
|
||||
@RequestParam(required = false, defaultValue = "TOTAL_SALES_DESC") sort: ChatCharacterCalculateSort,
|
||||
pageable: Pageable
|
||||
) = ApiResponse.ok(
|
||||
service.getCharacterCalculate(
|
||||
startDateStr,
|
||||
endDateStr,
|
||||
sort,
|
||||
pageable.offset,
|
||||
pageable.pageSize
|
||||
)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat.calculate
|
||||
|
||||
import com.querydsl.core.types.dsl.CaseBuilder
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
|
||||
import kr.co.vividnext.sodalive.chat.character.QChatCharacter.chatCharacter
|
||||
import kr.co.vividnext.sodalive.chat.character.image.QCharacterImage.characterImage
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
class AdminChatCalculateQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) {
|
||||
fun getCharacterCalculate(
|
||||
startUtc: LocalDateTime,
|
||||
endInclusiveUtc: LocalDateTime,
|
||||
sort: ChatCharacterCalculateSort,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<ChatCharacterCalculateQueryData> {
|
||||
val imageCanExpr = CaseBuilder()
|
||||
.`when`(useCan.canUsage.eq(CanUsage.CHARACTER_IMAGE_PURCHASE))
|
||||
.then(useCan.can.add(useCan.rewardCan))
|
||||
.otherwise(0)
|
||||
|
||||
val messageCanExpr = CaseBuilder()
|
||||
.`when`(useCan.canUsage.eq(CanUsage.CHAT_MESSAGE_PURCHASE))
|
||||
.then(useCan.can.add(useCan.rewardCan))
|
||||
.otherwise(0)
|
||||
|
||||
val imageSum = imageCanExpr.sum()
|
||||
val messageSum = messageCanExpr.sum()
|
||||
val totalSum = imageSum.add(messageSum)
|
||||
|
||||
val query = queryFactory
|
||||
.select(
|
||||
QChatCharacterCalculateQueryData(
|
||||
chatCharacter.id,
|
||||
chatCharacter.name,
|
||||
chatCharacter.imagePath,
|
||||
imageSum,
|
||||
messageSum
|
||||
)
|
||||
)
|
||||
.from(useCan)
|
||||
.innerJoin(useCan.characterImage, characterImage)
|
||||
.innerJoin(characterImage.chatCharacter, chatCharacter)
|
||||
.where(
|
||||
useCan.isRefund.isFalse
|
||||
.and(useCan.canUsage.`in`(CanUsage.CHARACTER_IMAGE_PURCHASE, CanUsage.CHAT_MESSAGE_PURCHASE))
|
||||
.and(useCan.createdAt.goe(startUtc))
|
||||
.and(useCan.createdAt.loe(endInclusiveUtc))
|
||||
)
|
||||
.groupBy(chatCharacter.id, chatCharacter.name, chatCharacter.imagePath)
|
||||
|
||||
when (sort) {
|
||||
ChatCharacterCalculateSort.TOTAL_SALES_DESC ->
|
||||
query.orderBy(totalSum.desc(), chatCharacter.id.desc())
|
||||
|
||||
ChatCharacterCalculateSort.LATEST_DESC ->
|
||||
query.orderBy(chatCharacter.id.desc(), totalSum.desc())
|
||||
}
|
||||
|
||||
return query
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.fetch()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat.calculate
|
||||
|
||||
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Service
|
||||
class AdminChatCalculateService(
|
||||
private val repository: AdminChatCalculateQueryRepository
|
||||
) {
|
||||
private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
private val kstZone: ZoneId = ZoneId.of("Asia/Seoul")
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getCharacterCalculate(
|
||||
startDateStr: String,
|
||||
endDateStr: String,
|
||||
sort: ChatCharacterCalculateSort,
|
||||
offset: Long,
|
||||
pageSize: Int
|
||||
): List<ChatCharacterCalculateItem> {
|
||||
// 날짜 유효성 검증 (KST 기준)
|
||||
val startDate = LocalDate.parse(startDateStr, dateFormatter)
|
||||
val endDate = LocalDate.parse(endDateStr, dateFormatter)
|
||||
val todayKst = LocalDate.now(kstZone)
|
||||
|
||||
require(!endDate.isAfter(todayKst)) { "끝 날짜는 오늘 날짜까지만 입력 가능합니다." }
|
||||
require(!startDate.isAfter(endDate)) { "시작 날짜는 끝 날짜보다 이후일 수 없습니다." }
|
||||
require(!endDate.isAfter(startDate.plusMonths(6))) { "조회 가능 기간은 최대 6개월입니다." }
|
||||
|
||||
// 입력은 KST, UTC로 변환해 [start, end(미포함)] 조회
|
||||
val startUtc = startDateStr.convertLocalDateTime()
|
||||
val endInclusiveUtc = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
|
||||
|
||||
val rows = repository.getCharacterCalculate(startUtc, endInclusiveUtc, sort, offset, pageSize.toLong())
|
||||
return rows.map { it.toItem() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat.calculate
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
// 정렬 옵션
|
||||
enum class ChatCharacterCalculateSort {
|
||||
TOTAL_SALES_DESC,
|
||||
LATEST_DESC
|
||||
}
|
||||
|
||||
// QueryDSL 프로젝션용 DTO
|
||||
data class ChatCharacterCalculateQueryData @QueryProjection constructor(
|
||||
val characterId: Long,
|
||||
val characterName: String,
|
||||
val characterImagePath: String?,
|
||||
val imagePurchaseCan: Int?,
|
||||
val messagePurchaseCan: Int?
|
||||
)
|
||||
|
||||
// 응답 DTO
|
||||
data class ChatCharacterCalculateItem(
|
||||
@JsonProperty("characterId") val characterId: Long,
|
||||
@JsonProperty("characterImage") val characterImage: String?,
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("imagePurchaseCan") val imagePurchaseCan: Int,
|
||||
@JsonProperty("messagePurchaseCan") val messagePurchaseCan: Int,
|
||||
@JsonProperty("totalCan") val totalCan: Int,
|
||||
@JsonProperty("totalKrw") val totalKrw: Int,
|
||||
@JsonProperty("settlementKrw") val settlementKrw: Int
|
||||
)
|
||||
|
||||
fun ChatCharacterCalculateQueryData.toItem(): ChatCharacterCalculateItem {
|
||||
val image = imagePurchaseCan ?: 0
|
||||
val message = messagePurchaseCan ?: 0
|
||||
val total = image + message
|
||||
val totalKrw = BigDecimal(total).multiply(BigDecimal(100))
|
||||
val settlement = totalKrw.multiply(BigDecimal("0.10")).setScale(0, RoundingMode.HALF_UP)
|
||||
|
||||
return ChatCharacterCalculateItem(
|
||||
characterId = characterId,
|
||||
characterImage = characterImagePath,
|
||||
name = characterName,
|
||||
imagePurchaseCan = image,
|
||||
messagePurchaseCan = message,
|
||||
totalCan = total,
|
||||
totalKrw = totalKrw.toInt(),
|
||||
settlementKrw = settlement.toInt()
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue