From a3442b8f2f4de7af5a80642ce968eca9be07c288 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 3 May 2024 18:58:42 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B5=AC=EA=B8=80=20=EC=9D=B8=20=EC=95=B1=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=9C=20=EA=B2=80=EC=A6=9D=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + .../vividnext/sodalive/SodaLiveApplication.kt | 2 + .../sodalive/can/charge/ChargeController.kt | 37 +++++---- .../sodalive/can/charge/ChargeService.kt | 81 +++++-------------- .../sodalive/google/GooglePlayService.kt | 25 ++++++ 5 files changed, 69 insertions(+), 77 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/google/GooglePlayService.kt diff --git a/build.gradle.kts b/build.gradle.kts index 078e33d..2d3a3e5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.springframework.retry:spring-retry") implementation("org.jetbrains.kotlin:kotlin-reflect") // jwt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt index bb4472f..78fe613 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt @@ -2,10 +2,12 @@ package kr.co.vividnext.sodalive import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.retry.annotation.EnableRetry import org.springframework.scheduling.annotation.EnableAsync @SpringBootApplication @EnableAsync +@EnableRetry class SodaLiveApplication fun main(args: Array) { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt index 44e3674..53c0eca 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.can.charge +import kr.co.vividnext.sodalive.can.payment.PaymentGateway import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.Member @@ -59,25 +60,29 @@ class ChargeController(private val service: ChargeService) { throw SodaException("로그인 정보를 확인해주세요.") } - val chargeId = service.googleCharge( - member = member, - title = request.title, - chargeCan = request.chargeCan, - price = request.price, - currencyCode = request.currencyCode, - productId = request.productId, - purchaseToken = request.purchaseToken, - paymentGateway = request.paymentGateway - ) - - ApiResponse.ok( - service.googleVerify( - memberId = member.id!!, - chargeId = chargeId, + if (request.paymentGateway == PaymentGateway.GOOGLE_IAP) { + val chargeId = service.googleCharge( + member = member, + title = request.title, + chargeCan = request.chargeCan, + price = request.price, + currencyCode = request.currencyCode, productId = request.productId, purchaseToken = request.purchaseToken, paymentGateway = request.paymentGateway ) - ) + + ApiResponse.ok( + service.processGoogleIap( + memberId = member.id!!, + chargeId = chargeId, + productId = request.productId, + purchaseToken = request.purchaseToken, + paymentGateway = request.paymentGateway + ) + ) + } else { + throw SodaException("결제정보에 오류가 있습니다.") + } } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt index 31e9310..57bf8a9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.can.charge import com.fasterxml.jackson.databind.ObjectMapper -import com.google.api.services.androidpublisher.AndroidPublisher import kr.co.bootpay.Bootpay import kr.co.vividnext.sodalive.can.CanRepository import kr.co.vividnext.sodalive.can.charge.event.ChargeSpringEvent @@ -10,6 +9,7 @@ import kr.co.vividnext.sodalive.can.payment.Payment import kr.co.vividnext.sodalive.can.payment.PaymentGateway import kr.co.vividnext.sodalive.can.payment.PaymentStatus import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.google.GooglePlayService import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRepository import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -21,6 +21,8 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.context.ApplicationEventPublisher import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpHeaders +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable import org.springframework.security.core.userdetails.User import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -37,7 +39,7 @@ class ChargeService( private val okHttpClient: OkHttpClient, private val applicationEventPublisher: ApplicationEventPublisher, - private val androidPublisher: AndroidPublisher, + private val googlePlayService: GooglePlayService, @Value("\${bootpay.application-id}") private val bootpayApplicationId: String, @@ -215,7 +217,7 @@ class ChargeService( } @Transactional - fun googleVerify( + fun processGoogleIap( memberId: Long, chargeId: Long, productId: String, @@ -227,73 +229,30 @@ class ChargeService( val member = memberRepository.findByIdOrNull(id = memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") - if (paymentGateway == PaymentGateway.GOOGLE_IAP) { - val response = androidPublisher.purchases().products() - .get("kr.co.vividnext.sodalive", productId, purchaseToken) - .execute() ?: throw SodaException("결제정보에 오류가 있습니다.") - charge.payment!!.orderId = response.orderId + if (charge.payment!!.status == PaymentStatus.REQUEST) { + val orderId = verifyPurchase(purchaseToken, productId) + if (orderId.isNotBlank()) { + charge.payment!!.orderId = orderId + charge.payment!!.status = PaymentStatus.COMPLETE + member.charge(charge.chargeCan, 0, "aos") - if ( - response.purchaseState == 0 && - charge.payment!!.status == PaymentStatus.REQUEST - ) { - if (consumeWithRetry(productId, purchaseToken, charge, member)) { - applicationEventPublisher.publishEvent( - ChargeSpringEvent( - chargeId = charge.id!!, - memberId = member.id!! - ) + applicationEventPublisher.publishEvent( + ChargeSpringEvent( + chargeId = charge.id!!, + memberId = member.id!! ) - } else { - throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요") - } + ) } else { - throw SodaException("결제정보에 오류가 있습니다.") + throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요") } } else { throw SodaException("결제정보에 오류가 있습니다.") } } - private fun consumeWithRetry(productId: String, purchaseToken: String, charge: Charge, member: Member): Boolean { - var attempt = 0 - var delay = 2000L - val retries = 3 - - var lastError: Exception? = null - - while (attempt < retries) { - try { - androidPublisher.purchases().products().consume( - "kr.co.vividnext.sodalive", - productId, - purchaseToken - ) - - val response = androidPublisher.purchases().products() - .get("kr.co.vividnext.sodalive", productId, purchaseToken) - .execute() ?: throw SodaException("결제정보에 오류가 있습니다.") - - if (response.consumptionState == 1) { - charge.payment!!.status = PaymentStatus.COMPLETE - member.charge(charge.chargeCan, 0, "aos") - - return true - } else { - attempt += 1 - Thread.sleep(delay) - delay *= 2 - } - } catch (e: Exception) { - lastError = e - attempt += 1 - Thread.sleep(delay) - delay *= 2 - } - } - - lastError?.printStackTrace() - return false + @Retryable(value = [Exception::class], maxAttempts = 3, backoff = Backoff(delay = 2000)) + fun verifyPurchase(purchaseToken: String, productId: String): String { + return googlePlayService.verifyAndConsumePurchase(purchaseToken, productId) } private fun requestRealServerVerify(verifyRequest: AppleVerifyRequest): Boolean { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/google/GooglePlayService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/google/GooglePlayService.kt new file mode 100644 index 0000000..a924112 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/google/GooglePlayService.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.google + +import com.google.api.services.androidpublisher.AndroidPublisher +import com.google.api.services.androidpublisher.model.ProductPurchasesAcknowledgeRequest +import org.springframework.stereotype.Service + +@Service +class GooglePlayService(private val androidPublisher: AndroidPublisher) { + fun verifyAndConsumePurchase(purchaseToken: String, productId: String): String { + val packageName = "kr.co.vividnext.sodalive" + val purchase = androidPublisher.purchases().products().get(packageName, productId, purchaseToken).execute() + + if (purchase.purchaseState == 0 && purchase.acknowledgementState == 0) { + val request = ProductPurchasesAcknowledgeRequest() + androidPublisher.purchases().products() + .acknowledge(packageName, productId, purchaseToken, request) + .execute() + + androidPublisher.purchases().products().consume(packageName, productId, purchaseToken).execute() + return purchase.orderId + } + + return "" + } +}