Skip to content

Xử lý kết quả thanh toán (Payment Notification)

🔁 Giao diện Redirect (Client)

Sau khi luồng thanh toán hoàn tất, khách hàng được điều hướng đến redirectUrl mà bên đối tác đã cung cấp trong create request. Một vài thông số sẽ được thêm vào URL dưới dạng query parameters:

  • Method: GET
  • Cấu trúc: redirectUrl?partnerCode=xxx&orderId=xxx&...

📋 Query Parameters

ParameterLoạiMô tả
partnerCodeStringMã đối tác
orderIdStringMã đơn hàng
requestIdStringMã yêu cầu
amountStringSố tiền
orderInfoStringThông tin đơn hàng
orderTypeStringLoại đơn hàng
transIdStringID giao dịch (nếu có)
resultCodeStringMã kết quả (0 = thành công)
messageStringThông báo kết quả
payTypeStringPhương thức thanh toán
responseTimeStringThời gian phản hồi
m2signatureStringChữ ký xác thực

⚠️ Lưu ý quan trọng

CẢNH BÁO

  1. Luôn xác thực chữ ký: Đây là bước bắt buộc để đảm bảo yêu cầu đến từ Pay2S chứ không phải từ kẻ xấu.
  2. Kiểm tra Amount: Đảm bảo số tiền nhận được khớp với số tiền lưu trong database của bạn.
  3. Xử lý idempotent: Có thể nhận được redirect nhiều lần cho cùng một giao dịch - cần kiểm tra orderId trước khi xử lý.
  4. Timeout: Nên xử lý nhanh chóng, không nên để người dùng chờ lâu.
  5. Log tất cả: Lưu lại các tham số nhận được để debug sau này.

💻 Code mẫu

php
<?php
header('Content-type: text/html; charset=utf-8');


$accessKey = '';
$secretKey = '';

if (!empty($_GET)) {
    $partnerCode = $_GET["partnerCode"];
    $orderId = $_GET["orderId"];
    $requestId = $_GET["requestId"];
    $amount = $_GET["amount"];
    $orderInfo = $_GET["orderInfo"];
    $orderType = $_GET["orderType"];
    $transId = $_GET["transId"] ?? '';
    $resultCode = $_GET["resultCode"];
    $message = $_GET["message"];
    $payType = $_GET["payType"];
    $responseTime = $_GET["responseTime"];
    $extraData = $_GET["extraData"] ?? '';
    $m2signature = $_GET["m2signature"]; // Pay2S signature

    // Tạo chuỗi hash để xác thực chữ ký
    $rawHash = "accessKey=$accessKey&amount=$amount&message=$message&orderId=$orderId&orderInfo=$orderInfo&orderType=$orderType&partnerCode=$partnerCode&payType=$payType&requestId=$requestId&responseTime=$responseTime&resultCode=$resultCode";
    $partnerSignature = hash_hmac("sha256", $rawHash, $secretKey);

    // Kiểm tra chữ ký
    if ($m2signature == $partnerSignature) {
        if ($resultCode == '0') {
            $result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>';
        } else {
            $result = '<div class="alert alert-danger"><strong>Payment status: </strong>' . $message . '</div>';
        }
    } else {
        $result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
    }
}
js
const express = require('express');
const crypto = require('crypto');

const app = express();
const port = 3000;

app.get('/redirect', (req, res) => {
    const accessKey = ''; // Thay bằng accessKey thực tế
    const secretKey = ''; // Thay bằng secretKey thực tế

    // Lấy các giá trị từ query parameters
    const partnerCode = req.query.partnerCode;
    const orderId = req.query.orderId;
    const requestId = req.query.requestId;
    const amount = req.query.amount;
    const orderInfo = req.query.orderInfo;
    const orderType = req.query.orderType;
    const transId = req.query.transId || '';
    const resultCode = req.query.resultCode;
    const message = req.query.message;
    const payType = req.query.payType;
    const responseTime = req.query.responseTime;
    const extraData = req.query.extraData || '';
    const m2signature = req.query.m2signature; // Pay2S signature

    // Tạo chuỗi rawHash
    const rawHash = `accessKey=${accessKey}&amount=${amount}&message=${message}&orderId=${orderId}&orderInfo=${orderInfo}&orderType=${orderType}&partnerCode=${partnerCode}&payType=${payType}&requestId=${requestId}&responseTime=${responseTime}&resultCode=${resultCode}`;

    // Tạo chữ ký HMAC SHA256
    const partnerSignature = crypto.createHmac('sha256', secretKey).update(rawHash).digest('hex');

    console.log('Debug rawHash:', rawHash);
    console.log('Debug partnerSignature:', partnerSignature);

    let result;

    // Kiểm tra chữ ký
    if (m2signature === partnerSignature) {
        if (resultCode === '0') {
            result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>';
        } else {
            result = `<div class="alert alert-danger"><strong>Payment status: </strong>${message}</div>`;
        }
    } else {
        result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>';
    }

    // Trả về kết quả
    res.send(result);
});

// Khởi động server
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});
js
using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;

namespace Pay2SRedirect.Controllers
{
    public class PaymentController : Controller
    {
        // Hành động xử lý redirect
        [HttpGet("redirect")]
        public IActionResult RedirectFromPay2S(
            string partnerCode, string orderId, string requestId, 
            string amount, string orderInfo, string orderType, 
            string transId, string resultCode, string message, 
            string payType, string responseTime, string extraData, 
            string m2signature)
        {
            string accessKey = "";  // Thay thế với accessKey thực tế
            string secretKey = "";  // Thay thế với secretKey thực tế

            // Tạo chuỗi rawHash cho chữ ký
            var rawHash = $"accessKey={accessKey}&amount={amount}&message={message}&orderId={orderId}&orderInfo={orderInfo}&orderType={orderType}&partnerCode={partnerCode}&payType={payType}&requestId={requestId}&responseTime={responseTime}&resultCode={resultCode}";

            // Tạo chữ ký HMAC SHA256
            var partnerSignature = CreateHmacSha256Signature(rawHash, secretKey);

            // Kiểm tra chữ ký
            if (m2signature == partnerSignature)
            {
                if (resultCode == "0")
                {
                    // Trả về kết quả thành công
                    ViewBag.Result = "<div class='alert alert-success'><strong>Payment status: </strong>Success</div>";
                }
                else
                {
                    // Trả về kết quả lỗi
                    ViewBag.Result = $"<div class='alert alert-danger'><strong>Payment status: </strong>{message}</div>";
                }
            }
            else
            {
                // Thông báo chữ ký không hợp lệ
                ViewBag.Result = "<div class='alert alert-danger'>This transaction could be hacked, please check your signature and returned signature</div>";
            }

            // Trả về view chứa kết quả
            return View();
        }

        // Hàm tạo chữ ký HMAC SHA256
        private string CreateHmacSha256Signature(string data, string secretKey)
        {
            using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
            {
                byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
                return BitConverter.ToString(hash).Replace("-", "").ToLower();
            }
        }
    }
}
python
from flask import Flask, request, render_template_string
import hmac
import hashlib

app = Flask(__name__)

def verify_signature(params, signature, secret_key):
    """Xác thực chữ ký HMAC SHA256"""
    raw_hash = f"accessKey={params.get('accessKey')}&amount={params.get('amount')}&message={params.get('message')}&orderId={params.get('orderId')}&orderInfo={params.get('orderInfo')}&orderType={params.get('orderType')}&partnerCode={params.get('partnerCode')}&payType={params.get('payType')}&requestId={params.get('requestId')}&responseTime={params.get('responseTime')}&resultCode={params.get('resultCode')}"
    
    computed_signature = hmac.new(
        secret_key.encode('utf-8'),
        raw_hash.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return computed_signature == signature

@app.route('/redirect')
def redirect_from_pay2s():
    access_key = ""  # Khóa truy cập
    secret_key = ""  # Khóa bí mật
    
    # Lấy các giá trị từ query parameters
    params = request.args.to_dict()
    
    partner_code = params.get('partnerCode')
    order_id = params.get('orderId')
    request_id = params.get('requestId')
    amount = params.get('amount')
    order_info = params.get('orderInfo')
    order_type = params.get('orderType')
    trans_id = params.get('transId', '')
    result_code = params.get('resultCode')
    message = params.get('message')
    pay_type = params.get('payType')
    response_time = params.get('responseTime')
    extra_data = params.get('extraData', '')
    m2signature = params.get('m2signature')  # Pay2S signature
    
    # Xác thực chữ ký
    if verify_signature(params, m2signature, secret_key):
        if result_code == '0':
            result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>'
        else:
            result = f'<div class="alert alert-danger"><strong>Payment status: </strong>{message}</div>'
    else:
        result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>'
    
    return render_template_string('''
        <!DOCTYPE html>
        <html>
        <head>
            <title>Payment Result</title>
            <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
        </head>
        <body>
            <div class="container mt-5">
                {{ result | safe }}
            </div>
        </body>
        </html>
    ''', result=result)

if __name__ == '__main__':
    app.run(port=3000, debug=True)
java
import com.google.gson.Gson;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

@Controller
public class Pay2SRedirectController {
    
    private static String createSignature(String rawData, String secretKey) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(
            secretKey.getBytes(StandardCharsets.UTF_8),
            0,
            secretKey.getBytes(StandardCharsets.UTF_8).length,
            "HmacSHA256"
        );
        mac.init(secretKeySpec);
        byte[] hash = mac.doFinal(rawData.getBytes(StandardCharsets.UTF_8));
        
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }
    
    @GetMapping("/redirect")
    public String redirectFromPay2S(
            @RequestParam String partnerCode,
            @RequestParam String orderId,
            @RequestParam String requestId,
            @RequestParam String amount,
            @RequestParam String orderInfo,
            @RequestParam String orderType,
            @RequestParam(required = false) String transId,
            @RequestParam String resultCode,
            @RequestParam String message,
            @RequestParam String payType,
            @RequestParam String responseTime,
            @RequestParam(required = false) String extraData,
            @RequestParam String m2signature,
            Model model) {
        
        String accessKey = "";  // Khóa truy cập
        String secretKey = "";  // Khóa bí mật
        
        try {
            // Tạo chuỗi rawHash
            String rawHash = String.format(
                "accessKey=%s&amount=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s",
                accessKey, amount, message, orderId, orderInfo, orderType, partnerCode, payType, requestId, responseTime, resultCode
            );
            
            // Tạo chữ ký
            String partnerSignature = createSignature(rawHash, secretKey);
            
            String result;
            
            // Kiểm tra chữ ký
            if (m2signature.equals(partnerSignature)) {
                if ("0".equals(resultCode)) {
                    result = "<div class='alert alert-success'><strong>Payment status: </strong>Success</div>";
                } else {
                    result = "<div class='alert alert-danger'><strong>Payment status: </strong>" + message + "</div>";
                }
            } else {
                result = "<div class='alert alert-danger'>This transaction could be hacked, please check your signature and returned signature</div>";
            }
            
            model.addAttribute("result", result);
        } catch (Exception e) {
            model.addAttribute("result", "<div class='alert alert-danger'>Error: " + e.getMessage() + "</div>");
        }
        
        return "payment-result";
    }
}
go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"fmt"
	"html/template"
	"log"
	"net/http"
)

func createSignature(rawData, secretKey string) string {
	h := hmac.New(sha256.New, []byte(secretKey))
	h.Write([]byte(rawData))
	return fmt.Sprintf("%x", h.Sum(nil))
}

func redirectHandler(w http.ResponseWriter, r *http.Request) {
	accessKey := ""  // Khóa truy cập
	secretKey := ""  // Khóa bí mật
	
	// Parse query parameters
	err := r.ParseForm()
	if err != nil {
		http.Error(w, "Error parsing form", http.StatusBadRequest)
		return
	}
	
	partnerCode := r.FormValue("partnerCode")
	orderId := r.FormValue("orderId")
	requestId := r.FormValue("requestId")
	amount := r.FormValue("amount")
	orderInfo := r.FormValue("orderInfo")
	orderType := r.FormValue("orderType")
	resultCode := r.FormValue("resultCode")
	message := r.FormValue("message")
	payType := r.FormValue("payType")
	responseTime := r.FormValue("responseTime")
	m2signature := r.FormValue("m2signature")
	
	// Tạo chuỗi rawHash
	rawHash := fmt.Sprintf(
		"accessKey=%s&amount=%s&message=%s&orderId=%s&orderInfo=%s&orderType=%s&partnerCode=%s&payType=%s&requestId=%s&responseTime=%s&resultCode=%s",
		accessKey, amount, message, orderId, orderInfo, orderType, partnerCode, payType, requestId, responseTime, resultCode,
	)
	
	// Tạo chữ ký
	partnerSignature := createSignature(rawHash, secretKey)
	
	var result template.HTML
	
	// Kiểm tra chữ ký
	if m2signature == partnerSignature {
		if resultCode == "0" {
			result = template.HTML("<div class='alert alert-success'><strong>Payment status: </strong>Success</div>")
		} else {
			result = template.HTML(fmt.Sprintf("<div class='alert alert-danger'><strong>Payment status: </strong>%s</div>", message))
		}
	} else {
		result = template.HTML("<div class='alert alert-danger'>This transaction could be hacked, please check your signature and returned signature</div>")
	}
	
	// Trả về HTML
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	tmpl := template.Must(template.New("result").Parse(`
		<!DOCTYPE html>
		<html>
		<head>
			<title>Payment Result</title>
			<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
		</head>
		<body>
			<div class="container mt-5">
				{{ . }}
			</div>
		</body>
		</html>
	`))
	tmpl.Execute(w, result)
}

func main() {
	http.HandleFunc("/redirect", redirectHandler)
	log.Println("Server running on port 3000")
	log.Fatal(http.ListenAndServe(":3000", nil))
}
ruby
require 'sinatra'
require 'openssl'
require 'erb'

def verify_signature(params, signature, secret_key)
  raw_hash = "accessKey=#{params['accessKey']}&amount=#{params['amount']}&message=#{params['message']}&orderId=#{params['orderId']}&orderInfo=#{params['orderInfo']}&orderType=#{params['orderType']}&partnerCode=#{params['partnerCode']}&payType=#{params['payType']}&requestId=#{params['requestId']}&responseTime=#{params['responseTime']}&resultCode=#{params['resultCode']}"
  
  computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_hash)
  computed_signature == signature
end

get '/redirect' do
  access_key = ''  # Khóa truy cập
  secret_key = ''  # Khóa bí mật
  
  partner_code = params['partnerCode']
  order_id = params['orderId']
  request_id = params['requestId']
  amount = params['amount']
  order_info = params['orderInfo']
  order_type = params['orderType']
  trans_id = params['transId'] || ''
  result_code = params['resultCode']
  message = params['message']
  pay_type = params['payType']
  response_time = params['responseTime']
  extra_data = params['extraData'] || ''
  m2signature = params['m2signature']
  
  # Xác thực chữ ký
  if verify_signature(params, m2signature, secret_key)
    if result_code == '0'
      result = '<div class="alert alert-success"><strong>Payment status: </strong>Success</div>'
    else
      result = "<div class=\"alert alert-danger\"><strong>Payment status: </strong>#{message}</div>"
    end
  else
    result = '<div class="alert alert-danger">This transaction could be hacked, please check your signature and returned signature</div>'
  end
  
  erb :payment_result, locals: { result: result }
end

📋 Quy trình xử lý Redirect

1. Người dùng thanh toán thành công trên Pay2S

2. Pay2S chuyển hướng người dùng đến redirectUrl với query parameters

3. Backend nhận các tham số từ URL

4. Xác thực chữ ký m2signature

5. Kiểm tra resultCode:
   - 0: Giao dịch thành công → Cập nhật database
   - ≠ 0: Giao dịch thất bại → Hiển thị thông báo lỗi

6. Hiển thị kết quả cho người dùng

🔍 Kết hợp với IPN

  • Redirect: Thông báo ngay cho người dùng thấy kết quả trên browser
  • IPN: Thông báo cho backend để xử lý tự động và lưu database

Khuyến cáo: Sử dụng cả hai để đảm bảo không bị mất thông tin giao dịch.

❓ Câu hỏi thường gặp

Q: Tại sao cần xác thực chữ ký?
A: Để đảm bảo yêu cầu đích thực đến từ Pay2S, không phải từ kẻ xấu giả mạo.

Q: Điều gì xảy ra nếu người dùng không nhận được redirect?
A: Pay2S sẽ gửi IPN để backend xử lý, sau đó người dùng có thể kiểm tra trạng thái thủ công.

Q: Có thể nhận được redirect nhiều lần không?
A: Có, do mạng hoặc lỗi khác. Vì thế cần kiểm tra orderId trước khi xử lý.

Q: resultCode là gì?
A: Mã trạng thái giao dịch:

  • 0 = Thành công
  • 9000 = Đã được xác thực (authorization)
  • >0 = Thất bại (xem IPN doc để biết mã lỗi cụ thể)