NTM Solutions

Thứ Tư, 12 tháng 8, 2026

🥤 Bài 8 — Thanh toán QR cho máy bán nước tự động

Ở các bài trước, máy của chúng ta đã có:

  • ESP32 điều khiển.

  • Motor và cơ cấu trục xoắn.

  • Encoder/Hall Sensor.

  • Cảm biến phát hiện sản phẩm.

  • State Machine.

  • Giao diện cảm ứng.

Bây giờ chúng ta thêm một thành phần rất quan trọng:

Thanh toán QR.

Mục tiêu cuối cùng là:

Khách chọn Coca
        ↓
Máy tạo giao dịch
        ↓
Hiển thị QR
        ↓
Khách quét QR
        ↓
Thanh toán thành công
        ↓
Server xác nhận
        ↓
ESP32 nhận lệnh
        ↓
Nhả Coca

1. Không nên để ESP32 xử lý thanh toán trực tiếp

Đây là nguyên tắc quan trọng.

Không nên thiết kế:

QR
 ↓
ESP32
 ↓
Ngân hàng

ESP32 không nên trực tiếp xử lý:

  • thông tin tài khoản ngân hàng

  • thông tin thẻ

  • chữ ký giao dịch

  • khóa bí mật API

  • xác thực thanh toán

Thay vào đó, chúng ta cần một Backend Server.

Kiến trúc:

              KHÁCH
                │
                ▼
          Quét mã QR
                │
                ▼
        Cổng thanh toán
                │
                ▼
             SERVER
                │
                ▼
             ESP32
                │
                ▼
             MOTOR
                │
                ▼
              🥤

2. Tại sao cần Server?

Giả sử Coca có giá:

15.000đ

ESP32 yêu cầu server tạo một giao dịch:

POST /api/orders

Dữ liệu:

{
    "machine": "VM001",
    "product": "A1",
    "amount": 15000
}

Server tạo:

Order #10025

Sau đó trả về thông tin thanh toán.


3. Máy không nên tin vào QR

Một sai lầm rất nguy hiểm là:

Hiển thị QR

↓

Khách quét

↓

ESP32 tự cho rằng đã thanh toán

Không được.

Khách có thể:

  • chụp ảnh QR.

  • gửi ảnh cho người khác.

  • không thanh toán.

  • thanh toán sai số tiền.

Máy chỉ được nhả hàng khi:

Server xác nhận giao dịch hợp lệ.


4. Tạo đơn hàng

Khi khách chọn:

A1
Coca
15.000đ

ESP32 gửi:

machine = VM001
product = A1
amount = 15000

Server tạo một Order:

orders

id: 10025
machine: VM001
product: A1
amount: 15000
status: pending

Ban đầu:

pending

5. QR thanh toán

Server có thể tạo dữ liệu QR dựa trên giao dịch.

Ví dụ:

VietQR

Nội dung có thể chứa:

Ngân hàng
Số tài khoản
Số tiền
Nội dung chuyển khoản

Ví dụ nội dung:

VM001-10025

Khách chuyển:

15.000đ

với nội dung:

VM001-10025

Server sẽ dùng thông tin giao dịch để xác định tiền thuộc về đơn hàng nào.


6. Vì sao phải có mã giao dịch riêng?

Không nên dùng:

COCA

làm nội dung thanh toán.

Vì trong một ngày có thể có:

COCA
COCA
COCA
COCA

Thay vào đó:

VM001-10025
VM001-10026
VM001-10027

Mỗi giao dịch có một ID riêng.


7. State Machine mới

Ở bài 6 chúng ta có:

IDLE
 ↓
SELECT
 ↓
PAYMENT
 ↓
DISPENSE
 ↓
CHECK
 ↓
SUCCESS

Bây giờ PAYMENT chi tiết hơn:

SELECT
   ↓
CREATE_ORDER
   ↓
SHOW_QR
   ↓
WAIT_PAYMENT
   ↓
PAYMENT_SUCCESS
   ↓
DISPENSE

Nếu hết thời gian:

WAIT_PAYMENT
      ↓
   TIMEOUT
      ↓
     IDLE

8. Thời gian chờ thanh toán

Không thể để máy chờ vô hạn.

Ví dụ:

Thời gian thanh toán:

120 giây

Nếu khách không thanh toán:

PAYMENT TIMEOUT

Máy trở về:

IDLE

Đơn hàng:

pending

có thể được đánh dấu:

expired

9. Server nhận thông báo thanh toán

Có hai cách phổ biến.

Cách 1 — Webhook

Cổng thanh toán gửi thông báo cho server:

Payment Provider
       ↓
POST /api/payment/webhook
       ↓
Laravel

Server kiểm tra:

Giao dịch có tồn tại?

Số tiền có đúng?

Mã giao dịch có đúng?

Trạng thái có thành công?

Chữ ký có hợp lệ?

Nếu tất cả đúng:

status = paid

10. Cách 2 — ESP32 hỏi Server

Nếu hệ thống thanh toán hoặc kiến trúc của bạn không thuận tiện cho webhook trực tiếp đến thiết bị, ESP32 có thể hỏi server:

GET /api/orders/10025

Server trả:

{
    "id": 10025,
    "status": "paid"
}

ESP32:

pending

↓

pending

↓

pending

↓

paid

Khi nhận:

paid

thì:

DISPENSE

11. Webhook và Polling

Phương phápƯu điểmNhược điểm
WebhookNhanh, hiệu quảBackend phải nhận callback
PollingDễ hiểu, dễ làm prototypeTốn request
Kết hợpLinh hoạtKiến trúc phức tạp hơn

Đối với phiên bản học tập, chúng ta có thể bắt đầu bằng:

ESP32 polling Server

Sau khi hệ thống chạy ổn định mới chuyển sang kiến trúc nâng cao.


12. API Laravel

Vì chúng ta đang học Laravel, Backend có thể xây dựng bằng Laravel.

Ví dụ:

POST /api/orders
GET  /api/orders/{order}
POST /api/payment/webhook

13. Tạo Order

Controller:

public function store(Request $request)
{
    $order = Order::create([
        'machine_code' => $request->machine,
        'product_code' => $request->product,
        'amount'       => $request->amount,
        'status'       => 'pending',
    ]);

    return response()->json([
        'order_id' => $order->id,
        'status'   => $order->status,
    ]);
}

Sau này có thể bổ sung:

  • QR data.

  • Transaction ID.

  • Expired time.

  • Payment provider.

  • Signature.


14. Database

Một bảng orders đơn giản:

orders

id
machine_code
product_code
amount
status
transaction_code
paid_at
created_at
updated_at

Ví dụ:

10025
VM001
A1
15000
paid
VM001-10025
2026-08-09 12:05:32

15. Không tin dữ liệu từ ESP32

ESP32 gửi:

{
    "product": "A1",
    "amount": 15000
}

Nhưng server không nên tin tuyệt đối amount.

Ví dụ người dùng sửa request thành:

{
    "product": "A1",
    "amount": 100
}

Nếu server tin dữ liệu này thì máy có thể bị khai thác.

Server phải lấy giá từ database:

A1 → Coca → 15.000đ

Sau đó tự xác định:

amount = 15000

16. Luồng an toàn

Nên thiết kế:

ESP32
  │
  │ product=A1
  ▼
Laravel
  │
  │ tìm giá A1
  ▼
15.000đ
  │
  ▼
Create Order
  │
  ▼
Generate QR
  │
  ▼
Customer Payment
  │
  ▼
Payment Provider
  │
  ▼
Webhook
  │
  ▼
Laravel
  │
  ▼
Order = PAID
  │
  ▼
ESP32
  │
  ▼
DISPENSE

17. ESP32 kiểm tra thanh toán

Ví dụ:

bool checkPayment(int orderId)
{
    // Gửi HTTP request đến server

    // Server trả về:
    // pending
    // paid
    // expired

    return false;
}

Trong State Machine:

case WAIT_PAYMENT:

    if (checkPayment(orderId)) {
        currentState = DISPENSE;
    }

    break;

18. Không dùng delay()

Không nên:

delay(5000);

rồi kiểm tra thanh toán.

Thay vào đó:

if (millis() - lastCheck >= 2000) {

    lastCheck = millis();

    checkPayment(orderId);
}

ESP32 có thể đồng thời:

  • cập nhật màn hình.

  • kiểm tra cảm ứng.

  • kiểm tra thanh toán.

  • xử lý cảm biến.

  • giám sát motor.


19. Trường hợp khách thanh toán nhưng máy lỗi

Đây là tình huống rất quan trọng.

Ví dụ:

Khách thanh toán 15.000đ
        ↓
Payment = PAID
        ↓
Motor chạy
        ↓
Lon bị kẹt

Không được chỉ hiện:

ERROR

và kết thúc.

Server phải ghi nhận:

payment = paid
delivery = failed

Sau đó hệ thống có thể:

  • thử nhả lại.

  • đánh dấu cần hoàn tiền.

  • thông báo nhân viên.

  • ghi log sự cố.


20. Trạng thái Order

Nên tách thanh toángiao hàng.

Ví dụ:

payment_status
----------------
pending
paid
failed
expired
refunded

và:

delivery_status
----------------
pending
dispensing
success
failed

Như vậy:

PAID + DELIVERY_FAILED

không bị nhầm thành:

PAYMENT_FAILED

21. Một giao dịch hoàn chỉnh

Ví dụ khách mua A1:

12:10:01
SELECT A1

12:10:02
CREATE ORDER #10025

12:10:03
SHOW QR

12:10:15
PAYMENT PAID

12:10:16
DISPENSING

12:10:18
PRODUCT DETECTED

12:10:18
DELIVERY SUCCESS

Server có thể lưu toàn bộ lịch sử.


22. Dashboard quản trị

Vì Backend dùng Laravel, sau này chúng ta có thể tạo:

ADMIN DASHBOARD

Hiển thị:

Doanh thu hôm nay
------------------
1.250.000đ

Đơn hàng
------------------
87

Đã giao
------------------
84

Lỗi
------------------
3

Danh sách:

Order    Machine    Product    Amount    Status
10025    VM001      A1         15.000    SUCCESS
10026    VM001      B2         20.000    SUCCESS
10027    VM001      A1         15.000    ERROR

Đây chính là bước kết nối dự án máy bán nước với những kiến thức Laravel Admin CRUD mà chúng ta đã học trước đó.


23. Một máy hay nhiều máy

Ngay từ đầu nên có:

machine_code

Ví dụ:

VM001
VM002
VM003

Server sẽ biết:

Order #10025

Machine:
VM002

Sau này một hệ thống Laravel có thể quản lý:

             SERVER

       ┌───────┼───────┐
       ↓       ↓       ↓
     VM001   VM002   VM003
       ↓       ↓       ↓
     ESP32   ESP32   ESP32

Đây chính là nền tảng để biến một mô hình học tập thành hệ thống IoT vending machine.


24. Bảo mật

Khi đưa máy ra Internet, không được để:

ESP32 → API

mà không xác thực.

Có thể dùng:

API Key

hoặc token riêng cho từng máy:

VM001
TOKEN_XXXXXXXX

Server kiểm tra token trước khi trả dữ liệu.

Ngoài ra:

  • Chỉ sử dụng HTTPS.

  • Không lưu secret thanh toán trong firmware nếu không cần.

  • Không cho ESP32 tự quyết định giao dịch đã thanh toán.

  • Kiểm tra số tiền ở server.

  • Kiểm tra mã giao dịch.

  • Chống xử lý một giao dịch hai lần.


25. Chống nhả hàng hai lần

Giả sử ESP32 hỏi:

Order #10025 = paid

và nhận được:

paid

ESP32 nhả hàng.

Nhưng do mạng chập chờn, ESP32 lại hỏi lần nữa:

Order #10025 = paid

Nếu chương trình không cẩn thận:

Motor
 ↓
Lon 1

Motor
 ↓
Lon 2

Để tránh điều này, Order cần có trạng thái giao hàng:

delivery_status = dispensing

Sau khi giao thành công:

delivery_status = success

ESP32 không được nhả lại Order đã có:

success

26. State Machine hoàn chỉnh

Sau bài 8:

                    ┌──────────┐
                    │   IDLE   │
                    └────┬─────┘
                         ↓
                    ┌──────────┐
                    │  SELECT  │
                    └────┬─────┘
                         ↓
                 ┌──────────────┐
                 │ CREATE_ORDER │
                 └──────┬───────┘
                        ↓
                 ┌──────────────┐
                 │   SHOW_QR    │
                 └──────┬───────┘
                        ↓
                ┌────────────────┐
                │ WAIT_PAYMENT    │
                └───────┬────────┘
                        │
              ┌─────────┴─────────┐
              ↓                   ↓
           TIMEOUT              PAID
              ↓                   ↓
            IDLE             DISPENSE
                                  ↓
                                CHECK
                              ↙       ↘
                         SUCCESS      ERROR
                            ↓           ↓
                           IDLE       SUPPORT

Đây đã là một luồng giao dịch hoàn chỉnh.


🧪 Bài thực hành

Chưa cần kết nối ngân hàng thật.

Hãy xây dựng trước một phiên bản giả lập thanh toán:

Bước 1

Khách chọn:

A1

Bước 2

Laravel tạo:

Order #10001

với:

status = pending

Bước 3

Màn hình ESP32 hiển thị:

15.000đ

[ QR DEMO ]

Bước 4

Tạo một nút trong Admin:

[ XÁC NHẬN THANH TOÁN ]

Bước 5

Admin bấm nút.

Database chuyển:

pending
    ↓
paid

Bước 6

ESP32 nhận:

paid

và chuyển:

WAIT_PAYMENT
       ↓
DISPENSE

Bước 7

Motor nhả sản phẩm.

Như vậy bạn có thể kiểm tra toàn bộ kiến trúc thanh toán mà chưa cần kết nối dịch vụ thanh toán thật.


🎯 Tổng kết

Sau bài 8, kiến trúc của máy đã tiến thêm một bước lớn:

                INTERNET
                    │
                    ▼
             ┌────────────┐
             │  LARAVEL   │
             │   SERVER   │
             └─────┬──────┘
                   │
             Payment API
                   │
                   ▼
                  QR
                   │
                   ▼
                KHÁCH
                   │
                Thanh toán
                   │
                   ▼
             ┌────────────┐
             │  LARAVEL   │
             └─────┬──────┘
                   │
                PAID
                   │
                   ▼
                ESP32
                   │
                   ▼
                 MOTOR
                   │
                   ▼
                  🥤

Điểm quan trọng nhất của bài này là:

Thanh toán và nhả hàng phải là hai hệ thống riêng biệt, được kết nối bằng trạng thái giao dịch.

Đừng để ESP32 tự quyết định "đã thanh toán". Server phải xác nhận giao dịch, sau đó ESP32 mới được phép nhả sản phẩm.


🥤 Bài 9 — Quản lý kho và tự động báo hết hàng

Ở bài tiếp theo, chúng ta sẽ giải quyết một vấn đề rất thực tế:

Máy biết còn bao nhiêu lon?

Chúng ta sẽ xây dựng:

A1 → Coca → 5 lon
A2 → Pepsi → 2 lon
B1 → Sprite → 0 lon

Khi bán một lon:

5 → 4

Khi còn:

0

giao diện tự chuyển thành:

HẾT HÀNG

và hệ thống quản trị Laravel có thể biết máy nào đang cần nạp hàng.

Không có nhận xét nào:

Đăng nhận xét

Facebook Youtube RSS