변경 파일 목록
총 5개
변경 파일 코드
추가삭제
변경 전KakaopayConfig.php
| */ | |
| class KakaopayConfig | |
| { | |
| public $kakaopayApiDomain = ''; | |
| public $kakaopayReadyUrl = ''; | |
| public $kakaopayShopDomain = ''; | |
| // kakaopay 결제 취소 url | |
| $this->kakaopayCancelUrl = $this->kakaopayApiDomain . '/api/KakaoCancel'; | |
| // kakaopay 결과 노티 url | |
| $this->kakaopayNotiUrl = $this->kakaopayPaymentDomain . DS . 'pg_noti.php'; | |
| // kakaopay 결과 회신 url | |
| $this->kakaopayReturnUrl = $this->kakaopayPaymentDomain . DS . 'pg_return.php'; | |
변경 후KakaopayConfig.php
| */ | |
| class KakaopayConfig | |
| { | |
| // 노티 url 표식 — 이 표식이 있는 주문만 중계서버가 결제 취소 직전 확인(PgNoti mode=cancel)을 보낸다 | |
| const RELAY_CANCEL_CHECK_QUERY = 'relayCancelCheck=1'; | |
| public $kakaopayApiDomain = ''; | |
| public $kakaopayReadyUrl = ''; | |
| public $kakaopayShopDomain = ''; | |
| // kakaopay 결제 취소 url | |
| $this->kakaopayCancelUrl = $this->kakaopayApiDomain . '/api/KakaoCancel'; | |
| // kakaopay 결과 노티 url — 중계서버는 이 표식이 있을 때만 결제 취소 전 확인을 보낸다(구버전 쇼핑몰은 기존 동작 유지) | |
| $this->kakaopayNotiUrl = $this->kakaopayPaymentDomain . DS . 'pg_noti.php?' . self::RELAY_CANCEL_CHECK_QUERY; | |
| // kakaopay 결과 회신 url | |
| $this->kakaopayReturnUrl = $this->kakaopayPaymentDomain . DS . 'pg_return.php'; | |
변경 전NotiCancelReservation.php
변경 후NotiCancelReservation.php
| <?php | |
| /* | |
| * Copyright (C) 2026 NHN COMMERCE. - All Rights Reserved | |
| * | |
| * Unauthorized copying or redistribution of this file in source and binary forms via any medium | |
| * is strictly prohibited. | |
| */ | |
| namespace Bundle\Component\Payment\Kakaopay; | |
| use Bundle\Component\Order\OrderAdmin; | |
| use Bundle\Component\Payment\PaymentNotiLockGuard; | |
| use Framework\Database\DBManager; | |
| use Framework\Debug\Exception\DatabaseException; | |
| use Logger; | |
| use Origin\Enum\Order\OrderStatusCode; | |
| use Origin\Enum\Order\OrderStatusGroup; | |
| /** | |
| * 카카오페이 중계서버(commerce-relay-kakaopay)가 결제를 취소하기 직전에 쇼핑몰과 결과를 확정하는 취소 예약. | |
| * | |
| * 중계서버는 노티 응답이 정상(OK/PAYED)이 아니면 결제를 취소한다. 취소 후에 알리면 그사이 저장 중인 노티가 | |
| * 결제완료로 덮어쓰므로, 취소 전에 승인 확정 표시(PaymentNotiLockGuard::markAccepted)를 대기 없이 잡아 | |
| * 노티의 확정과 상호배제한다. 표시를 잡으면 결제시도 주문을 결제실패로 바꾸고(취소 예약), 못 잡으면 노티가 | |
| * 이미 확정해 저장 중이므로 결제를 유지시킨다. 노티는 확정 표시 후 isPaymentAttempt()로 예약 여부를 다시 확인한다. | |
| */ | |
| class NotiCancelReservation | |
| { | |
| const RESULT_PAYED = 'PAYED'; | |
| const RESULT_CANCEL = 'CANCEL'; | |
| // 암호화 노티 데이터를 다시 보내 결제시도 주문을 실패로 바꾸지 못하도록 승인 직후 요청만 받는다 | |
| const REQUEST_VALID_SECONDS = 600; | |
| private const RESERVE_REASON = '중계서버 결제 취소로 인해'; | |
| // 카카오페이 결제수단 코드 — 다른 결제수단의 결제시도 주문은 취소 예약하지 않는다 | |
| private const SETTLE_KIND_KAKAOPAY = 'pk'; | |
| private DBManager $db; | |
| private PaymentNotiLockGuard $lockGuard; | |
| /** | |
| * @param DBManager $db 노티 처리에 사용하는 DB 커넥션과 동일한 인스턴스 | |
| * @param PaymentNotiLockGuard $lockGuard | |
| */ | |
| public function __construct(DBManager $db, PaymentNotiLockGuard $lockGuard) | |
| { | |
| $this->db = $db; | |
| $this->lockGuard = $lockGuard; | |
| } | |
| /** | |
| * @param string $orderNo | |
| * @param OrderAdmin $orderAdmin 결제실패 상태 변경(복귀 결제실패 처리와 동일 경로) | |
| * @return string RESULT_PAYED = 결제 유지, RESULT_CANCEL = 취소 진행 | |
| * @throws DatabaseException | |
| */ | |
| public function reserve(string $orderNo, OrderAdmin $orderAdmin): string | |
| { | |
| if (!$this->lockGuard->markAccepted($orderNo)) { | |
| return self::RESULT_PAYED; | |
| } | |
| try { | |
| $orderData = $this->getOrderData($orderNo); | |
| $orderStatus = (string)($orderData['orderStatus'] ?? ''); | |
| // 노티가 확정 표시를 풀기 전에 결제완료 저장까지 끝낸 경우 | |
| if ($orderStatus !== '' && substr($orderStatus, 0, 1) !== OrderStatusGroup::FAIL->value) { | |
| return self::RESULT_PAYED; | |
| } | |
| if ($orderStatus !== OrderStatusCode::PAYMENT_ATTEMPT->value) { | |
| return self::RESULT_CANCEL; | |
| } | |
| // 카카오페이 주문이 아니면 이 결제와 맞지 않는 주문이므로 건드리지 않고 결제만 취소되게 한다 | |
| if (($orderData['settleKind'] ?? '') !== self::SETTLE_KIND_KAKAOPAY) { | |
| Logger::channel('pgKakaopayResult')->warning(__METHOD__ . ' 카카오페이 주문 아님, 취소 예약 생략', ['orderNo' => $orderNo, 'settleKind' => $orderData['settleKind'] ?? '']); | |
| return self::RESULT_CANCEL; | |
| } | |
| // 상태변경이 화면용 스크립트 등을 출력해도 중계서버 응답 JSON 과 섞이지 않게 버린다 | |
| ob_start(); | |
| try { | |
| $orderAdmin->updateStatusPreprocess( | |
| $orderNo, | |
| $orderAdmin->getOrderGoods($orderNo), | |
| OrderStatusGroup::FAIL->value, | |
| OrderStatusCode::PAYMENT_FAILED->value, | |
| self::RESERVE_REASON, | |
| true | |
| ); | |
| } finally { | |
| ob_end_clean(); | |
| } | |
| return self::RESULT_CANCEL; | |
| } finally { | |
| $this->lockGuard->releaseAccepted($orderNo); | |
| } | |
| } | |
| /** | |
| * 노티가 확정 표시를 남긴 뒤 저장 직전에 호출한다 — 취소 예약됐으면 false. | |
| * | |
| * @param string $orderNo | |
| * @return bool | |
| * @throws DatabaseException | |
| */ | |
| public function isPaymentAttempt(string $orderNo): bool | |
| { | |
| return ($this->getOrderData($orderNo)['orderStatus'] ?? '') === OrderStatusCode::PAYMENT_ATTEMPT->value; | |
| } | |
| /** | |
| * @param string $approvedAt 카카오 승인 시각(Y-m-d H:i:s) | |
| * @return bool | |
| */ | |
| public function isFreshApproval(string $approvedAt): bool | |
| { | |
| $approvedTime = strtotime($approvedAt); | |
| if ($approvedTime === false) { | |
| return false; | |
| } | |
| return abs(time() - $approvedTime) <= self::REQUEST_VALID_SECONDS; | |
| } | |
| /** | |
| * 분산서버(read replica) 상점에서 복제 지연으로 방금 바뀐 상태를 놓치지 않도록 primary에서 조회한다. | |
| * | |
| * @param string $orderNo | |
| * @return array orderStatus·settleKind (주문이 없으면 빈 배열) | |
| * @throws DatabaseException | |
| */ | |
| private function getOrderData(string $orderNo): array | |
| { | |
| if (method_exists($this->db, 'stickyPrimary')) { | |
| $this->db->stickyPrimary(); | |
| } | |
| $arrBind = []; | |
| $this->db->bind_param_push($arrBind, 's', $orderNo); | |
| $orderData = $this->db->query_fetch('SELECT orderStatus, settleKind FROM ' . DB_ORDER . ' WHERE orderNo = ?', $arrBind, false); | |
| return is_array($orderData) ? $orderData : []; | |
| } | |
| } | |
변경 전PgNoti.php
| namespace Bundle\Component\Payment\Kakaopay; | |
| use App; | |
| use Component\Payment\PaymentNotiLockGuard; | |
| use Globals; | |
| use Request; | |
| { | |
| // 중계서버가 1차 노티를 10초 만에 포기하고 재시도하므로, 뒤에 온 노티는 앞선 처리가 끝날 때까지 대기한다. | |
| private const NOTI_LOCK_WAIT_SECONDS = 20; | |
| private $pgConfig = ''; | |
| private $pgSetting = ''; | |
| // 주문번호 | |
| $this->orderNo = $requestValue['partner_order_id']; | |
| // 노티 중복 수신 방지 — 중계서버는 나중 응답만 보고 카카오 결제 취소 여부를 판단하므로, | |
| // 락 대기 후 실제 확정된 결과로 응답한다(즉시 낙관적 OK 금지). | |
| $lockGuard = new PaymentNotiLockGuard(App::getInstance('DB'), 'kakaopay'); | |
| if ($lockGuard->lock($this->orderNo, self::NOTI_LOCK_WAIT_SECONDS) === false) { | |
| // 앞선 노티가 검증을 통과해 저장 중이면 FAIL 응답 시 정상 결제가 취소되므로 PAYED로 응답한다 | |
| if ($lockGuard->isAccepted($this->orderNo)) { | |
| Logger::channel('pgKakaopayResult')->warning('Noti Lock Wait Timeout, Accepted :' . $this->pgMobileCode, [$this->orderNo]); | |
| exit; | |
| } | |
| // 대기가 끝나는 사이 앞선 처리가 끝났을 수 있어 한 번 더 시도한다 | |
| if ($lockGuard->lock($this->orderNo) === false) { | |
| Logger::channel('pgKakaopayResult')->error('Noti Lock Failed :' . $this->pgMobileCode, [$this->orderNo]); | |
| echo '{"result":"FAIL"}'; | |
| exit; | |
| } | |
| } | |
| Logger::channel('pgKakaopayResult')->info('PG Settle Data :' . $this->pgMobileCode, [$this->orderNo . ' : Settle Success']); | |
| // 저장이 늦어져 재시도 노티가 들어와도 결제가 취소되지 않도록 승인 확정을 표시한다 | |
| $this->accepted = $lockGuard->markAccepted($this->orderNo); | |
| if (!$this->accepted) { | |
| Logger::channel('pgKakaopayResult')->warning('Noti Accepted Mark Failed :' . $this->pgMobileCode, [$this->orderNo]); | |
| } | |
| // 결제 결과 저장 | |
| } | |
| } | |
| /** | |
| * 승인 확정 표시 후 저장되지 않고 끝나면(실패 응답·예외·치명 오류) 재시도 노티가 PAYED로 응답해 | |
| * 카카오 결제가 유지됐을 수 있어 대사용으로 남긴다. 종료 시점 한 곳에서 판정한다. | |
변경 후PgNoti.php
| namespace Bundle\Component\Payment\Kakaopay; | |
| use App; | |
| use Component\Payment\Kakaopay\NotiCancelReservation; | |
| use Component\Payment\PaymentNotiLockGuard; | |
| use Globals; | |
| use Request; | |
| { | |
| // 중계서버가 1차 노티를 10초 만에 포기하고 재시도하므로, 뒤에 온 노티는 앞선 처리가 끝날 때까지 대기한다. | |
| private const NOTI_LOCK_WAIT_SECONDS = 20; | |
| // 중계서버 취소 직전 확인이 결제실패 기록을 마칠 때까지 기다린다(그 전에 상태를 읽으면 결제시도로 보여 저장이 진행됨) | |
| private const ACCEPT_WAIT_SECONDS = 10; | |
| // 중계서버가 노티 응답을 확정하지 못했을 때 결제를 취소하기 직전에 보내는 확인 요청(암호화 data 안의 mode) | |
| private const RELAY_MODE_CANCEL = 'cancel'; | |
| private $pgConfig = ''; | |
| private $pgSetting = ''; | |
| // 주문번호 | |
| $this->orderNo = $requestValue['partner_order_id']; | |
| $lockGuard = new PaymentNotiLockGuard(App::getInstance('DB'), 'kakaopay'); | |
| $cancelReservation = new NotiCancelReservation(App::getInstance('DB'), $lockGuard); | |
| // 중계서버가 결제를 취소하기 직전 확인 — 노티의 승인 확정과 상호배제해 취소 예약 또는 결제 유지로 응답한다 | |
| if (is_scalar($requestValue['mode'] ?? null) && (string)$requestValue['mode'] === self::RELAY_MODE_CANCEL) { | |
| $approvedAt = $requestValue['approved_at'] ?? ''; | |
| $this->replyCancelReservation($cancelReservation, is_scalar($approvedAt) ? (string)$approvedAt : ''); | |
| } | |
| // 노티 중복 수신 방지 — 중계서버는 나중 응답만 보고 카카오 결제 취소 여부를 판단하므로, | |
| // 락 대기 후 실제 확정된 결과로 응답한다(즉시 낙관적 OK 금지). | |
| if (!$lockGuard->lock($this->orderNo, self::NOTI_LOCK_WAIT_SECONDS)) { | |
| // 앞선 노티가 검증을 통과해 저장 중이면 FAIL 응답 시 정상 결제가 취소되므로 PAYED로 응답한다 | |
| if ($lockGuard->isAccepted($this->orderNo)) { | |
| Logger::channel('pgKakaopayResult')->warning('Noti Lock Wait Timeout, Accepted :' . $this->pgMobileCode, [$this->orderNo]); | |
| exit; | |
| } | |
| // 대기가 끝나는 사이 앞선 처리가 끝났을 수 있어 한 번 더 시도한다 | |
| // 그래도 처리 중이면 결과를 모르므로 WAIT로 응답해 중계서버가 취소 전 확인을 거치게 한다(구버전 중계서버는 실패로 봄) | |
| if (!$lockGuard->lock($this->orderNo)) { | |
| Logger::channel('pgKakaopayResult')->error('Noti Lock Failed :' . $this->pgMobileCode, [$this->orderNo]); | |
| echo '{"result":"WAIT"}'; | |
| exit; | |
| } | |
| } | |
| Logger::channel('pgKakaopayResult')->info('PG Settle Data :' . $this->pgMobileCode, [$this->orderNo . ' : Settle Success']); | |
| // 저장이 늦어져 재시도 노티가 들어와도 결제가 취소되지 않도록 승인 확정을 표시한다 | |
| $this->accepted = $lockGuard->markAccepted($this->orderNo, self::ACCEPT_WAIT_SECONDS); | |
| // 확정 표시를 못 잡으면 취소 직전 확인과 상호배제가 깨지므로 저장하지 않고 결과 미확정으로 응답한다 | |
| if (!$this->accepted) { | |
| Logger::channel('pgKakaopayResult')->warning('Noti Accepted Mark Failed :' . $this->pgMobileCode, [$this->orderNo]); | |
| $this->pgClass->failDoRollbackQueryPG(); | |
| echo '{"result":"WAIT"}'; | |
| exit; | |
| } | |
| // 중계서버가 취소 직전 확인에서 결제실패로 예약했으면 카카오 결제가 취소되므로 결제완료로 저장하지 않는다 | |
| if (!$cancelReservation->isPaymentAttempt($this->orderNo)) { | |
| $this->accepted = false; | |
| Logger::channel('pgKakaopayResult')->warning('Noti Cancel Reserved, Skip Save :' . $this->pgMobileCode, [$this->orderNo]); | |
| throw new Exception('PG_STOP_ORDER_DATA'); | |
| } | |
| // 결제 결과 저장 | |
| } | |
| } | |
| /** | |
| * 중계서버 취소 직전 확인에 응답하고 종료한다. | |
| * | |
| * @param NotiCancelReservation $cancelReservation | |
| * @param string $approvedAt | |
| * @return void | |
| */ | |
| private function replyCancelReservation(NotiCancelReservation $cancelReservation, string $approvedAt): void | |
| { | |
| // 예약하지 못하면 결과를 모르는 것이므로 WAIT로 응답해 중계서버가 취소하지 않고 결제 확인 필요로 처리하게 한다 | |
| if (!$cancelReservation->isFreshApproval($approvedAt)) { | |
| Logger::channel('pgKakaopayResult')->warning('Relay Cancel Check Expired :' . $this->pgMobileCode, [$this->orderNo, $approvedAt]); | |
| echo '{"result":"WAIT"}'; | |
| exit; | |
| } | |
| try { | |
| $result = $cancelReservation->reserve($this->orderNo, App::load('\\Component\\Order\\OrderAdmin')); | |
| } catch (\Throwable $e) { | |
| Logger::channel('pgKakaopayResult')->error('Relay Cancel Reserve Failed :' . $this->pgMobileCode, ['orderNo' => $this->orderNo, 'exception' => $e, 'errorMessage' => $e->getMessage()]); | |
| echo '{"result":"WAIT"}'; | |
| exit; | |
| } | |
| Logger::channel('pgKakaopayResult')->info('Relay Cancel Check :' . $this->pgMobileCode, [$this->orderNo, $result]); | |
| echo '{"result":"' . $result . '"}'; | |
| exit; | |
| } | |
| /** | |
| * 승인 확정 표시 후 저장되지 않고 끝나면(실패 응답·예외·치명 오류) 재시도 노티가 PAYED로 응답해 | |
| * 카카오 결제가 유지됐을 수 있어 대사용으로 남긴다. 종료 시점 한 곳에서 판정한다. | |
변경 전PgReturn.php
| use Request; | |
| use Logger; | |
| use Origin\Enum\Order\OrderStatusCode; | |
| use Origin\Traits\Payment\PaymentProcessingSessionTrait; | |
| /** | |
| */ | |
| const KAKAOPAY_PG_DB_ERROR = '99'; | |
| // 결제 성공 복귀 시 노티의 결제완료 저장을 기다리는 최대 시간(초)과 주문상태 확인 간격(마이크로초) | |
| private int $notiWaitSeconds = 25; | |
| private int $notiPollMicroseconds = 500000; | |
| /** | |
| * 생성자 | |
| // 인증 처리 체크 | |
| if (empty($setPayData['retcode']) === true || $this->orderNo == '') { | |
| $script .= "location.href='../../order/order_end.php?orderNo='"; | |
| } elseif ($setPayData['retcode'] != '0') { | |
| // 카카오페이 결제 실패 | |
| $script .= "location.href='../../order/order_end.php?orderNo=" . $this->orderNo . "&mode="; | |
| return 'alert(' . json_encode($message) . ');' . $script . "location.href='../../mypage/order_list.php';"; | |
| } | |
| /** | |
| * 결제 성공으로 돌아왔는데 주문이 아직 결제시도(f1)면 노티가 결제완료를 저장할 때까지 주문상태를 짧은 간격으로 다시 확인한다. | |
| * 노티 락은 메일·SMS 등 후처리까지 끝나야 풀려서, 락 대신 주문상태로 판정해 저장 즉시 빠져나온다. | |
| * | |
| * @return bool true = 최대 대기 후에도 결제시도 상태(또는 확인 실패) | |
| */ | |
| private function isNotiProcessing(): bool | |
| { | |
| try { | |
| $db = \App::getInstance('DB'); | |
| $db->stickyPrimary(); | |
| } | |
| $deadline = microtime(true) + $this->notiWaitSeconds; | |
| while ($this->getOrderStatus($db) === OrderStatusCode::PAYMENT_ATTEMPT->value) { | |
| if (microtime(true) >= $deadline) { | |
| return true; | |
변경 후PgReturn.php
| use Request; | |
| use Logger; | |
| use Origin\Enum\Order\OrderStatusCode; | |
| use Origin\Enum\Order\OrderStatusGroup; | |
| use Origin\Traits\Payment\PaymentProcessingSessionTrait; | |
| /** | |
| */ | |
| const KAKAOPAY_PG_DB_ERROR = '99'; | |
| /** | |
| * 중계서버가 결제 결과를 확정하지 못해 카카오 결제를 유지함 (쇼핑몰 응답 지연·결제 취소 실패·결제창 재진입) | |
| */ | |
| const KAKAOPAY_PG_CONFIRM_REQUIRED = '98'; | |
| // 결제 성공 복귀 시 노티의 결제완료 저장을 기다리는 최대 시간(초)과 주문상태 확인 간격(마이크로초) | |
| private int $notiWaitSeconds = 25; | |
| private int $notiPollMicroseconds = 500000; | |
| // 결제 확인 필요(98) 복귀는 고객이 이미 오래 기다린 뒤라 노티 저장을 짧게만 기다린다 | |
| private int $confirmRequiredWaitSeconds = 10; | |
| /** | |
| * 생성자 | |
| // 인증 처리 체크 | |
| if (empty($setPayData['retcode']) === true || $this->orderNo == '') { | |
| $script .= "location.href='../../order/order_end.php?orderNo='"; | |
| } elseif ($setPayData['retcode'] === self::KAKAOPAY_PG_CONFIRM_REQUIRED) { | |
| // 카카오 결제는 유지됐으므로 결제실패로 바꾸지 않는다. 노티가 저장을 끝내는 중일 수 있어 잠시 기다린 뒤 | |
| // 결제완료면 주문완료로, 아니면 사유를 남기고 주문내역 안내로 보낸다(대사 대상) | |
| if (!$this->isNotiProcessing($this->confirmRequiredWaitSeconds) && $this->isPaid()) { | |
| $script .= "location.href='../../order/order_end.php?orderNo=" . $this->orderNo . "';"; | |
| } else { | |
| \Logger::channel('pgKakaopayResult')->error(__METHOD__ . ' Relay Confirm Required : ' . $this->orderNo, $setPayData); | |
| $getPg['pgFailReason'] = is_scalar($setPayData['retmsg'] ?? null) ? (string)$setPayData['retmsg'] : ''; | |
| $this->pgClass->setPgErrorData($this->orderNo, $getPg, null); | |
| $script = $this->getOrderListGuideScript($script); | |
| } | |
| } elseif ($setPayData['retcode'] != '0') { | |
| // 카카오페이 결제 실패 | |
| $script .= "location.href='../../order/order_end.php?orderNo=" . $this->orderNo . "&mode="; | |
| return 'alert(' . json_encode($message) . ');' . $script . "location.href='../../mypage/order_list.php';"; | |
| } | |
| /** | |
| * 노티가 결제완료를 이미 저장했는지 primary에서 확인한다. 확인할 수 없으면 false(주문내역 안내로 보냄). | |
| * | |
| * @return bool | |
| */ | |
| private function isPaid(): bool | |
| { | |
| try { | |
| $db = \App::getInstance('DB'); | |
| if (method_exists($db, 'stickyPrimary')) { | |
| $db->stickyPrimary(); | |
| } | |
| $orderStatus = $this->getOrderStatus($db); | |
| } catch (\Throwable $e) { | |
| \Logger::channel('pgKakaopayResult')->warning(__METHOD__ . ' 주문상태 확인 실패 : ' . $this->orderNo, ['exception' => $e, 'errorMessage' => $e->getMessage()]); | |
| return false; | |
| } | |
| return $orderStatus !== '' && substr($orderStatus, 0, 1) !== OrderStatusGroup::FAIL->value; | |
| } | |
| /** | |
| * 결제 성공으로 돌아왔는데 주문이 아직 결제시도(f1)면 노티가 결제완료를 저장할 때까지 주문상태를 짧은 간격으로 다시 확인한다. | |
| * 노티 락은 메일·SMS 등 후처리까지 끝나야 풀려서, 락 대신 주문상태로 판정해 저장 즉시 빠져나온다. | |
| * | |
| * @param int|null $waitSeconds 최대 대기 시간(초), null 이면 기본값(notiWaitSeconds) | |
| * @return bool true = 최대 대기 후에도 결제시도 상태(또는 확인 실패) | |
| */ | |
| private function isNotiProcessing(?int $waitSeconds = null): bool | |
| { | |
| try { | |
| $db = \App::getInstance('DB'); | |
| $db->stickyPrimary(); | |
| } | |
| $deadline = microtime(true) + ($waitSeconds ?? $this->notiWaitSeconds); | |
| while ($this->getOrderStatus($db) === OrderStatusCode::PAYMENT_ATTEMPT->value) { | |
| if (microtime(true) >= $deadline) { | |
| return true; | |
변경 전PaymentNotiLockGuard.php
| } | |
| /** | |
| * 주문·금액 검증을 통과해 결제를 확정 처리 중이라는 표시를 남긴다. lock() 획득 후에만 호출한다. | |
| * | |
| * @param string $orderNo | |
| * @return bool | |
| * @throws DatabaseException | |
| */ | |
| public function markAccepted(string $orderNo): bool | |
| { | |
| $arrBind = []; | |
| $this->db->bind_param_push($arrBind, 's', $this->acceptedLockName($orderNo)); | |
| $result = $this->db->query_fetch('SELECT GET_LOCK(?, 0) AS acquired', $arrBind, false); | |
| return !empty($result) && (int)$result['acquired'] === 1; | |
| } | |
| /** | |
| * 다른 요청이 승인 확정 표시를 들고 처리 중인지 확인한다. | |
| * | |
변경 후PaymentNotiLockGuard.php
| } | |
| /** | |
| * 주문·금액 검증을 통과해 결제를 확정 처리 중이라는 표시를 남긴다. | |
| * 노티는 lock() 획득 후 호출하고, 중계서버 취소 직전 확인은 노티의 확정과 상호배제하려고 대기 없이 호출한다. | |
| * | |
| * @param string $orderNo | |
| * @param int $waitSeconds GET_LOCK 대기 시간(초). 0이면 즉시 실패. | |
| * @return bool | |
| * @throws DatabaseException | |
| */ | |
| public function markAccepted(string $orderNo, int $waitSeconds = 0): bool | |
| { | |
| // 중계서버 취소 직전 확인은 lock()을 거치지 않으므로 여기서도 primary에 고정한다(복제 DB에 표시가 잡히면 상호배제가 깨짐) | |
| if (method_exists($this->db, 'stickyPrimary')) { | |
| $this->db->stickyPrimary(); | |
| } | |
| $arrBind = []; | |
| $this->db->bind_param_push($arrBind, 's', $this->acceptedLockName($orderNo)); | |
| $this->db->bind_param_push($arrBind, 'i', $waitSeconds); | |
| $result = $this->db->query_fetch('SELECT GET_LOCK(?, ?) AS acquired', $arrBind, false); | |
| return !empty($result) && (int)$result['acquired'] === 1; | |
| } | |
| /** | |
| * markAccepted()로 잡은 승인 확정 표시만 해제한다(주문 락을 잡지 않은 중계서버 취소 직전 확인용). | |
| * | |
| * @param string $orderNo | |
| * @return void | |
| * @throws DatabaseException | |
| */ | |
| public function releaseAccepted(string $orderNo): void | |
| { | |
| $arrBind = []; | |
| $this->db->bind_param_push($arrBind, 's', $this->acceptedLockName($orderNo)); | |
| $this->db->query_fetch('SELECT RELEASE_LOCK(?) AS released', $arrBind, false); | |
| } | |
| /** | |
| * 다른 요청이 승인 확정 표시를 들고 처리 중인지 확인한다. | |
| * | |