Ở bài 8, máy đã có thể:
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ẻ.