쿠폰 시스템

- 쿠폰 생성 API 추가
- 쿠폰 리스트 API 추가
This commit is contained in:
Klaus 2024-01-01 04:46:57 +09:00
parent f304242eb4
commit 38cf9e453d
13 changed files with 265 additions and 0 deletions

View File

@ -44,6 +44,7 @@ dependencies {
kapt("org.springframework.boot:spring-boot-configuration-processor")
// aws
implementation("com.amazonaws:aws-java-sdk-sqs:1.12.380")
implementation("com.amazonaws:aws-java-sdk-ses:1.12.380")
implementation("com.amazonaws:aws-java-sdk-s3:1.12.380")
implementation("com.amazonaws:aws-java-sdk-cloudfront:1.12.380")

View File

@ -0,0 +1,25 @@
package kr.co.vividnext.sodalive.aws.sqs
import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component
enum class SqsEventType {
GENERATE_COUPON
}
class SqsEvent(
val type: SqsEventType,
val message: String
)
@Component
class SqsEventListener(private val sqsService: SqsService) {
@Async
@EventListener
fun sendMessage(sqsEvent: SqsEvent) {
when (sqsEvent.type) {
SqsEventType.GENERATE_COUPON -> sqsService.sendGenerateCouponMessage(sqsEvent.message)
}
}
}

View File

@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.aws.sqs
import com.amazonaws.services.sqs.AmazonSQS
import com.amazonaws.services.sqs.model.SendMessageRequest
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
@Service
class SqsService(
private val amazonSQS: AmazonSQS,
@Value("\${cloud.aws.sqs.generate-coupon-url}")
private val generateCouponQueueUrl: String
) {
fun sendGenerateCouponMessage(message: String) {
val request = SendMessageRequest()
.withQueueUrl(generateCouponQueueUrl)
.withMessageBody(message)
amazonSQS.sendMessage(request)
}
}

View File

@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.can.coupon
import kr.co.vividnext.sodalive.common.BaseEntity
import java.time.LocalDateTime
import javax.persistence.Entity
@Entity
data class CanCoupon(
val couponName: String,
val can: Int,
val couponCount: Int,
val validity: LocalDateTime,
val isActive: Boolean,
val isMultipleUse: Boolean
) : BaseEntity()

View File

@ -0,0 +1,39 @@
package kr.co.vividnext.sodalive.can.coupon
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/can/coupon")
class CanCouponController(private val service: CanCouponService) {
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
fun generateCoupon(
@RequestBody request: GenerateCanCouponRequest,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.generateCoupon(request))
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
fun getCouponList(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.getCouponList(offset = pageable.offset, limit = pageable.pageSize.toLong()))
}
}

View File

@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.can.coupon
import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.member.Member
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.JoinColumn
import javax.persistence.ManyToOne
import javax.persistence.OneToOne
@Entity
data class CanCouponNumber(
val couponNumber: String
) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "can_coupon_id", nullable = false)
var canCoupon: CanCoupon? = null
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = true)
var member: Member? = null
}

View File

@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.can.coupon
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.coupon.QCanCouponNumber.canCouponNumber
import org.springframework.data.jpa.repository.JpaRepository
interface CanCouponNumberRepository : JpaRepository<CanCouponNumber, Long>, CanCouponNumberQueryRepository
interface CanCouponNumberQueryRepository {
fun getUseCouponCount(id: Long): Int
}
class CanCouponNumberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponNumberQueryRepository {
override fun getUseCouponCount(id: Long): Int {
return queryFactory
.select(canCouponNumber.id)
.from(canCouponNumber)
.where(canCouponNumber.member.isNotNull)
.fetch()
.size
}
}

View File

@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.can.coupon
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.coupon.QCanCoupon.canCoupon
import org.springframework.data.jpa.repository.JpaRepository
interface CanCouponRepository : JpaRepository<CanCoupon, Long>, CanCouponQueryRepository
interface CanCouponQueryRepository {
fun getCouponList(offset: Long, limit: Long): List<CanCoupon>
}
class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponQueryRepository {
override fun getCouponList(offset: Long, limit: Long): List<CanCoupon> {
return queryFactory
.selectFrom(canCoupon)
.orderBy(canCoupon.id.desc())
.offset(offset)
.limit(limit)
.fetch()
}
}

View File

@ -0,0 +1,41 @@
package kr.co.vividnext.sodalive.can.coupon
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.sqs.SqsEvent
import kr.co.vividnext.sodalive.aws.sqs.SqsEventType
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import java.time.format.DateTimeFormatter
@Service
class CanCouponService(
private val repository: CanCouponRepository,
private val couponNumberRepository: CanCouponNumberRepository,
private val objectMapper: ObjectMapper,
private val applicationEventPublisher: ApplicationEventPublisher
) {
fun generateCoupon(request: GenerateCanCouponRequest) {
val message = objectMapper.writeValueAsString(request)
applicationEventPublisher.publishEvent(SqsEvent(type = SqsEventType.GENERATE_COUPON, message = message))
}
fun getCouponList(offset: Long, limit: Long): List<GetCouponListResponse> {
return repository.getCouponList(offset = offset, limit = limit)
.asSequence()
.map {
val useCouponCount = couponNumberRepository.getUseCouponCount(id = it.id!!)
GetCouponListResponse(
id = it.id!!,
couponName = it.couponName,
can = "${it.can}",
couponCount = it.couponCount,
useCouponCount = useCouponCount,
validity = it.validity.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
isMultipleUse = it.isMultipleUse,
isActive = it.isActive
)
}
.toList()
}
}

View File

@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.can.coupon
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.LocalDateTime
data class GenerateCanCouponRequest(
@JsonProperty("couponName") val couponName: String,
@JsonProperty("can") val can: Int,
@JsonProperty("validity") val validity: LocalDateTime,
@JsonProperty("isMultipleUse") val isMultipleUse: Boolean,
@JsonProperty("couponNumberCount") val couponNumberCount: Int
)

View File

@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.can.coupon
data class GetCouponListResponse(
val id: Long,
val couponName: String,
val can: String,
val couponCount: Int,
val useCouponCount: Int,
val validity: String,
val isMultipleUse: Boolean,
val isActive: Boolean
)

View File

@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.configs
import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.services.sqs.AmazonSQS
import com.amazonaws.services.sqs.AmazonSQSClientBuilder
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class AmazonSQSConfig(
@Value("\${cloud.aws.credentials.access-key}")
private val accessKey: String,
@Value("\${cloud.aws.credentials.secret-key}")
private val secretKey: String,
@Value("\${cloud.aws.region.static}")
private val region: String
) {
@Bean
fun amazonSQS(): AmazonSQS {
val basicAWSCredentials = BasicAWSCredentials(accessKey, secretKey)
val awsStaticCredentialsProvider = AWSStaticCredentialsProvider(basicAWSCredentials)
return AmazonSQSClientBuilder.standard()
.withCredentials(awsStaticCredentialsProvider)
.withRegion(region)
.build()
}
}

View File

@ -37,6 +37,8 @@ cloud:
keyPairId: ${CONTENT_CLOUD_FRONT_KEY_PAIR_ID}
cloudFront:
host: ${CLOUD_FRONT_HOST}
sqs:
generateCouponUrl: ${AMAZON_SQS_GENERATE_CAN_COUPON_QUEUE_URL}
region:
static: ap-northeast-2
stack: