Merge pull request '구글 인 앱 결제 검증코드 수정' (#176) from test into main
Reviewed-on: #176
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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<String>) { | ||||
|   | ||||
| @@ -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("결제정보에 오류가 있습니다.") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 "" | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user