웹사이트 결제 시스템에서 위챗페이 환불 처리 방법

1. 환불 요청 인터페이스 사용법

위챗페이는 거래 후 특정 기간 내에 구매자 또는 판매자의 사유로 인해 환불이 필요할 경우, 판매자가 환불 요청을 통해 지불 금액을 구매자에게 반환할 수 있도록 지원합니다. 이 과정은 원래 지불 경로를 따라 자동으로 이루어지며, 고객 측에서 환불 내역을 확인할 수 있도록 정보가 저장됩니다.

  • 제한사항: 1년 이상 지난 주문은 환불 요청이 불가능합니다.
  • 다중 환불 지원: 한 거래에 대해 여러 차례 부분 환불이 가능하지만, 각각의 환불 요청에는 고유한 환불 주문 번호를 사용해야 하며, 총 환불 금액은 원래 주문 금액을 초과할 수 없습니다.
  • 요청 빈도 제한: 정상 요청은 초당 150회까지 허용되며, 오류 발생 시 초당 최대 6회까지만 허용됩니다.
  • 부분 환불 횟수: 하나의 결제 주문에 대한 부분 환불은 최대 50회까지 가능합니다.

API 엔드포인트

https://api.mch.weixin.qq.com/secapi/pay/refund

보안 요구사항

이 인터페이스 호출은 양방향 인증서(서버 및 클라이언트 모두)가 필요합니다. 인증서는 위챗페이 마케팅 플랫폼의 '계정 센터 → API 보안'에서 다운로드할 수 있으며, 프로젝트 내 상수로 설정하는 것이 권장됩니다.

예시 코드 (Java)

public Map<String, String> initiateRefund(String outTradeNo, String outRefundNo, BigDecimal totalAmount, BigDecimal refundAmount, String reason) {
    Map<String, String> params = new HashMap<>();
    
    params.put("out_trade_no", outTradeNo);
    params.put("out_refund_no", outRefundNo);
    params.put("total_fee", String.valueOf(totalAmount.multiply(BigDecimal.valueOf(100)).intValue()));
    params.put("refund_fee", String.valueOf(refundAmount.multiply(BigDecimal.valueOf(100)).intValue()));
    params.put("refund_desc", reason);
    params.put("notify_url", "https://yourdomain.com/refund/callback");
    params.put("sign_type", "MD5");
    
    // 필수 파라미터: appid, mch_id, nonce_str, sign
    params.put("appid", "your_app_id");
    params.put("mch_id", "your_mch_id");
    params.put("nonce_str", generateNonceStr());
    params.put("sign", generateSignature(params));
    
    return wxPay.refund(params);
}

2. 환불 결과 통지 처리

환불 처리 완료 후, 위챗페이는 설정된 notify_url로 비동기 알림을 전송합니다. 이 알림은 암호화되어 전달되므로, 반드시 마스터 키를 이용해 복호화해야 합니다.

복호화 절차

  1. 암호화된 문자열을 Base64로 디코딩
  2. 마스터 키를 MD5로 해시하여 32자리 소문자 키 생성
  3. 생성된 키로 AES-256-ECB 방식으로 복호화 (PKCS7Padding 적용)

주의사항

  • Java 기본 설치 패키지에서는 256비트 AES 암호화가 제한되어 있을 수 있습니다. 이를 해결하려면 JCE 무제한 정책 파일을 다운로드하고 jre/lib/security 폴더에 교체해야 합니다.
  • 같은 알림이 여러 번 전달될 수 있으므로, 중복 처리 방지를 위해 상태 체크와 동시성 제어(예: 데이터 락)가 필수입니다.

예시 처리 코드

@PostMapping("/refund/callback")
public void handleRefundCallback(HttpServletRequest request, HttpServletResponse response) {
    try (InputStream in = request.getInputStream();
         ByteArrayOutputStream out = new ByteArrayOutputStream()) {
        
        byte[] buffer = new byte[1024];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        
        String rawXml = new String(out.toByteArray(), StandardCharsets.UTF_8);
        Map<String, String> map = WXPayUtil.xmlToMap(rawXml);

        if ("SUCCESS".equals(map.get("return_code"))) {
            String encryptedData = map.get("req_info");
            String decrypted = AESUtil.decrypt(encryptedData, merchantKey);
            Map<String, String> result = WXPayUtil.xmlToMap(decrypted);

            String refundStatus = result.get("refund_status");
            String outRefundNo = result.get("out_refund_no");

            // 상태 검사 및 처리
            if ("SUCCESS".equals(refundStatus)) {
                updateRefundStatus(outRefundNo, "COMPLETED");
                response.getWriter().write("<xml><return_code>SUCCESS</return_code><return_msg>OK</return_msg></xml>");
            } else {
                response.getWriter().write("<xml><return_code>FAIL</return_code><return_msg>Processing failed</return_msg></xml>");
            }
        } else {
            response.getWriter().write("<xml><return_code>FAIL</return_code><return_msg>Invalid request</return_msg></xml>");
        }

    } catch (Exception e) {
        log.error("Refund callback error", e);
    }
}

3. 환불 상태 조회

환불 요청 후, 실제 환불 상태를 확인하기 위해 해당 인터페이스를 호출할 수 있습니다. 특히, 한 주문에 대해 10건 이상의 환불이 발생한 경우, 페이지네이션을 활용하여 추가 정보를 조회해야 합니다.

API 엔드포인트

https://api.mch.weixin.qq.com/pay/refundquery

조회 파라미터 우선순위

  1. refund_id (최우선)
  2. out_refund_no
  3. transaction_id
  4. out_trade_no

페이지네이션 예시

// 25번째 환불부터 35번째까지 조회
Map<String, String> params = new HashMap<>();
params.put("out_trade_no", "your_order_no");
params.put("offset", "24"); // 0부터 시작

Map<String, String> result = wxPay.refundQuery(params);

응답 필드 설명

필드명설명
refund_statusREFUNDCLOSE / SUCCESS / CHANGE
total_refund_count총 환불 건수
settlement_refund_fee실제 정산된 환불 금액 (분 단위)

결론

위챗페이 환불 처리는 세 가지 핵심 단계로 구성됩니다: 요청, 통지 수신, 상태 조회. 특히 복호화 과정과 보안 정책 설정은 필수이며, 중복 처리 방지와 오류 회피를 위한 로직 설계가 중요합니다. 개발 시 공식 문서와 예제 코드를 참조하여 신속하게 구현할 수 있습니다.

태그: 위챗페이 환불 처리 API 보안 AES 복호화 웹 서버 통신

7월 16일 20:44에 게시됨