Bài 12 · Nâng cao · 26 phút
Nạp tiền QR qua SePay & idempotency
Biên soạn bởi Nguyễn Anh Tuấn
Cho khách nạp tiền vào ví bằng cách quét mã QR qua SePay: luồng tạo yêu cầu nạp - khách quét QR chuyển khoản - SePay gửi webhook xác nhận - cộng số dư. Vì sao cần idempotency (chống xử lý trùng) để một lần chuyển khoản không bị cộng tiền hai lần dù webhook gửi lại nhiều lần; xử lý trạng thái giao dịch an toàn. Tiền bạc là chỗ sai một ly đi một dặm. (Kèm affiliate SePay - công khai minh bạch.)
Khách đã đăng nhập được rồi, giờ tới phần họ mong nhất ở một web bán hàng có ví: nạp tiền để mua. Khoá cho khách nạp bằng cách quét mã QR chuyển khoản qua SePay - tiện và quen với người Việt.
Trung thực
Một lần nạp đi qua bốn nhịp. Mèo con không tự viết phần khó; hiểu luồng để mô tả đúng cho Claude và kiểm lại:
- ▸1. Tạo yêu cầu nạp: khách nhập số tiền, app tạo một giao dịch trạng thái "chờ".
- ▸2. Hiện QR: app hiển thị mã QR SePay để khách quét và chuyển khoản.
- ▸3. SePay phát hiện tiền về: khi tiền vào tài khoản, SePay gửi webhook báo cho app.
- ▸4. Cộng số dư: app nhận webhook, xác nhận hợp lệ rồi cộng tiền vào ví khách.
Làm sao app biết khách đã chuyển khoản? Không phải ngồi hỏi SePay liên tục. Thay vào đó SePay chủ động gọi tới một địa chỉ (URL) của app khi có tiền về - cú gọi đó gọi là webhook. App có một endpoint nghe webhook này, đọc thông tin (ai nạp, bao nhiêu, mã giao dịch) rồi xử lý.
Nhớ nhanh
Đây là chỗ dễ vấp nhất. Webhook có thể bị gửi nhiều lần cho cùng một lần chuyển khoản: mạng chập chờn, hoặc SePay không nhận được phản hồi nên tự thử lại. Nếu mỗi lần nhận webhook app đều cộng tiền, khách nạp một lần mà số dư nhảy hai, ba lần - sai tiền.
Lời giải là idempotency: xử lý cùng một giao dịch bao nhiêu lần thì kết quả vẫn như xử lý một lần. App kiểm mã giao dịch trước khi cộng:
Mấu chốt: mỗi giao dịch có một mã định danh duy nhất từ SePay. Lưu mã đó vào bảng giao dịch và đặt ràng buộc UNIQUE - để database tự chặn bản trùng. Trước khi cộng, app kiểm đã có mã này chưa:
Mã giả minh hoạ - cộng số dư an toàn (Claude viết bằng code Next.js)
# nhận webhook: tiền về, mã giao dịch = ma_gd
if da_co(ma_gd): # đã xử lý rồi
return "ok" # bỏ qua, KHÔNG cộng lại
luu(ma_gd) # cột ma_gd là UNIQUE -> chốt ở database
cong_so_du(nguoi_dung, so_tien)
danh_dau_hoan_tat(ma_gd) - ▸Mã giao dịch duy nhất + ràng buộc UNIQUE trên database = không lọt bản trùng.
- ▸Kiểm "đã xử lý chưa" trước khi cộng; rồi thì bỏ qua.
- ▸Trạng thái giao dịch rõ ràng: chờ → hoàn tất, để dễ lần lại khi cần.
Một rủi ro nữa: nếu app cộng tiền cho bất kỳ webhook nào gọi tới, kẻ xấu chỉ cần gọi webhook giả là tự bơm số dư. Phải xác thực webhook đúng là từ SePay - qua chữ ký hoặc secret do SePay cấp. Webhook không hợp lệ thì từ chối, không cộng đồng nào.
Trung thực
Idempotency lo "không cộng trùng". Nhưng còn một ca khó hơn: khách chuyển khoản TAY và gõ sai hoặc thiếu nội dung nạp. Tiền về tài khoản SePay, webhook báo, nhưng app không biết cộng cho ai vì thiếu mã tham chiếu khớp tài khoản.
Tuyệt đối không lờ đi hay nuốt khoản này - khách mất tiền mà không được cộng sẽ tố lừa đảo. Cách làm đúng:
- ▸Phòng từ gốc: mỗi yêu cầu nạp sinh một MÃ THAM CHIẾU duy nhất; QR điền sẵn nội dung + số tiền để khách khỏi gõ tay.
- ▸Webhook khớp được tài khoản → cộng đúng khách; KHÔNG khớp → đưa vào "tiền treo", KHÔNG bỏ qua.
- ▸Admin đối chiếu (số tiền, người gửi, thời gian) rồi khớp tay cho đúng khách, hoặc hoàn tiền nếu không xác minh được.
- ▸Mọi xử lý tiền treo GHI LOG (audit); đối soát ở báo cáo sẽ thấy các khoản treo.
Trung thực: tránh bị tố lừa đảo
SePay nhận chuyển khoản/QR và gửi webhook xác nhận - vừa đủ cho phần nạp tiền của khoá. Mèo con tạo tài khoản SePay, lấy thông tin kết nối (khoá API, địa chỉ webhook) đặt vào .env, rồi mô tả cho Claude gắn vào app.
Ưu đãi & minh bạch
Bước tiếp theo
Câu hỏi thường gặp
Webhook là cách SePay chủ động gọi tới một địa chỉ (URL) của app khi có biến động - ở đây là "tiền đã về tài khoản". App có một endpoint nghe webhook đó để biết mà cộng số dư, thay vì phải liên tục hỏi SePay "có tiền chưa".
Idempotent nghĩa là xử lý một việc nhiều lần cũng cho kết quả y như xử lý một lần. Cần vì webhook có thể bị gửi LẶP (mạng chập chờn, SePay tự thử lại). Nếu mỗi lần nhận webhook đều cộng tiền, khách bị cộng hai ba lần cho một lần chuyển khoản. Idempotency chặn chuyện đó.
Mỗi giao dịch có một mã định danh duy nhất (từ SePay). Trước khi cộng, app kiểm "đã xử lý mã này chưa". Chưa thì cộng số dư VÀ lưu mã lại; rồi thì bỏ qua. Đặt ràng buộc UNIQUE lên cột mã giao dịch để database tự chốt, không lọt được bản trùng.
Có thể, nếu app tin mọi webhook ẩn danh. Vì vậy phải XÁC THỰC webhook đúng là từ SePay (qua chữ ký/secret do SePay cấp). Webhook không hợp lệ thì từ chối, không cộng đồng nào. Đây là phần để subagent bảo mật rà kỹ.
Tiền vẫn về tài khoản SePay và webhook vẫn báo, nhưng app không khớp được tài khoản nào để cộng. Tuyệt đối không bỏ qua: đưa khoản đó vào "tiền treo" (suspense) trạng thái chờ, rồi admin đối chiếu (số tiền, người gửi, thời gian) khớp tay cho đúng khách, hoặc hoàn tiền nếu không xác minh được. Mọi bước ghi log để minh bạch, tránh bị tố lừa đảo.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Idempotency trong xử lý webhook nạp tiền nghĩa là gì?
- 1
Bảng giao dịch nạp tiền
Nhờ Claude thêm bảng giao_dich (id, nguoi_dung_id, so_tien, ma_gd, trang_thai) vào schema Drizzle, đặt ma_gd là UNIQUE, rồi generate & migrate.
✅ Hoàn thành khi: Bảng giao_dich tồn tại trong database, cột ma_gd có ràng buộc UNIQUE.
- 2
Tạo yêu cầu nạp + hiện QR
Nhờ Claude dựng trang nạp tiền: nhập số tiền, tạo yêu cầu nạp và hiển thị mã QR SePay để khách quét.
✅ Hoàn thành khi: Vào trang nạp, nhập số tiền và thấy một mã QR hiện ra (dùng chế độ test/sandbox của SePay).
- 3
Nhận webhook & cộng số dư
Nhờ Claude làm endpoint nhận webhook SePay; khi có xác nhận tiền về thì cộng số dư cho đúng khách.
✅ Hoàn thành khi: Khi có một xác nhận tiền về, số dư ví của khách tăng đúng bằng số tiền đã nạp.
- 4
Thử idempotency
Gửi lại cùng một webhook (cùng mã giao dịch) hai lần - tự gửi hoặc nhờ Claude mô phỏng.
✅ Hoàn thành khi: Lần gửi thứ hai KHÔNG làm số dư tăng thêm; số dư chỉ cộng đúng một lần cho mã giao dịch đó.
- 5
Từ chối webhook giả
Nhờ Claude xác thực chữ ký/secret của webhook; thử gửi một webhook không kèm secret hợp lệ.
✅ Hoàn thành khi: Webhook không hợp lệ bị từ chối và KHÔNG cộng tiền; chỉ webhook đúng từ SePay mới được xử lý.
- 6
Tiền treo cho webhook không khớp
Nhờ Claude: khi webhook tiền về mà KHÔNG khớp được tài khoản (sai nội dung), ghi vào bảng tiền treo trạng thái "chờ khớp" thay vì bỏ qua.
✅ Hoàn thành khi: Gửi một webhook nội dung sai → khoản tiền vào "tiền treo", không cộng cho ai, và hiện trong danh sách chờ xử lý.
- 7
Action khớp tay / hoàn tiền
Nhờ Claude làm action admin: khớp một khoản tiền treo cho đúng khách (hoặc đánh dấu hoàn tiền), ghi log.
✅ Hoàn thành khi: Khớp tay một khoản treo → số dư đúng khách tăng + có log; hoặc đánh dấu hoàn tiền được, khoản rời khỏi danh sách treo.
- 8
Cho subagent bảo mật rà
Gọi subagent rà bảo mật (OWASP) kiểm phần nạp tiền trước khi mở cho người dùng.
✅ Hoàn thành khi: Có danh sách phát hiện; mỗi mục được sửa hoặc mèo con xác nhận lý do an toàn.