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로 비동기 알림을 전송합니다. 이 알림은 암호화되어 전달되므로, 반드시 마스터 키를 이용해 복호화해야 합니다.
복호화 절차
- 암호화된 문자열을 Base64로 디코딩
- 마스터 키를 MD5로 해시하여 32자리 소문자 키 생성
- 생성된 키로 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
조회 파라미터 우선순위
refund_id(최우선)out_refund_notransaction_idout_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_status | REFUNDCLOSE / SUCCESS / CHANGE |
| total_refund_count | 총 환불 건수 |
| settlement_refund_fee | 실제 정산된 환불 금액 (분 단위) |
결론
위챗페이 환불 처리는 세 가지 핵심 단계로 구성됩니다: 요청, 통지 수신, 상태 조회. 특히 복호화 과정과 보안 정책 설정은 필수이며, 중복 처리 방지와 오류 회피를 위한 로직 설계가 중요합니다. 개발 시 공식 문서와 예제 코드를 참조하여 신속하게 구현할 수 있습니다.