Để triển khai thanh toán qua WeChat Mini Program theo chuẩn API v3 (JSAPI), cần hoàn tất các bước thiết lập ban đầu và tích hợp đồng bộ giữa frontend và backend. Dưới đây là hướng dẫn kỹ thuật đã được tái cấu trúc, loại bỏ yếu tố thương mại và tập trung vào luồng xử lý kỹ thuật thuần túy.
Yêu cầu khởi tạo hệ thống
- Hồ sơ pháp lý: Giấy phép kinh doanh (bản điện tử), mã số thuế, thông tin tài khoản ngân hàng doanh nghiệp
- Chứng thực danh tính người quản lý mini program (CMND/CCCD quét)
- Phí xác thực chính thức của WeChat: 300 CNY
- Tên miền HTTPS đã xác minh (bắt buộc khi phát hành; trong môi trường dev có thể tắt kiểm tra tên miền trong công cụ phát triển WeChat)
Giao diện người dùng (UniApp/Vue-based)
Thành phần nút thanh toán kích hoạt luồng JSAPI — chỉ hiển thị cho đơn hàng chưa thanh toán:
<template>
<view class="product-card" v-for="(item, idx) in productList" :key="idx">
<view class="product-header">
<text class="name">{{ item.name }}</text>
<text class="price">¥{{ (item.amount / 100).toFixed(2) }}</text>
<text class="desc">{{ item.summary }}</text>
<button
v-if="item.status === 'pending'"
size="mini"
@click="initiatePayment(item)"
class="pay-btn"
>Thanh toán ngay</button>
<button v-else size="mini" disabled class="paid-btn">Đã thanh toán</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
productList: [
{
id: 'prod-001',
name: 'Dịch vụ A',
amount: 5000, // 50.00 CNY → biểu diễn bằng xu
summary: 'Gói cơ bản',
status: 'pending'
},
{
id: 'prod-002',
name: 'Dịch vụ B',
amount: 12000,
summary: 'Gói nâng cao',
status: 'completed'
}
]
};
},
methods: {
async initiatePayment(item) {
try {
// Lấy mã đăng nhập từ WeChat SDK
const loginRes = await wx.login({ timeout: 10000 });
if (!loginRes.code) throw new Error('Không lấy được code đăng nhập');
// Gửi code tới backend để trao đổi lấy openid & session_key
const authRes = await wx.request({
url: 'https://api.example.com/v1/auth/wechat-login',
method: 'POST',
data: { code: loginRes.code },
header: { 'Content-Type': 'application/json' }
});
if (authRes.data.code !== 200 || !authRes.data.data?.openid) {
throw new Error('Xác thực thất bại');
}
// Yêu cầu backend tạo prepay_id và ký thông tin thanh toán
const payRes = await wx.request({
url: 'https://api.example.com/v1/payment/jsapi',
method: 'POST',
data: {
openid: authRes.data.data.openid,
amount: item.amount,
description: item.summary,
tradeNo: `TRX-${Date.now()}`
},
header: { 'Content-Type': 'application/json' }
});
if (payRes.data.code !== 200) throw new Error('Tạo lệnh thanh toán thất bại');
// Kích hoạt giao diện thanh toán WeChat
await wx.requestPayment({
timeStamp: payRes.data.timeStamp,
nonceStr: payRes.data.nonceStr,
package: payRes.data.package,
signType: payRes.data.signType,
paySign: payRes.data.paySign,
success: () => console.log('Thanh toán thành công'),
fail: (err) => console.warn('Thanh toán bị hủy hoặc lỗi:', err)
});
} catch (err) {
console.error('Lỗi thanh toán:', err);
}
}
}
};
</script>
Backend Java (Spring Boot + Apache HttpClient)
1. Endpoint xác thực mã đăng nhập (code → openid/session_key)
@PostMapping("/v1/auth/wechat-login")
public ResponseEntity<ApiResponse<Map<String, String>>> exchangeCodeForOpenId(@RequestBody Map<String, String> payload) {
String code = payload.get("code");
if (StringUtils.isBlank(code)) {
return ResponseEntity.badRequest().body(ApiResponse.error("Thiếu tham số code"));
}
String url = String.format(
"%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
WeChatApiUrls.SESSION_EXCHANGE,
WxConfig.APP_ID,
WxConfig.APP_SECRET,
code
);
try {
String response = HttpUtils.httpGet(url);
JSONObject json = JSON.parseObject(response);
if (json.containsKey("openid")) {
Map<String, String> data = new HashMap<>();
data.put("openid", json.getString("openid"));
data.put("session_key", json.getString("session_key"));
return ResponseEntity.ok(ApiResponse.success(data));
} else {
return ResponseEntity.status(400).body(ApiResponse.error(json.getString("errmsg")));
}
} catch (Exception e) {
return ResponseEntity.status(500).body(ApiResponse.error("Lỗi kết nối tới WeChat API"));
}
}
2. Endpoint tạo yêu cầu thanh toán JSAPI
@PostMapping("/v1/payment/jsapi")
public ResponseEntity<ApiResponse<Map<String, Object>>> generateJsApiOrder(@RequestBody PaymentRequest req) throws Exception {
// Tạo body yêu cầu theo chuẩn v3
ObjectNode requestBody = JsonNodeFactory.instance.objectNode();
requestBody.put("mchid", WxConfig.MCH_ID)
.put("appid", WxConfig.APP_ID)
.put("description", req.getDescription())
.put("notify_url", WxConfig.NOTIFY_URL)
.put("out_trade_no", req.getTradeNo());
requestBody.set("amount", JsonNodeFactory.instance.objectNode()
.put("total", req.getAmount())
.put("currency", "CNY"));
requestBody.set("payer", JsonNodeFactory.instance.objectNode()
.put("openid", req.getOpenid()));
// Gửi yêu cầu POST tới API WeChat với chữ ký v3
CloseableHttpClient client = WxV3SignatureClient.createClient();
HttpPost post = new HttpPost(WeChatApiUrls.JSAPI_ORDER);
post.setEntity(new StringEntity(requestBody.toString(), StandardCharsets.UTF_8));
post.setHeader("Accept", "application/json");
post.setHeader("Content-Type", "application/json");
// Tự động thêm header Authorization bởi client đã được cấu hình chữ ký
try (CloseableHttpResponse resp = client.execute(post)) {
int status = resp.getStatusLine().getStatusCode();
if (status == 200) {
JSONObject result = JSON.parseObject(EntityUtils.toString(resp.getEntity()));
String prepayId = result.getString("prepay_id");
// Sinh thông tin ký phía client (frontend)
long timestamp = System.currentTimeMillis() / 1000;
String nonce = SecureRandomUtil.generateAlphanumeric(24);
String pkg = "prepay_id=" + prepayId;
String sign = WxV3SignatureUtil.generateJsApiSignature(
timestamp, nonce, pkg, WxConfig.API_V3_KEY, WxConfig.PRIVATE_KEY_PEM
);
Map<String, Object> response = new HashMap<>();
response.put("timeStamp", String.valueOf(timestamp));
response.put("nonceStr", nonce);
response.put("package", pkg);
response.put("signType", "RSA-SHA256");
response.put("paySign", sign);
return ResponseEntity.ok(ApiResponse.success(response));
} else {
throw new RuntimeException("WeChat API trả về lỗi: " + status);
}
}
}
Cấu hình hệ thống
WeChatApiUrls.java
public class WeChatApiUrls {
public static final String SESSION_EXCHANGE = "https://api.weixin.qq.com/sns/jscode2session";
public static final String JSAPI_ORDER = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
public static final String NOTIFY_URL = "https://api.example.com/v1/webhook/wechat-payment";
}
WxConfig.java (cấu hình bảo mật)
@ConfigurationProperties(prefix = "wechat.v3")
@Component
@Data
public class WxConfig {
public static String APP_ID;
public static String APP_SECRET;
public static String MCH_ID;
public static String API_V3_KEY;
public static String PRIVATE_KEY_PEM;
public static String NOTIFY_URL;
// Các setter được Spring tự động inject
}
Lưu ý: Chữ ký API v3 yêu cầu sử dụng chứng thư SSL của merchant và khóa riêng tư định dạng PEM để ký các yêu cầu HTTP — không sử dụng MD5 hay SHA1 như phiên bản cũ.