Compare commits

..

No commits in common. "7aa58847972d05985af20d97c30a730b5e860fc0" and "5b237a154739a9200944b30d81b29934900a63b3" have entirely different histories.

5 changed files with 77 additions and 69 deletions

View File

@ -31,7 +31,6 @@ 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,12 +2,10 @@ 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,6 +1,5 @@
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
@ -60,7 +59,6 @@ class ChargeController(private val service: ChargeService) {
throw SodaException("로그인 정보를 확인해주세요.") throw SodaException("로그인 정보를 확인해주세요.")
} }
if (request.paymentGateway == PaymentGateway.GOOGLE_IAP) {
val chargeId = service.googleCharge( val chargeId = service.googleCharge(
member = member, member = member,
title = request.title, title = request.title,
@ -73,7 +71,7 @@ class ChargeController(private val service: ChargeService) {
) )
ApiResponse.ok( ApiResponse.ok(
service.processGoogleIap( service.googleVerify(
memberId = member.id!!, memberId = member.id!!,
chargeId = chargeId, chargeId = chargeId,
productId = request.productId, productId = request.productId,
@ -81,8 +79,5 @@ class ChargeController(private val service: ChargeService) {
paymentGateway = request.paymentGateway paymentGateway = request.paymentGateway
) )
) )
} else {
throw SodaException("결제정보에 오류가 있습니다.")
}
} }
} }

View File

@ -1,6 +1,7 @@
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
@ -9,7 +10,6 @@ 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,8 +21,6 @@ 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
@ -39,7 +37,7 @@ class ChargeService(
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
private val applicationEventPublisher: ApplicationEventPublisher, private val applicationEventPublisher: ApplicationEventPublisher,
private val googlePlayService: GooglePlayService, private val androidPublisher: AndroidPublisher,
@Value("\${bootpay.application-id}") @Value("\${bootpay.application-id}")
private val bootpayApplicationId: String, private val bootpayApplicationId: String,
@ -217,7 +215,7 @@ class ChargeService(
} }
@Transactional @Transactional
fun processGoogleIap( fun googleVerify(
memberId: Long, memberId: Long,
chargeId: Long, chargeId: Long,
productId: String, productId: String,
@ -229,13 +227,17 @@ class ChargeService(
val member = memberRepository.findByIdOrNull(id = memberId) val member = memberRepository.findByIdOrNull(id = memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException("로그인 정보를 확인해주세요.")
if (charge.payment!!.status == PaymentStatus.REQUEST) { if (paymentGateway == PaymentGateway.GOOGLE_IAP) {
val orderId = verifyPurchase(purchaseToken, productId) val response = androidPublisher.purchases().products()
if (orderId.isNotBlank()) { .get("kr.co.vividnext.sodalive", productId, purchaseToken)
charge.payment!!.orderId = orderId .execute() ?: throw SodaException("결제정보에 오류가 있습니다.")
charge.payment!!.status = PaymentStatus.COMPLETE charge.payment!!.orderId = response.orderId
member.charge(charge.chargeCan, 0, "aos")
if (
response.purchaseState == 0 &&
charge.payment!!.status == PaymentStatus.REQUEST
) {
if (consumeWithRetry(productId, purchaseToken, charge, member)) {
applicationEventPublisher.publishEvent( applicationEventPublisher.publishEvent(
ChargeSpringEvent( ChargeSpringEvent(
chargeId = charge.id!!, chargeId = charge.id!!,
@ -248,11 +250,50 @@ class ChargeService(
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException("결제정보에 오류가 있습니다.")
} }
} else {
throw SodaException("결제정보에 오류가 있습니다.")
}
} }
@Retryable(value = [Exception::class], maxAttempts = 3, backoff = Backoff(delay = 2000)) private fun consumeWithRetry(productId: String, purchaseToken: String, charge: Charge, member: Member): Boolean {
fun verifyPurchase(purchaseToken: String, productId: String): String { var attempt = 0
return googlePlayService.verifyAndConsumePurchase(purchaseToken, productId) 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
} }
private fun requestRealServerVerify(verifyRequest: AppleVerifyRequest): Boolean { private fun requestRealServerVerify(verifyRequest: AppleVerifyRequest): Boolean {

View File

@ -1,25 +0,0 @@
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 ""
}
}