POST /user/:username/follower — toggle follow/unfollow. Kiểm tra self-follow, tìm record cũ, flip isFollowing, cập nhật followersCount.Follow trong Prisma: followerId, followingId, isFollowing. Bảng InvitationRequest cũng tồn tại (cho Circle).getFollower (người follow mình).Toggle pattern: 1 endpoint xử lý cả follow và unfollow. Nếu đã follow → unfollow, ngược lại → follow.
| # | Task | Chi tiết triển khai | Status |
|---|---|---|---|
| 01 | Follow / Unfollow (toggle) POST /user/:username/follower |
✅ Đã có. Toggle isFollowing trên bảng Follow. Cập nhật followersCount trên User. Chặn self-follow. |
done |
| 02 | GET danh sách followers GET /user/:username/followers |
✅ Đã hoàn thành. Route + controller method sử dụng username params. Cursor pagination. | done |
| 03 | GET danh sách following GET /user/:username/following |
✅ Đã hoàn thành. Filter where: { followerId: userId }. Cursor pagination. |
done |
| 04 | Bảng FriendRequest Prisma migration |
✅ Đã thêm model FriendRequest vào schema. Prisma client đã được generate. |
done |
| 05 | Gửi lời mời kết bạn POST /user/:username/friend-request |
✅ Đã hoàn thành (chưa có realtime notification). Kiểm tra người dùng tồn tại, tạo record status=PENDING. | done |
| 06 | Chấp nhận / Hủy lời mời PATCH /me/friend-requests/:username/acceptPATCH /me/friend-requests/:username/cancel |
✅ Đã hoàn thành (chưa auto follow & realtime). Tách riêng 2 endpoint cho rõ ràng logic route. Cập nhật trạng thái ACCEPTED / CANCELLED sử dụng Transaction. |
done |
| 07 | Danh sách lời mời GET /me/friend-requests/receivedGET /me/friend-requests/sent |
✅ Đã hoàn thành. Lấy danh sách lời mời PENDING. Cursor pagination. Có include sender/receiver info (name, avatar, username). | done |
| 08 | Check mutual friend Helper function |
Hàm isMutualFriend(userA, userB): check cả 2 có follow lẫn nhau hoặc có FriendRequest status=ACCEPTED. Dùng cho feed filter FRIENDS_ONLY. |
todo |
| 09 | Realtime: Có người Follow Pusher Event |
Bắn event Pusher qua channel private-user-{userId} khi một user nhấn Follow. Gửi thông tin người follow để UI hiển thị Toast. |
todo |
| 10 | Realtime: Nhận Friend Request Pusher Event |
Bắn event báo có lời mời kết bạn mới khi gọi API POST /user/:username/friend-request. Cập nhật con số (badge) trên màn hình Frontend. |
todo |
| 11 | Realtime: Accept Friend Request Pusher Event |
Bắn event thông báo cho người gửi biết lời mời của họ đã được chấp nhận khi gọi API PATCH /me/friend-requests/:username. |
todo |
| 12 | Lưu Notification vào DB Prisma Schema |
Thiết kế bảng Notification để lưu lịch sử thông báo (để user xem lại khi offline). Các hành động trên sẽ vừa bắn event realtime vừa insert vào bảng này. |
todo |
Luồng Route mới: Thay vì xử lý Accept/Reject chung 1 endpoint, API đã tách biệt rõ ràng thành 2 endpoint độc lập /cancel và /accept. Việc này giúp tuân thủ SRP (Single Responsibility Principle) và thuận tiện cho việc mở rộng logic hoặc phân quyền sau này.
Luồng Cursor Pagination an toàn: Frontend chỉ truyền ID của user (sender/receiver), Backend tự ghép thành Composite Key để truy vấn trang tiếp theo. (Tương tự với danh sách "Sent" nhưng tráo đổi vị trí ID).
(followerId, followingId) trong DB + upsert thay vì check-then-create.followersCount trên User có thể sai nếu increment/decrement fail sau khi update Follow. Giải pháp: wrap trong transaction hoặc dùng background job recount định kỳ.AntiSpam có sẵn.FRIENDS_ONLY cần hiện bài user mới. Nếu dùng fan-out-on-write, cần enqueue bài cũ vào feed mới. Nếu fan-out-on-read thì chỉ cần update social graph cache.handleFriendRequestAccept và handleFriendRequestCancel, nhưng cả 2 đều sử dụng Prisma Transaction (transactionService.doInTransaction). Chuỗi hành động: Tìm ID đối tượng từ username → Kiểm tra lời mời có tồn tại không qua cặp (senderId, receiverId) tương ứng → Cập nhật trạng thái thành ACCEPTED hoặc CANCELLED. Việc tách 2 route giúp làm rõ logic module route. Toàn bộ được bọc trong transaction để dễ rollback khi lỗi.after (Cursor Pagination) cho Friend Requests:FriendRequest làm cursor nhằm bảo mật (tránh bị ID enumeration/cào data). Thay vào đó, cursor là ID của User trong cặp quan hệ:after phải là ID của người gửi (senderId) của record cuối cùng ở trang hiện tại.after phải là ID của người nhận (receiverId) của record cuối cùng ở trang hiện tại.after, Backend sẽ tự động dùng nó kết hợp với ID của người đang đăng nhập để tạo ra composite cursor { senderId_receiverId } query trực tiếp vào Database rất nhanh và an toàn. Frontend Developer cần chú ý truyền đúng giá trị này khi làm tính năng "Load more".
| Layer | Công nghệ | Ghi chú |
|---|---|---|
| Database | MySQL (Prisma) | Bảng Follow có sẵn. Cần thêm FriendRequest. Unique constraints cho idempotent. |
| API | Express Router + Zod validate | Validate params (username), body (action). Authorization middleware có sẵn. |
| Realtime | Pusher (Soketi) | Channel private-user-{userId}. Event: friend-request:new, friend-request:accepted. |
| Anti-spam | Redis sliding window | Key: follow-rate:{userId}. TTL 60s. Max 30 requests. |