1. 12:00 06/08/2026
Tiếp Phần 6 (đã có đăng nhập/đăng ký). Đây là phần QUAN TRỌNG NHẤT của cả loạt bài: xử lý đặt hàng thật sự - tạo đơn hàng, trừ tồn kho, xoá giỏ hàng, và cho khách xem lại lịch sử đơn hàng của mình.
==============================
1. FILE checkout.php (trang đặt hàng)
==============================
<?php
require_once __DIR__ . '/includes/functions.php';
require_login();
$page_title = 'Dat hang';
// Lay gio hang, tinh lai gia + tong tien tu CSDL (khong tin gia tu session/form)
$cart_items = [];
$total = 0;
if (!empty($_SESSION['cart'])) {
$ids = array_keys($_SESSION['cart']);
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT id, name, price, sale_price, stock FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
foreach ($stmt->fetchAll() as $p) {
$qty = (int) $_SESSION['cart'][$p['id']];
$price = $p['sale_price'] ?: $p['price'];
$subtotal = $price * $qty;
$total += $subtotal;
$cart_items[] = ['product' => $p, 'qty' => $qty, 'price' => $price, 'subtotal' => $subtotal];
}
}
if (empty($cart_items)) {
redirect('cart.php');
}
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
$full_name = trim($_POST['full_name'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$address = trim($_POST['address'] ?? '');
$note = trim($_POST['note'] ?? '');
$payment_method = ($_POST['payment_method'] ?? 'cod') === 'bank_transfer' ? 'bank_transfer' : 'cod';
if ($full_name === '') $errors[] = 'Vui long nhap ho ten nguoi nhan.';
if ($phone === '') $errors[] = 'Vui long nhap so dien thoai.';
if ($address === '') $errors[] = 'Vui long nhap dia chi giao hang.';
// Kiem tra lai ton kho ngay truoc khi tao don, tranh truong hop 2 khach
// cung dat 1 san pham sap het hang trong cung 1 thoi diem
foreach ($cart_items as $item) {
if ($item['qty'] > (int) $item['product']['stock']) {
$errors[] = 'San pham "' . $item['product']['name'] . '" khong du ton kho.';
}
}
if (empty($errors)) {
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare(
'INSERT INTO orders (user_id, full_name, phone, address, note, payment_method, total_amount, status)
VALUES (:u, :n, :p, :a, :note, :pm, :total, "pending")'
);
$stmt->execute([
':u' => $_SESSION['user_id'],
':n' => $full_name,
':p' => $phone,
':a' => $address,
':note' => $note,
':pm' => $payment_method,
':total' => $total,
]);
$order_id = (int) $pdo->lastInsertId();
$item_stmt = $pdo->prepare(
'INSERT INTO order_items (order_id, product_id, product_name, price, quantity, subtotal)
VALUES (:o, :pid, :name, :price, :qty, :sub)'
);
$stock_stmt = $pdo->prepare('UPDATE products SET stock = stock - :qty WHERE id = :id AND stock >= :qty2');
foreach ($cart_items as $item) {
$item_stmt->execute([
':o' => $order_id,
':pid' => $item['product']['id'],
':name' => $item['product']['name'],
':price' => $item['price'],
':qty' => $item['qty'],
':sub' => $item['subtotal'],
]);
$stock_stmt->execute([':qty' => $item['qty'], ':id' => $item['product']['id'], ':qty2' => $item['qty']]);
if ($stock_stmt->rowCount() === 0) {
// Het hang dung luc dang xu ly -> huy toan bo giao dich
throw new RuntimeException('San pham "' . $item['product']['name'] . '" vua het hang.');
}
}
$pdo->commit();
unset($_SESSION['cart']);
redirect('order_success.php?id=' . $order_id);
} catch (Throwable $e) {
$pdo->rollBack();
$errors[] = 'Dat hang khong thanh cong: ' . $e->getMessage();
}
}
}
require __DIR__ . '/includes/header.php';
?>
<h1 class="page-title">Xac nhan don hang</h1>
<?php foreach ($errors as $err): ?>
<div class="alert alert-error"><?= e($err) ?></div>
<?php endforeach; ?>
<div style="display:flex; gap:24px; flex-wrap:wrap;">
<form action="checkout.php" method="post" class="form-box" style="flex:1; min-width:300px;">
<?= csrf_field() ?>
<div class="form-group">
<label>Ho ten nguoi nhan</label>
<input type="text" name="full_name" value="<?= e($_POST['full_name'] ?? $user['full_name']) ?>" required>
</div>
<div class="form-group">
<label>So dien thoai</label>
<input type="text" name="phone" value="<?= e($_POST['phone'] ?? $user['phone']) ?>" required>
</div>
<div class="form-group">
<label>Dia chi giao hang</label>
<textarea name="address" rows="3" required><?= e($_POST['address'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label>Ghi chu (tuy chon)</label>
<textarea name="note" rows="2"><?= e($_POST['note'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label>Hinh thuc thanh toan</label>
<label style="font-weight:normal;"><input type="radio" name="payment_method" value="cod" checked> Thanh toan khi nhan hang (COD)</label><br>
<label style="font-weight:normal;"><input type="radio" name="payment_method" value="bank_transfer"> Chuyen khoan ngan hang</label>
</div>
<button type="submit" class="btn" style="width:100%;">Dat hang</button>
</form>
<div class="form-box" style="flex:1; min-width:300px;">
<h2 style="font-size:16px; margin-bottom:12px;">Don hang cua ban</h2>
<?php foreach ($cart_items as $item): ?>
<div style="display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px solid #eee; font-size:14px;">
<span><?= e($item['product']['name']) ?> x<?= $item['qty'] ?></span>
<span><?= format_price($item['subtotal']) ?></span>
</div>
<?php endforeach; ?>
<div style="display:flex; justify-content:space-between; padding-top:12px; font-size:16px;">
<strong>Tong cong</strong>
<strong class="price"><?= format_price($total) ?></strong>
</div>
</div>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
==============================
2. FILE order_success.php (đặt hàng thành công)
==============================
<?php
require_once __DIR__ . '/includes/functions.php';
require_login();
$page_title = 'Dat hang thanh cong';
$order_id = (int) ($_GET['id'] ?? 0);
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ? AND user_id = ? LIMIT 1');
$stmt->execute([$order_id, $_SESSION['user_id']]);
$order = $stmt->fetch();
if (!$order) {
http_response_code(404);
die('Khong tim thay don hang.');
}
require __DIR__ . '/includes/header.php';
?>
<div class="form-box" style="text-align:center;">
<h1 class="page-title">Dat hang thanh cong!</h1>
<p>Ma don hang cua ban la <strong>#<?= $order['id'] ?></strong></p>
<p>Tong tien: <strong class="price"><?= format_price($order['total_amount']) ?></strong></p>
<p>Chung toi se lien he qua so dien thoai <strong><?= e($order['phone']) ?></strong> de xac nhan don hang.</p>
<p style="margin-top:16px;"><a href="account.php" class="btn">Xem lich su don hang</a></p>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
==============================
3. FILE account.php (cập nhật - bổ sung lịch sử đơn hàng)
==============================
<?php
require_once __DIR__ . '/includes/functions.php';
require_login();
$page_title = 'Tai khoan cua toi';
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
$stmt = $pdo->prepare('SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$_SESSION['user_id']]);
$orders = $stmt->fetchAll();
$status_labels = [
'pending' => 'Cho xac nhan',
'confirmed' => 'Da xac nhan',
'shipping' => 'Dang giao hang',
'completed' => 'Hoan thanh',
'cancelled' => 'Da huy',
];
require __DIR__ . '/includes/header.php';
?>
<h1 class="page-title">Xin chao, <?= e($user['full_name']) ?></h1>
<p>Email: <?= e($user['email']) ?> - SDT: <?= e($user['phone'] ?: 'Chua cap nhat') ?></p>
<h2 style="margin-top:24px; font-size:18px;">Lich su don hang</h2>
<table class="data-table" style="margin-top:10px;">
<thead>
<tr><th>Ma don</th><th>Ngay dat</th><th>Tong tien</th><th>Trang thai</th><th></th></tr>
</thead>
<tbody>
<?php foreach ($orders as $o): ?>
<tr>
<td>#<?= $o['id'] ?></td>
<td><?= date('d/m/Y H:i', strtotime($o['created_at'])) ?></td>
<td><?= format_price($o['total_amount']) ?></td>
<td><?= e($status_labels[$o['status']] ?? $o['status']) ?></td>
<td><a href="order_view.php?id=<?= $o['id'] ?>">Xem chi tiet</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<tr><td colspan="5">Ban chua co don hang nao.</td></tr>
<?php endif; ?>
</tbody>
</table>
<?php require __DIR__ . '/includes/footer.php'; ?>
==============================
4. FILE order_view.php (khách xem chi tiết 1 đơn hàng của chính mình)
==============================
<?php
require_once __DIR__ . '/includes/functions.php';
require_login();
$order_id = (int) ($_GET['id'] ?? 0);
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ? AND user_id = ? LIMIT 1');
$stmt->execute([$order_id, $_SESSION['user_id']]);
$order = $stmt->fetch();
if (!$order) {
http_response_code(404);
die('Khong tim thay don hang.');
}
$page_title = 'Don hang #' . $order['id'];
$stmt = $pdo->prepare('SELECT * FROM order_items WHERE order_id = ?');
$stmt->execute([$order_id]);
$items = $stmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<h1 class="page-title">Chi tiet don hang #<?= $order['id'] ?></h1>
<p>Nguoi nhan: <?= e($order['full_name']) ?> - <?= e($order['phone']) ?></p>
<p>Dia chi: <?= e($order['address']) ?></p>
<table class="data-table" style="margin-top:14px;">
<thead><tr><th>San pham</th><th>Don gia</th><th>SL</th><th>Thanh tien</th></tr></thead>
<tbody>
<?php foreach ($items as $it): ?>
<tr>
<td><?= e($it['product_name']) ?></td>
<td><?= format_price($it['price']) ?></td>
<td><?= $it['quantity'] ?></td>
<td><?= format_price($it['subtotal']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p style="text-align:right; margin-top:12px; font-size:18px;">Tong cong: <strong class="price"><?= format_price($order['total_amount']) ?></strong></p>
<?php require __DIR__ . '/includes/footer.php'; ?>
==============================
GIẢI THÍCH CÁC ĐIỂM QUAN TRỌNG NHẤT PHẦN NÀY
==============================
1. DÙNG TRANSACTION ($pdo->beginTransaction() / commit() / rollBack()): tạo đơn hàng gồm nhiều bước ghi CSDL (insert orders, insert nhiều order_items, trừ stock từng sản phẩm) - nếu 1 bước giữa chừng lỗi (VD hết hàng) mà không dùng transaction thì sẽ tạo ra đơn hàng "què" (đã trừ kho sản phẩm này nhưng chưa kịp trừ sản phẩm kia). Transaction đảm bảo TẤT CẢ thành công hoặc TẤT CẢ được huỷ bỏ (rollback), dữ liệu không bao giờ ở trạng thái nửa vời.
2. CÂU UPDATE stock = stock - :qty WHERE id = :id AND stock >= :qty2: đây là kỹ thuật trừ kho AN TOÀN khi có nhiều người mua cùng lúc (race condition) - nếu chỉ SELECT stock ra kiểm tra rồi mới UPDATE riêng thì giữa 2 bước đó có thể có người khác mua trước làm hết hàng. Viết điều kiện "stock >= quantity" NGAY TRONG câu UPDATE giúp MySQL tự kiểm tra và cập nhật trong 1 bước nguyên tử (atomic), rowCount() = 0 nghĩa là không đủ hàng để trừ.
3. Giá tiền tính lại 100% từ CSDL (bảng products) ở checkout.php, KHÔNG lấy số tiền từ session/form gửi lên - nguyên tắc "không bao giờ tin dữ liệu phía client" để tránh khách sửa giá tiền qua DevTools trước khi gửi đơn.
4. order_view.php và order_success.php đều có điều kiện "WHERE id = ? AND user_id = ?" - đảm bảo khách A không xem được đơn hàng của khách B chỉ bằng cách đổi số id trên URL (lỗ hổng gọi là IDOR - Insecure Direct Object Reference).
Vậy là website đã hoàn chỉnh phần "bán hàng" cho khách. Phần 8 tiếp theo sẽ làm trang QUẢN TRỊ (admin) để chủ shop tự thêm/sửa/xoá danh mục, sản phẩm và xử lý đơn hàng - không phải vào phpMyAdmin sửa tay nữa. Hẹn gặp lại!
--- Hết Phần 7 ---
----------------------------------------
MỤC LỤC LOẠT BÀI:
1. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 1: Tổng Quan Dự Án, Yêu Cầu Môi Trường & Cấu Trúc Thư Mục - https://diendan.anyvlogs.info/chu-de/457-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-1-tong-quan-du-an-yeu-cau-moi-truong-cau-truc-thu-muc
2. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 2: Thiết Kế Cơ Sở Dữ Liệu (Database) Đầy Đủ Cho Website Bán Hàng - https://diendan.anyvlogs.info/chu-de/458-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-2-thiet-ke-co-so-du-lieu-database-day-du-cho-website-ban-hang
3. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 3: Dựng Khung Ứng Dụng - Kết Nối PDO, Hàm Dùng Chung, Giao Diện Chung - https://diendan.anyvlogs.info/chu-de/459-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-3-dung-khung-ung-dung-ket-noi-pdo-ham-dung-chung-giao-dien-chung
4. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 4: Trang Chủ, Danh Mục Sản Phẩm & Chi Tiết Sản Phẩm - https://diendan.anyvlogs.info/chu-de/460-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-4-trang-chu-danh-muc-san-pham-chi-tiet-san-pham
5. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 5: Xây Dựng Giỏ Hàng (Cart) Bằng Session - https://diendan.anyvlogs.info/chu-de/461-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-5-xay-dung-gio-hang-cart-bang-session
6. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 6: Đăng Ký, Đăng Nhập, Đăng Xuất Thành Viên - https://diendan.anyvlogs.info/chu-de/462-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-6-dang-ky-dang-nhap-dang-xuat-thanh-vien
>> 7. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 7: Đặt Hàng, Thanh Toán COD & Lịch Sử Đơn Hàng - https://diendan.anyvlogs.info/chu-de/463-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-7-dat-hang-thanh-toan-cod-lich-su-don-hang
8. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 8: Trang Quản Trị (Admin) - Quản Lý Danh Mục, Sản Phẩm, Đơn Hàng - https://diendan.anyvlogs.info/chu-de/464-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-8-trang-quan-tri-admin-quan-ly-danh-muc-san-pham-don-hang
9. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 9: Bảo Mật Website (CSRF, XSS, SQL Injection, Upload An Toàn) - https://diendan.anyvlogs.info/chu-de/465-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-9-bao-mat-website-csrf-xss-sql-injection-upload-an-toan
10. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 10 (Kết Thúc): Tối Ưu Hiệu Năng & Triển Khai Lên Hosting Thật - https://diendan.anyvlogs.info/chu-de/466-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-10-ket-thuc-toi-uu-hieu-nang-trien-khai-len-hosting-that
Phần trước: https://diendan.anyvlogs.info/chu-de/462-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-6-dang-ky-dang-nhap-dang-xuat-thanh-vien
Phần sau: https://diendan.anyvlogs.info/chu-de/464-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-8-trang-quan-tri-admin-quan-ly-danh-muc-san-pham-don-hang
0