구글 인 앱 결제 검증코드 수정

This commit is contained in:
Klaus 2024-05-03 18:58:42 +09:00
parent 30793b75d5
commit a3442b8f2f
5 changed files with 69 additions and 77 deletions

View File

@ -31,6 +31,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.retry:spring-retry")
implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-reflect")
// jwt // jwt

View File

@ -2,10 +2,12 @@ package kr.co.vividnext.sodalive
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.retry.annotation.EnableRetry
import org.springframework.scheduling.annotation.EnableAsync import org.springframework.scheduling.annotation.EnableAsync
@SpringBootApplication @SpringBootApplication
@EnableAsync @EnableAsync
@EnableRetry
class SodaLiveApplication class SodaLiveApplication
fun main(args: Array<String>) { fun main(args: Array<String>) {

View File

@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.can.charge 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.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
@ -59,25 +60,29 @@ class ChargeController(private val service: ChargeService) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException("로그인 정보를 확인해주세요.")
} }
val chargeId = service.googleCharge( if (request.paymentGateway == PaymentGateway.GOOGLE_IAP) {
member = member, val chargeId = service.googleCharge(
title = request.title, member = member,
chargeCan = request.chargeCan, title = request.title,
price = request.price, chargeCan = request.chargeCan,
currencyCode = request.currencyCode, price = request.price,
productId = request.productId, currencyCode = request.currencyCode,
purchaseToken = request.purchaseToken,
paymentGateway = request.paymentGateway
)
ApiResponse.ok(
service.googleVerify(
memberId = member.id!!,
chargeId = chargeId,
productId = request.productId, productId = request.productId,
purchaseToken = request.purchaseToken, purchaseToken = request.purchaseToken,
paymentGateway = request.paymentGateway paymentGateway = request.paymentGateway
) )
)
ApiResponse.ok(
service.processGoogleIap(
memberId = member.id!!,
chargeId = chargeId,
productId = request.productId,
purchaseToken = request.purchaseToken,
paymentGateway = request.paymentGateway
)
)
} else {
throw SodaException("결제정보에 오류가 있습니다.")
}
} }
} }

View File

@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.can.charge package kr.co.vividnext.sodalive.can.charge
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.google.api.services.androidpublisher.AndroidPublisher
import kr.co.bootpay.Bootpay import kr.co.bootpay.Bootpay
import kr.co.vividnext.sodalive.can.CanRepository import kr.co.vividnext.sodalive.can.CanRepository
import kr.co.vividnext.sodalive.can.charge.event.ChargeSpringEvent 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.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.common.SodaException 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.Member
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -21,6 +21,8 @@ import org.springframework.beans.factory.annotation.Value
import org.springframework.context.ApplicationEventPublisher import org.springframework.context.ApplicationEventPublisher
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpHeaders 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.security.core.userdetails.User
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -37,7 +39,7 @@ class ChargeService(
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val androidPublisher: AndroidPublisher, private val googlePlayService: GooglePlayService,
@Value("\${bootpay.application-id}") @Value("\${bootpay.application-id}")
private val bootpayApplicationId: String, private val bootpayApplicationId: String,
@ -215,7 +217,7 @@ class ChargeService(
} }
@Transactional @Transactional
fun googleVerify( fun processGoogleIap(
memberId: Long, memberId: Long,
chargeId: Long, chargeId: Long,
productId: String, productId: String,
@ -227,73 +229,30 @@ class ChargeService(
val member = memberRepository.findByIdOrNull(id = memberId) val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException("로그인 정보를 확인해주세요.")
if (paymentGateway == PaymentGateway.GOOGLE_IAP) { if (charge.payment!!.status == PaymentStatus.REQUEST) {
val response = androidPublisher.purchases().products() val orderId = verifyPurchase(purchaseToken, productId)
.get("kr.co.vividnext.sodalive", productId, purchaseToken) if (orderId.isNotBlank()) {
.execute() ?: throw SodaException("결제정보에 오류가 있습니다.") charge.payment!!.orderId = orderId
charge.payment!!.orderId = response.orderId charge.payment!!.status = PaymentStatus.COMPLETE
member.charge(charge.chargeCan, 0, "aos")
if ( applicationEventPublisher.publishEvent(
response.purchaseState == 0 && ChargeSpringEvent(
charge.payment!!.status == PaymentStatus.REQUEST chargeId = charge.id!!,
) { memberId = member.id!!
if (consumeWithRetry(productId, purchaseToken, charge, member)) {
applicationEventPublisher.publishEvent(
ChargeSpringEvent(
chargeId = charge.id!!,
memberId = member.id!!
)
) )
} else { )
throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요")
}
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요")
} }
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException("결제정보에 오류가 있습니다.")
} }
} }
private fun consumeWithRetry(productId: String, purchaseToken: String, charge: Charge, member: Member): Boolean { @Retryable(value = [Exception::class], maxAttempts = 3, backoff = Backoff(delay = 2000))
var attempt = 0 fun verifyPurchase(purchaseToken: String, productId: String): String {
var delay = 2000L return googlePlayService.verifyAndConsumePurchase(purchaseToken, productId)
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
} }
private fun requestRealServerVerify(verifyRequest: AppleVerifyRequest): Boolean { private fun requestRealServerVerify(verifyRequest: AppleVerifyRequest): Boolean {

View File

@ -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 ""
}
}