0

[Series Thực Chiến E-commerce] Bài 35: Thuật toán "Thả Tim" - API Like Bài Viết (Like Blog)

Chào bố đời! Tới khúc này là bắt đầu thấy "khoai" rồi đây.

Tính năng Like/Dislike bài viết nhìn giao diện thì chỉ là một cái nút bấm, nhưng ẩn sau đó là một mớ logic khá lắt léo ở Backend. Chúng ta phải xử lý các kịch bản:

Đang bình thường -> Bấm Like -> Cộng Like.

Đang Like rồi -> Bấm Like phát nữa -> Bỏ Like (Toggle).

Đang Dislike (Chê) -> Đổi ý bấm Like -> Bỏ Dislike, Cộng Like.

Đoạn code của bố đời đã hình dung ra được luồng đi dùng $pull$push của Mongoose rất xuất sắc. Tuy nhiên, nó đang dính một bug logic chí mạng ở kịch bản số 3, cùng với một lỗi copy-paste comment kinh điển. Cùng mình "bắt bug" nhé!

1. Phẫu thuật Controller: Lỗi "Quên thả tim"

Anh em nhìn lại đoạn if đầu tiên trong code của mình:

// Kiểm tra nếu người dùng đã dislike bài viết này
  const alreadyDisliked = blog?.dislikes?.find(el => el.toString() === _id);
  if (alreadyDisliked) {
    // ❌ BUG LOGIC LÀ ĐÂY:
    const response = await Blog.findByIdAndUpdate(id, { $pull: { dislikes: _id } }, { new: true });
    return res.json({ ... }); 
  }

Phân tích lỗi: Giả sử mình đang ghét bài viết này (Dislike). Sau đó mình đọc lại thấy hay quá, mình bấm nút Like. Code của bố đời chạy vào hàm if (alreadyDisliked), nó $pull (xóa) mình khỏi mảng dislikes, sau đó nó return kết thúc hàm luôn! Kết quả: Mình bấm Like, nhưng hệ thống chỉ xóa Dislike chứ không hề thêm Like cho mình. Khách hàng phải bấm nút Like lần thứ 2 thì nó mới chạy xuống dưới để $push vào mảng likes. Trải nghiệm người dùng (UX) như vậy là hỏng bét!

Cách fix chuẩn thực chiến: Nếu đang Dislike mà bấm Like, chúng ta phải làm 2 việc cùng lúc: Rút tên khỏi sổ thù vặt ($pull dislikes) VÀ ghi danh vào bảng vàng ($push likes). Mongoose cho phép làm điều này trong 1 nốt nhạc!

Anh em sửa lại file controllers/blog.js như sau:

const mongoose = require('mongoose');

// Hàm likeBlog xử lý việc thích hoặc bỏ thích một bài viết
const likeBlog = asyncHandler(async (req, res) => {
  const { _id } = req.user;  // Lấy _id của user từ token
  
  // 💡 Tip: Sửa lại tên biến thành 'bid' (Blog ID) cho đồng bộ với Bài 32 & 34 nhé
  // Lỗi copy-paste: Comment ghi lấy từ body nhưng code là lấy từ params kìa bố đời =))
  const { bid } = req.params;   
  
  if (!bid) return res.status(400).json({ message: 'Blog ID is required' });

  // Kiểm tra ID hợp lệ
  if (!mongoose.Types.ObjectId.isValid(bid)) {
    return res.status(400).json({ message: 'Invalid Blog ID' });
  }

  const blog = await Blog.findById(bid);
  if (!blog) return res.status(404).json({ message: 'Blog not found' });

  // 1. Kiểm tra xem người dùng có đang DISLIKE bài này không?
  const alreadyDisliked = blog?.dislikes?.find(el => el.toString() === _id);
  if (alreadyDisliked) {
    // Đang chê mà chuyển sang khen -> Rút khỏi mảng dislikes, Thêm vào mảng likes
    const response = await Blog.findByIdAndUpdate(bid, { 
        $pull: { dislikes: _id },
        $push: { likes: _id } 
    }, { new: true });
    
    return res.json({ success: true, rs: response });
  }

  // 2. Kiểm tra xem người dùng có đang LIKE bài này không?
  const isLiked = blog?.likes?.find(el => el.toString() === _id);
  if (isLiked) {
    // Đã khen rồi mà bấm khen tiếp -> Rút lại lời khen (Bỏ Like)
    const response = await Blog.findByIdAndUpdate(bid, { $pull: { likes: _id } }, { new: true });
    return res.json({ success: true, rs: response });
  } else {
    // 3. Chưa làm gì cả -> Thêm khen ngợi (Cộng Like)
    const response = await Blog.findByIdAndUpdate(bid, { $push: { likes: _id } }, { new: true });
    return res.json({ success: true, rs: response });
  }
});

module.exports = {
  // ... (nhớ export)
  likeBlog
}

2. Trạm gác Router: Đặt chốt cực kỳ chính xác

Sang đến file routers/blog.js, bố đời đặt chốt chặn vô cùng hoàn hảo!

router.put('/blogs/like/:bid', [verifyAccessToken], ctrls.likeBlog);

Hành động Like là hành động cần danh tính (để biết ai là người Like mà còn lưu _id vào mảng). Do đó bắt buộc phải có verifyAccessToken. Nhưng vì độc giả bình thường cũng có thể Like, nên không được dùng chốt isAdmin. Tư duy phân quyền ở đây của anh em đã rất cứng cáp rồi đấy!

Lời kết

Bật Postman lên và test ngay tính năng "Hack não" này:

Gửi request lần 1: Mảng likes có thêm ID của bạn.

Gửi request lần 2: Mảng likes mất ID của bạn (Bỏ like thành công).

Khi đã hiểu rõ thuật toán của nút Like, thì việc làm nút Dislike (Chê) cũng chỉ là sự tráo đổi vị trí của các mảng likes và dislikes mà thôi.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí