구글 인 앱 결제 검증코드 수정
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-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 | ||||||
|   | |||||||
| @@ -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>) { | ||||||
|   | |||||||
| @@ -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("결제정보에 오류가 있습니다.") | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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