1. Cài đặt thư viện nền tảng
Để tương tác với hệ thống thanh toán của WeChat phiên bản 3, dự án cần tích hợp SDK chính thức do đội phát triển cung cấp. Thư viện này tự động xử lý chữ ký RSA, kiểm tra chứng chỉ và mã hóa/giải mã tải trọng JSON theo chuẩn V3.
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.12</version>
</dependency>
2. Cấu hình Spring Boot cho SDK
Hệ thống yêu cầu khởi tạo một đối tượng cấu hình duy nhất để quản lý vòng đời chứng chỉ và khóa riêng tư. Dưới đây là lớp cấu hình sử dụng Spring `@Configuration` để đăng ký các bean dịch vụ và bộ phân tích thông báo.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.service.payments.nativepay.NativePayService;
@Configuration
public class WechatPaySetup {
@Value("${wechat.tenKhauThuDoi}")
private String tenKhau;
@Value("${wechat.duongDanKhoaTien}")
private String duongDanKhoa;
@Value("${wechat.chuoiKhanhChungThu}")
private String soKyCung;
@Value("${wechat.matKhaApiBan3}")
private String khoaBan3;
private Config cauHinhCores;
@PostConstruct
public void khoiTaoCauHinh() {
cauHinhCores = new RSAAutoCertificateConfig.Builder()
.merchantId(tenKhau)
.privateKeyFromPath(duongDanKhoa)
.merchantSerialNumber(soKyCung)
.apiV3Key(khoaBan3)
.build();
}
@Primary
@Bean
public NativePayService dichVuQuetMa() {
return new NativePayService.Builder()
.config(cauHinhCores)
.build();
}
@Primary
@Bean
public NotificationParser trichDoatBaoHiệu() {
return new NotificationParser((NotificationConfig) cauHinhCores);
}
}
3. Triển khai API tạo mã QR chờ thanh toán
Luồng giao dịch bắt đầu bằng việc gửi yêu cầu tiền thanh toán (Prepay) đến máy chủ WeChat. Nếu thành công, hệ thống sẽ trả về chuỗi URL mã QR (`code_url`) để phía client hiển thị hoặc mã hóa thành ảnh.
Controller:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/giaodich")
public class ControllerGiaoDich {
private final DichVuQuetMaImpl dichVu;
public ControllerGiaoDich(DichVuQuetMaImpl dichVu) {
this.dichVu = dichVu;
}
@PostMapping("/native-tao-ma")
public ApiResponse<String> ketNoiNativePay(Integer loaiFee) {
var thongTinGoiGiam = EnumGoiGiam.timTheoMa(loaiFee);
if (thongTinGoiGiam == null) {
return ApiResponse.loi("Tham số không hợp lệ");
}
String urlQR = dichVu.xuLyTaoDon(thongTinGoiGiam);
return urlQR != null ? ApiResponse.thanhCong(urlQR) : ApiResponse.loi("Tạo đơn thất bại");
}
}
Service:
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse;
import org.springframework.stereotype.Service;
@Service
public class DichVuQuetMaImpl {
private final NativePayService nativeApi;
private final MapperDonHang dbMapper;
public DichVuQuetMaImpl(NativePayService nativeApi, MapperDonHang dbMapper) {
this.nativeApi = nativeApi;
this.dbMapper = dbMapper;
}
public String xuLyTaoDon(EnumGoiGiam goiGiam) {
PrepayRequest yeuCau = new PrepayRequest();
yeuCau.setAppid("wxdemo123456");
yeuCau.setMchid("1000009876");
yeuCau.setOutTradeNo(ChuongTrinhTiengWechat.sinhMaDonHàng());
yeuCau.setDescription(goiGiam.getTen());
yeuCau.setNotifyUrl("https://dichvu.example.com/api/giaodich/thongbao");
Amount tongTien = new Amount();
tongTien.setTotal(goiGiam.getGiaTri());
yeuCau.setAmount(tongTien);
try {
PrepayResponse ketQua = nativeApi.prepay(yeuCau);
if (dbMapper.ghiNhanDonChờ(ketQua.getOutTradeNo(), goiGiam) < 1) {
return null;
}
return ketQua.getCodeUrl();
} catch (Exception e) {
LoggerFactory.getLogger(getClass()).error("Lỗi gọi API tiền thanh toán: {}", e.getMessage());
return null;
}
}
}
4. Xử lý cổng phản hồi thanh toán (Callback)
Trạng thái thanh toán thực tế phải dựa vào phản hồi bất đồng bộ từ WeChat. SDK cung cấp `NotificationParser` để tự động giải mã và xác minh chữ ký RSA-SHA256 dựa trên tiêu đề HTTP (`Wechatpay-*`).
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.ValidationException;
import com.wechat.pay.java.service.payments.model.Transaction;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ControllerThongBao {
private final DichVuQuetMaImpl dichVu;
public ControllerThongBao(DichVuQuetMaImpl dichVu) {
this.dichVu = dichVu;
}
@PostMapping("/api/giaodich/thongbao")
public KetQuaWechatPhanHoi xuLyThongBao(HttpServletRequest httpRequest) {
try {
return dichVu.kiemTraVaCapNhatTrangThai(httpRequest);
} catch (Exception e) {
LoggerFactory.getLogger(getClass()).error("Xảy ra lỗi khi phân tích thông báo:", e);
return new KetQuaWechatPhanHoi("FAIL", "LỖI HỆ THỐNG");
}
}
}
Logic kiểm tra & cập nhật:
@Transactional
public KetQuaWechatPhanHoi kiemTraVaCapNhatTrangThai(HttpServletRequest httpRequest) throws Exception {
RequestParam thamSoBaoHiệu = ChuongTrinhTiengWechat.xayDungThamSo(httpRequest);
Transaction giaoDich;
try {
giaoDich = trichDoatBaoHiệu.parse(thamSoBaoHiệu, Transaction.class);
} catch (ValidationException ex) {
return new KetQuaWechatPhanHoi("FAIL", "CHỮ KÝ KHÔNG HỢP LỆ");
}
if (Transaction.TradeStateEnum.SUCCESS != giaoDich.getTradeState()) {
return new KetQuaWechatPhanHoi("SUCCESS", "CHỜ THANH TOÁN");
}
var donHang = dbMapper.layDonTheoMa(giaoDich.getOutTradeNo());
if (donHang == null) return new KetQuaWechatPhanHoi("SUCCESS", "KHÔNG TÌM THẤY ĐƠN");
if (donHang.isDaThanhToan()) return new KetQuaWechatPhanHoi("SUCCESS", "ĐÃ XỬ LÝ TRƯỚC ĐÓ");
dbMapper.capNhatTrangThaiDaThu(giaoDich.getOutTradeNo(), giaoDich.getTransactionId());
// Thực hiện nghiệp vụ cộng điểm/hội viên ở đây
return new KetQuaWechatPhanHoi("SUCCESS", "SUCCESS");
}
5. Công cụ hỗ trợ trích xuất dữ liệu
Do luồng `InputStream` của HTTP request chỉ được đọc một lần, cần có lớp trợ giúp để đọc tiêu đề bảo mật và nội dung JSON, đồng thời định dạng lại thành đối tượng `RequestParam` mà SDK yêu cầu.
import com.wechat.pay.java.core.notification.RequestParam;
import org.springframework.util.StringUtils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ThreadLocalRandom;
public final class ChuongTrinhTiengWechat {
private static final String BANG_KY_TU = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final ThreadLocalRandom NGAU_NHIEN = ThreadLocalRandom.current();
public static String sinhMaDonHàng() {
return "PAY" + java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
.format(java.time.LocalDateTime.now()) + NGAU_NHIEN.nextInt(100000);
}
public static RequestParam xayDungThamSo(HttpServletRequest httpRequest) throws IOException {
String chuKy = httpRequest.getHeader("Wechatpay-Signature");
String chuoiNgauNhien = httpRequest.getHeader("Wechatpay-Nonce");
String thoiGian = httpRequest.getHeader("Wechatpay-Timestamp");
String kyCungThu = httpRequest.getHeader("Wechatpay-Serial");
String phuongThuc = httpRequest.getHeader("Wechatpay-Signature-Type");
return new RequestParam.Builder()
.serialNumber(kyCungThu)
.nonce(chuoiNgauNhien)
.signature(chuKy)
.timestamp(thoiGian)
.signType(phuongThuc)
.body(docNoiDungYeuCau(httpRequest))
.build();
}
private static String docNoiDungYeuCau(HttpServletRequest httpRequest) throws IOException {
StringBuilder chuoiTraVe = new StringBuilder();
try (var doc = new BufferedReader(new InputStreamReader(httpRequest.getInputStream(), StandardCharsets.UTF_8))) {
String dong;
while ((dong = doc.readLine()) != null) {
chuoiTraVe.append(dong);
}
}
return chuoiTraVe.toString();
}
}
6. Lưu ý quan trọng khi triển khai
- Mã giao dịch (`out_trade_no`) phải duy nhất trên toàn hệ thống trong vòng 2 năm. Không tái sử dụng mã cũ.
- Đường dẫn callback (`notify_url`) phải là domain đã đăng ký và được WeChat phê duyệt, hỗ trợ HTTPS.
- Khi nhận được phản hồi callback, luôn phản hồi lại `SUCCESS` ngay cả khi nghiệp vụ xử lý nội bộ chưa hoàn tất, tránh WeChat gửi lại thông báo trùng lặp (retries).
- Sử dụng `RSAAutoCertificateConfig` giúp SDK tự động tải và xoay chứng chỉ công khai của WeChat, giảm thiểu rủi ro lỗi xác thực chữ ký do chứng chỉ hết hạn.