NTM Solutions

Thứ Năm, 13 tháng 8, 2026

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

Ở bài 8, máy đã có thể:

  • Cho khách chọn sản phẩm.

  • Tạo đơn hàng.

  • Hiển thị QR.

  • Nhận xác nhận thanh toán.

  • Nhả sản phẩm.

  • Xác nhận giao hàng.

Nhưng còn một vấn đề rất quan trọng:

Máy phải biết trong mỗi ngăn còn bao nhiêu sản phẩm.

Nếu không quản lý tồn kho, khách có thể thanh toán một sản phẩm đã hết và máy mới phát hiện ra không thể giao hàng.


1. Tồn kho của máy bán nước

Giả sử máy có 4 ngăn:

A1 — Coca     — 5
A2 — Pepsi    — 4
B1 — Sprite   — 2
B2 — Sting    — 0

Trên màn hình:

┌─────────────────────────────┐
│       CHỌN SẢN PHẨM         │
│                             │
│  ┌────────┐   ┌────────┐   │
│  │ Coca   │   │ Pepsi  │   │
│  │ 15.000 │   │ 15.000 │   │
│  │ CÒN 5  │   │ CÒN 4  │   │
│  └────────┘   └────────┘   │
│                             │
│  ┌────────┐   ┌────────┐   │
│  │ Sprite │   │ Sting  │   │
│  │ 15.000 │   │HẾT HÀNG│   │
│  │ CÒN 2  │   │        │   │
│  └────────┘   └────────┘   │
└─────────────────────────────┘

Sản phẩm hết hàng phải được khóa ngay trên giao diện.


2. Có hai cách quản lý tồn kho

Đây là điểm rất quan trọng.

Cách 1 — Đếm bằng phần mềm

Ban đầu:

A1 = 5

Bán một lon:

5 → 4

Bán tiếp:

4 → 3

...

1 → 0

Đây là cách đơn giản nhất.


3. Cách 2 — Dùng cảm biến

Có thể đặt cảm biến để phát hiện sản phẩm thực tế.

Ví dụ:

🥤
🥤
🥤
🥤
🥤

Cảm biến ở cuối ngăn phát hiện còn sản phẩm hay không.

Hoặc dùng nhiều cảm biến:

🥤
──── Sensor 1

🥤
──── Sensor 2

🥤
──── Sensor 3

Cách này phức tạp hơn nhưng chính xác hơn trong một số thiết kế.


4. Phiên bản mini nên dùng cách nào?

Ở phiên bản học tập, chúng ta nên bắt đầu bằng:

Phần mềm quản lý số lượng + cảm biến xác nhận sản phẩm rơi.

Không cần lắp một cảm biến cho từng lon ngay từ đầu.

Luồng:

Bán hàng thành công

↓

Giảm tồn kho 1

↓

Cảm biến xác nhận lon rơi

↓

Hoàn tất

5. Tại sao không giảm kho ngay khi thanh toán?

Giả sử:

Tồn kho = 5

Khách thanh toán thành công.

Nếu ngay lập tức:

5 → 4

nhưng motor bị kẹt thì thực tế vẫn còn 5 lon.

Vì vậy tốt hơn là:

Thanh toán thành công
        ↓
Bắt đầu nhả hàng
        ↓
Cảm biến xác nhận
        ↓
Giảm tồn kho

Đây là một nguyên tắc quan trọng:

Chỉ ghi nhận đã bán khi sản phẩm thực sự được giao.


6. Database

Trong Laravel, chúng ta có thể tạo bảng products.

Ví dụ:

products

id
code
name
price
stock
status
created_at
updated_at

Dữ liệu:

1 | A1 | Coca   | 15000 | 5 | active
2 | A2 | Pepsi  | 15000 | 4 | active
3 | B1 | Sprite | 15000 | 2 | active
4 | B2 | Sting  | 15000 | 0 | active

7. Tại sao cần code?

Không nên chỉ dùng:

id = 1

Máy cần một mã dễ hiểu:

A1
A2
B1
B2

Ví dụ:

machine_products

machine_id | product_id | slot
VM001      | 1          | A1
VM001      | 2          | A2

Cách này còn tốt hơn nếu sau này có nhiều máy.


8. Một sản phẩm có thể nằm ở nhiều máy

Ví dụ:

Coca

VM001 → A1
VM002 → B2
VM003 → A4

Vì vậy không nên lưu:

products.stock

nếu stock đại diện cho tồn kho của từng máy.

Nên tách:

products

và:

machine_products

9. Thiết kế bảng machine_products

Ví dụ:

machine_products

id
machine_id
product_id
slot
stock
capacity
status

Dữ liệu:

1 | VM001 | Coca   | A1 | 5 | 6 | active
2 | VM001 | Pepsi  | A2 | 4 | 6 | active
3 | VM001 | Sprite | B1 | 2 | 6 | active
4 | VM001 | Sting  | B2 | 0 | 6 | out_of_stock

10. capacity để làm gì?

Ví dụ một ngăn chứa tối đa 6 lon:

capacity = 6

Hiện tại:

stock = 4

Có nghĩa:

Đã bán = 2

Còn = 4

Có thể nạp = 2

Dashboard có thể hiển thị:

A2

4 / 6

████████░░

11. Trạng thái sản phẩm

Có thể sử dụng:

active
out_of_stock
maintenance
disabled

Ví dụ:

A1 → active

A2 → active

B1 → out_of_stock

B2 → maintenance

Hai trạng thái cuối đều không cho khách mua.


12. Kiểm tra tồn kho trước khi tạo Order

Khi khách chọn A1:

SELECT A1

Server kiểm tra:

if ($machineProduct->stock <= 0) {
    return response()->json([
        'message' => 'Sản phẩm đã hết hàng.'
    ], 422);
}

Nếu còn:

stock > 0

mới tạo Order.


13. Không để ESP32 tự quyết định giá và tồn kho

Ví dụ ESP32 gửi:

{
    "product": "A1"
}

Server tự tìm:

A1
↓
Coca
↓
15.000đ
↓
stock = 5

Không nên để ESP32 gửi:

{
    "product": "A1",
    "price": 1000,
    "stock": 99
}

Server phải là nơi quyết định dữ liệu quan trọng.


14. Khi giao hàng thành công

Giả sử:

A1 = 5

Khách mua.

Cảm biến xác nhận:

PRODUCT_DETECTED

Server cập nhật:

5 → 4

Ví dụ Laravel:

$machineProduct->decrement('stock');

15. Khi hết hàng

Nếu:

stock = 1

Khách mua thành công:

1 → 0

Server chuyển:

status = out_of_stock

Giao diện:

┌──────────────┐
│    COCA      │
│              │
│   HẾT HÀNG   │
└──────────────┘

16. Không nên chỉ dựa vào stock = 0

Có thể xảy ra:

stock = 0
status = active

Điều này không tốt.

Tốt hơn là khi cập nhật:

if ($machineProduct->stock <= 0) {
    $machineProduct->update([
        'stock' => 0,
        'status' => 'out_of_stock',
    ]);
}

17. Khi nhân viên nạp hàng

Ví dụ:

A1 = 2

Nhân viên nạp thêm 4 lon:

2 + 4 = 6

Dashboard có nút:

[NẠP HÀNG]

Nhập:

Số lượng: 4

Server cập nhật:

stock = 6
status = active

Máy lập tức cho phép bán lại.


18. Không nên sửa stock trực tiếp

Một hệ thống thực tế nên có lịch sử nhập/xuất kho.

Ví dụ:

inventory_logs

id
machine_product_id
type
quantity
reason
created_at

19. Lịch sử kho

Ví dụ:

08:00  RESTOCK    +6
10:15  SALE       -1
10:30  SALE       -1
11:02  SALE       -1
12:20  RESTOCK    +3

Ta có thể biết chính xác:

Tại sao hôm nay A1 còn 6 lon?


20. Các loại biến động kho

Có thể dùng:

RESTOCK
SALE
ADJUSTMENT
DAMAGE
EXPIRED
RETURN

Ví dụ:

RESTOCK   +6
SALE      -1
DAMAGE    -1

21. Tạo lịch sử khi bán

Ví dụ:

InventoryLog::create([
    'machine_product_id' => $machineProduct->id,
    'type'                => 'SALE',
    'quantity'            => -1,
    'reason'              => 'Order #' . $order->id,
]);

Sau đó:

$machineProduct->decrement('stock');

Như vậy hệ thống vừa:

  • cập nhật tồn kho

  • lưu lịch sử


22. Cần Transaction

Đây là phần rất quan trọng.

Không nên:

Giảm stock

↓

Tạo log

↓

Lỗi

và cuối cùng dữ liệu bị lệch.

Laravel nên sử dụng database transaction:

DB::transaction(function () use ($machineProduct, $order) {

    $machineProduct->decrement('stock');

    InventoryLog::create([
        'machine_product_id' => $machineProduct->id,
        'type' => 'SALE',
        'quantity' => -1,
        'reason' => 'Order #' . $order->id,
    ]);

});

Nếu một thao tác lỗi:

Toàn bộ transaction được rollback.


23. Tránh bán vượt tồn kho

Giả sử chỉ còn:

stock = 1

Hai khách cùng lúc mua A1.

Nếu xử lý không cẩn thận:

Khách A → thấy 1
Khách B → thấy 1

A mua
B mua

1 → 0 → -1

Không được phép xảy ra.

Server cần khóa hoặc cập nhật dữ liệu một cách an toàn.

Ví dụ sử dụng transaction và lockForUpdate():

DB::transaction(function () use ($machineProduct) {

    $machineProduct = MachineProduct::whereKey(
        $machineProduct->id
    )
    ->lockForUpdate()
    ->first();

    if ($machineProduct->stock <= 0) {
        throw new RuntimeException('Out of stock');
    }

    $machineProduct->decrement('stock');
});

Đây là một kỹ thuật rất quan trọng khi hệ thống có nhiều người dùng cùng thao tác.


24. Cảm biến tồn kho

Đến đây phần mềm đã quản lý được:

A1 = 5

Nhưng máy vẫn cần biết sản phẩm thực tế có rơi hay không.

Đó là nhiệm vụ của cảm biến.

Ví dụ:

       🥤
       🥤
       🥤
       🥤
       🥤
       ↓
   ┌─────────┐
   │ IR      │
   │ SENSOR  │
   └─────────┘

Khi lon đi qua:

Sensor = DETECTED

ESP32 gửi:

{
    "machine": "VM001",
    "slot": "A1",
    "event": "product_delivered"
}

25. Tại sao cần cả phần mềm và cảm biến?

Vì hai thứ kiểm tra hai vấn đề khác nhau.

Database:

Theo hệ thống, còn bao nhiêu?

Sensor:

Sản phẩm có thực sự được giao không?

Kết hợp:

DATABASE
    +
SENSOR
    =
INVENTORY CONTROL

26. Nếu motor quay nhưng lon không rơi?

Ví dụ:

Payment = PAID

↓

Motor ON

↓

Encoder = OK

↓

Motor OFF

↓

IR Sensor = NO PRODUCT

Không được giảm kho.

Order:

payment_status = paid
delivery_status = failed

Máy có thể:

Thử lại
   ↓
Nếu vẫn lỗi
   ↓
Báo nhân viên

27. Tự động cảnh báo sắp hết hàng

Không cần chờ stock = 0.

Ví dụ:

stock <= 2

thì:

LOW STOCK

Dashboard:

⚠ A1 — Coca
   Còn 2 / 6

28. Mức cảnh báo

Có thể đặt:

stock > 2
→ NORMAL

stock <= 2
→ LOW STOCK

stock = 0
→ OUT OF STOCK

Sau này có thể cho mỗi sản phẩm một mức riêng:

min_stock = 2

29. Dashboard quản trị

Laravel Admin có thể hiển thị:

┌─────────────────────────────────────────────┐
│              INVENTORY                      │
├──────┬──────────┬───────┬────────┬─────────┤
│ Slot │ Product  │ Stock │ Status │ Action  │
├──────┼──────────┼───────┼────────┼─────────┤
│ A1   │ Coca     │ 5/6   │ Normal │         │
│ A2   │ Pepsi    │ 2/6   │ Low    │         │
│ B1   │ Sprite   │ 0/6   │ Empty  │         │
│ B2   │ Sting    │ 4/6   │ Normal │         │
└──────┴──────────┴───────┴────────┴─────────┘

30. Dashboard nhiều máy

Khi có nhiều vending machine:

VM001
├── A1 Coca     5
├── A2 Pepsi    2 ⚠
├── B1 Sprite   0 ❌
└── B2 Sting    6

VM002
├── A1 Coca     4
├── A2 Pepsi    5
├── B1 Sprite   3
└── B2 Sting    0 ❌

Admin có thể nhìn thấy ngay:

Máy nào cần nạp hàng.


31. Tự động tạo nhiệm vụ nạp hàng

Khi:

stock <= min_stock

Server có thể tạo:

Restock Task

Ví dụ:

TASK #1025

Machine: VM001
Slot: B1
Product: Sprite

Current: 0
Capacity: 6

→ CẦN NẠP 6 LON

Đây là bước đầu tiên để xây dựng hệ thống quản lý vận hành thực tế.


32. Luồng hoàn chỉnh

Bây giờ một giao dịch sẽ đi qua:

Khách chọn sản phẩm
        ↓
Kiểm tra tồn kho
        ↓
Tạo Order
        ↓
Thanh toán QR
        ↓
Payment = PAID
        ↓
ESP32 DISPENSE
        ↓
Motor quay
        ↓
Encoder xác nhận vị trí
        ↓
IR Sensor xác nhận lon
        ↓
Delivery = SUCCESS
        ↓
Stock - 1
        ↓
Inventory Log
        ↓
Kiểm tra Low Stock
        ↓
Hoàn tất

Đây là một luồng rất gần với cách chúng ta sẽ xây dựng phiên bản thực tế.


🧪 Bài thực hành

Chưa cần làm cảm biến tồn kho phức tạp.

Hãy xây dựng trước phần Backend.

Bước 1 — Tạo sản phẩm

A1 — Coca — 15.000đ
A2 — Pepsi — 15.000đ
B1 — Sprite — 15.000đ
B2 — Sting — 15.000đ

Bước 2 — Tạo máy

VM001

Bước 3 — Gán sản phẩm vào máy

A1 → Coca
A2 → Pepsi
B1 → Sprite
B2 → Sting

Bước 4 — Đặt tồn kho

A1 = 6
A2 = 6
B1 = 3
B2 = 0

Bước 5

Trên giao diện máy:

B2 → HẾT HÀNG

và không cho phép chọn.

Bước 6

Giả lập một giao dịch A1 thành công.

6 → 5

Tạo một InventoryLog:

SALE -1

Bước 7

Thử giảm:

3 → 2

Giao diện Admin phải hiển thị:

LOW STOCK

🎯 Tổng kết

Sau bài 9, máy của chúng ta đã có thêm một hệ thống quan trọng:

             VENDING MACHINE

                  ESP32
                    │
       ┌────────────┼────────────┐
       ↓            ↓            ↓
     Motor        Sensor        UI
       │            │
       └──────┬─────┘
              ↓
            SERVER
              │
       ┌──────┼──────┐
       ↓      ↓      ↓
     Order  Stock   Logs
       │      │
       └──────┼──────┘
              ↓
          ADMIN PANEL

Điểm quan trọng nhất:

Thanh toán thành công chưa đồng nghĩa với bán hàng thành công. Chỉ khi cảm biến xác nhận sản phẩm đã được giao thì hệ thống mới trừ tồn kho.

Và từ đây, máy đã bắt đầu có khả năng tự vận hành và báo tình trạng cho người quản lý.


🥤 Bài 10 — Hoàn thiện máy bán nước mini: ESP32 + Laravel + QR + Motor + Sensor

Bài tiếp theo sẽ ghép toàn bộ những gì đã học:

Màn hình
   ↓
Chọn sản phẩm
   ↓
Laravel
   ↓
QR Payment
   ↓
Xác nhận thanh toán
   ↓
ESP32
   ↓
Motor + Encoder
   ↓
Cảm biến
   ↓
Giao sản phẩm
   ↓
Trừ kho
   ↓
Laravel Admin

Đây sẽ là bài lắp ghép toàn bộ hệ thống thành một vending machine mini hoàn chỉnh, thay vì chỉ học từng linh kiện riêng lẻ.

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

Đăng nhận xét

Facebook Youtube RSS