feat(original-series-calculate): 오리지널 시리즈 정산 내역 조회를 추가한다
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.originalSeries
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
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
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
@RestController
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@RequestMapping("/admin/calculate")
|
||||
class AdminOriginalSeriesCalculateController(
|
||||
private val service: AdminOriginalSeriesCalculateService
|
||||
) {
|
||||
@GetMapping("/original-series/owners")
|
||||
fun getOriginalSeriesOwners() = ApiResponse.ok(service.getOriginalSeriesOwners())
|
||||
|
||||
@GetMapping("/original-series/settlement-details")
|
||||
fun getSettlementDetails(
|
||||
@RequestParam("startDate") startDateStr: String,
|
||||
@RequestParam("endDate") endDateStr: String,
|
||||
@RequestParam memberId: Long,
|
||||
pageable: Pageable
|
||||
) = ApiResponse.ok(
|
||||
service.getSettlementDetails(
|
||||
startDateStr = startDateStr,
|
||||
endDateStr = endDateStr,
|
||||
memberId = memberId,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize.toLong()
|
||||
)
|
||||
)
|
||||
|
||||
@GetMapping("/original-series/settlement-details/excel")
|
||||
fun downloadSettlementDetailsExcel(
|
||||
@RequestParam("startDate") startDateStr: String,
|
||||
@RequestParam("endDate") endDateStr: String
|
||||
): ResponseEntity<StreamingResponseBody> = createExcelResponse(
|
||||
fileName = "original-series-settlement-details.xlsx",
|
||||
response = service.downloadSettlementDetailsExcel(startDateStr, endDateStr)
|
||||
)
|
||||
|
||||
private fun createExcelResponse(
|
||||
fileName: String,
|
||||
response: StreamingResponseBody
|
||||
): ResponseEntity<StreamingResponseBody> {
|
||||
val encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replace("+", "%20")
|
||||
val headers = HttpHeaders().apply {
|
||||
add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''$encodedFileName")
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.headers(headers)
|
||||
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||
.body(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.originalSeries
|
||||
|
||||
import com.querydsl.core.types.dsl.BooleanExpression
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||
import kr.co.vividnext.sodalive.content.order.QOrder.order
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent
|
||||
import kr.co.vividnext.sodalive.member.QMember.member
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
class AdminOriginalSeriesCalculateQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) {
|
||||
fun getOriginalSeriesOwners(): List<GetAdminOriginalSeriesOwnerResponse> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetAdminOriginalSeriesOwnerResponse(
|
||||
member.id,
|
||||
member.nickname
|
||||
)
|
||||
)
|
||||
.from(series)
|
||||
.innerJoin(series.member, member)
|
||||
.where(
|
||||
series.isOriginal.isTrue
|
||||
.and(series.isActive.isTrue)
|
||||
.and(member.isActive.isTrue)
|
||||
)
|
||||
.groupBy(member.id, member.nickname)
|
||||
.orderBy(member.id.asc())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
fun getSettlementDetailTotalCount(
|
||||
startDate: LocalDateTime,
|
||||
endDate: LocalDateTime,
|
||||
memberId: Long
|
||||
): Int {
|
||||
return queryFactory
|
||||
.select(audioContent.id)
|
||||
.from(order)
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.innerJoin(seriesContent).on(seriesContent.content.id.eq(audioContent.id))
|
||||
.innerJoin(seriesContent.series, series)
|
||||
.innerJoin(series.member, member)
|
||||
.where(baseWhereCondition(startDate, endDate, memberId))
|
||||
.groupBy(
|
||||
series.id,
|
||||
series.title,
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
order.type,
|
||||
audioContent.price
|
||||
)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
|
||||
fun getSettlementDetails(
|
||||
startDate: LocalDateTime,
|
||||
endDate: LocalDateTime,
|
||||
memberId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<GetAdminOriginalSeriesSettlementDetailQueryData> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetAdminOriginalSeriesSettlementDetailQueryData(
|
||||
series.title,
|
||||
audioContent.title,
|
||||
audioContent.price,
|
||||
order.type,
|
||||
order.id.count(),
|
||||
order.can.sum().coalesce(0),
|
||||
order.point.sum().coalesce(0)
|
||||
)
|
||||
)
|
||||
.from(order)
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.innerJoin(seriesContent).on(seriesContent.content.id.eq(audioContent.id))
|
||||
.innerJoin(seriesContent.series, series)
|
||||
.innerJoin(series.member, member)
|
||||
.where(baseWhereCondition(startDate, endDate, memberId))
|
||||
.groupBy(
|
||||
series.id,
|
||||
series.title,
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
order.type,
|
||||
audioContent.price
|
||||
)
|
||||
.orderBy(series.id.asc(), audioContent.id.asc(), order.type.asc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
private fun baseWhereCondition(
|
||||
startDate: LocalDateTime,
|
||||
endDate: LocalDateTime,
|
||||
memberId: Long
|
||||
): BooleanExpression {
|
||||
return series.isOriginal.isTrue
|
||||
.and(series.isActive.isTrue)
|
||||
.and(member.isActive.isTrue)
|
||||
.and(member.id.eq(memberId))
|
||||
.and(order.isActive.isTrue)
|
||||
.and(order.createdAt.goe(startDate))
|
||||
.and(order.createdAt.loe(endDate))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.originalSeries
|
||||
|
||||
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
||||
import org.apache.poi.ss.usermodel.Sheet
|
||||
import org.apache.poi.ss.util.WorkbookUtil
|
||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class AdminOriginalSeriesCalculateService(
|
||||
private val repository: AdminOriginalSeriesCalculateQueryRepository
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun getOriginalSeriesOwners(): List<GetAdminOriginalSeriesOwnerResponse> {
|
||||
return repository.getOriginalSeriesOwners()
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getSettlementDetails(
|
||||
startDateStr: String,
|
||||
endDateStr: String,
|
||||
memberId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): GetAdminOriginalSeriesSettlementDetailListResponse {
|
||||
val (startDate, endDate) = toDateRange(startDateStr, endDateStr)
|
||||
val totalCount = repository.getSettlementDetailTotalCount(startDate, endDate, memberId)
|
||||
val items = repository
|
||||
.getSettlementDetails(startDate, endDate, memberId, offset, limit)
|
||||
.map { it.toResponse() }
|
||||
|
||||
return GetAdminOriginalSeriesSettlementDetailListResponse(totalCount, items)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun downloadSettlementDetailsExcel(startDateStr: String, endDateStr: String): StreamingResponseBody {
|
||||
val (startDate, endDate) = toDateRange(startDateStr, endDateStr)
|
||||
val owners = repository.getOriginalSeriesOwners()
|
||||
|
||||
return StreamingResponseBody { outputStream ->
|
||||
val workbook = SXSSFWorkbook(100)
|
||||
try {
|
||||
if (owners.isEmpty()) {
|
||||
writeHeaders(workbook.createSheet("오리지널 시리즈 정산"))
|
||||
} else {
|
||||
owners.forEach { owner ->
|
||||
val sheet = workbook.createSheet(toSheetName(owner))
|
||||
writeHeaders(sheet)
|
||||
|
||||
val totalCount = repository.getSettlementDetailTotalCount(startDate, endDate, owner.memberId)
|
||||
val items = if (totalCount == 0) {
|
||||
emptyList()
|
||||
} else {
|
||||
repository.getSettlementDetails(startDate, endDate, owner.memberId, 0L, totalCount.toLong())
|
||||
.map { it.toResponse() }
|
||||
}
|
||||
|
||||
writeRows(sheet, items)
|
||||
}
|
||||
}
|
||||
|
||||
workbook.write(outputStream)
|
||||
outputStream.flush()
|
||||
} finally {
|
||||
workbook.dispose()
|
||||
workbook.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeHeaders(sheet: Sheet) {
|
||||
val headerRow = sheet.createRow(0)
|
||||
EXCEL_HEADERS.forEachIndexed { index, value ->
|
||||
headerRow.createCell(index).setCellValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeRows(sheet: Sheet, items: List<GetAdminOriginalSeriesSettlementDetailResponse>) {
|
||||
items.forEachIndexed { index, item ->
|
||||
val row = sheet.createRow(index + 1)
|
||||
row.createCell(0).setCellValue(item.seriesTitle)
|
||||
row.createCell(1).setCellValue(item.contentTitle)
|
||||
row.createCell(2).setCellValue(item.price.toDouble())
|
||||
row.createCell(3).setCellValue(item.orderType)
|
||||
row.createCell(4).setCellValue(item.salesCount.toDouble())
|
||||
row.createCell(5).setCellValue(item.totalCan.toDouble())
|
||||
row.createCell(6).setCellValue(item.totalPoint.toDouble())
|
||||
}
|
||||
}
|
||||
|
||||
private fun toSheetName(owner: GetAdminOriginalSeriesOwnerResponse): String {
|
||||
return WorkbookUtil.createSafeSheetName("${owner.memberId}_${owner.nickname}")
|
||||
}
|
||||
|
||||
private fun toDateRange(startDateStr: String, endDateStr: String): Pair<LocalDateTime, LocalDateTime> {
|
||||
val startDate = startDateStr.convertLocalDateTime()
|
||||
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
|
||||
|
||||
return startDate to endDate
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EXCEL_HEADERS = listOf(
|
||||
"시리즈 제목",
|
||||
"콘텐츠 제목",
|
||||
"가격(캔)",
|
||||
"구분",
|
||||
"판매 수",
|
||||
"합계(캔)",
|
||||
"합계(포인트)"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.originalSeries
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
|
||||
data class GetAdminOriginalSeriesOwnerResponse @QueryProjection constructor(
|
||||
@JsonProperty("memberId") val memberId: Long,
|
||||
@JsonProperty("nickname") val nickname: String
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.originalSeries
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||
import kotlin.math.ceil
|
||||
|
||||
data class GetAdminOriginalSeriesSettlementDetailQueryData @QueryProjection constructor(
|
||||
val seriesTitle: String,
|
||||
val contentTitle: String,
|
||||
val basePrice: Int,
|
||||
val orderType: OrderType,
|
||||
val salesCount: Long,
|
||||
val totalCan: Int?,
|
||||
val totalPoint: Int?
|
||||
) {
|
||||
fun toResponse(): GetAdminOriginalSeriesSettlementDetailResponse {
|
||||
return GetAdminOriginalSeriesSettlementDetailResponse(
|
||||
seriesTitle = seriesTitle,
|
||||
contentTitle = contentTitle,
|
||||
price = if (orderType == OrderType.RENTAL) ceil(basePrice * 0.7).toInt() else basePrice,
|
||||
orderType = if (orderType == OrderType.RENTAL) "대여" else "소장",
|
||||
salesCount = salesCount.toInt(),
|
||||
totalCan = totalCan ?: 0,
|
||||
totalPoint = totalPoint ?: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.originalSeries
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
data class GetAdminOriginalSeriesSettlementDetailResponse(
|
||||
@JsonProperty("seriesTitle") val seriesTitle: String,
|
||||
@JsonProperty("contentTitle") val contentTitle: String,
|
||||
@JsonProperty("price") val price: Int,
|
||||
@JsonProperty("orderType") val orderType: String,
|
||||
@JsonProperty("salesCount") val salesCount: Int,
|
||||
@JsonProperty("totalCan") val totalCan: Int,
|
||||
@JsonProperty("totalPoint") val totalPoint: Int
|
||||
)
|
||||
|
||||
data class GetAdminOriginalSeriesSettlementDetailListResponse(
|
||||
@JsonProperty("totalCount") val totalCount: Int,
|
||||
@JsonProperty("items") val items: List<GetAdminOriginalSeriesSettlementDetailResponse>
|
||||
)
|
||||
Reference in New Issue
Block a user