From 6631730b28a31dac6b6f3120d989bc7656b50ad3 Mon Sep 17 00:00:00 2001 From: kenduNMT Date: Fri, 14 Nov 2025 14:43:02 +0700 Subject: [PATCH] cart --- .../controller/CartController.java | 77 +++++++ .../model/dto/request/AddToCartRequest.java | 19 ++ .../dto/request/UpdateCartItemRequest.java | 16 ++ .../model/dto/response/AuthResponse.java | 1 + .../model/dto/response/CartItemResponse.java | 19 ++ .../model/dto/response/CartResponse.java | 20 ++ .../model/entity/CartItem.java | 2 +- .../repository/CartItemRepository.java | 24 +++ .../repository/CartRepository.java | 24 +++ .../service/AuthService.java | 4 +- .../service/CartService.java | 199 ++++++++++++++++++ 11 files changed, 402 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/example/project_it207_server/controller/CartController.java create mode 100644 src/main/java/com/example/project_it207_server/model/dto/request/AddToCartRequest.java create mode 100644 src/main/java/com/example/project_it207_server/model/dto/request/UpdateCartItemRequest.java create mode 100644 src/main/java/com/example/project_it207_server/model/dto/response/CartItemResponse.java create mode 100644 src/main/java/com/example/project_it207_server/model/dto/response/CartResponse.java create mode 100644 src/main/java/com/example/project_it207_server/repository/CartItemRepository.java create mode 100644 src/main/java/com/example/project_it207_server/repository/CartRepository.java create mode 100644 src/main/java/com/example/project_it207_server/service/CartService.java diff --git a/src/main/java/com/example/project_it207_server/controller/CartController.java b/src/main/java/com/example/project_it207_server/controller/CartController.java new file mode 100644 index 0000000..f0678d9 --- /dev/null +++ b/src/main/java/com/example/project_it207_server/controller/CartController.java @@ -0,0 +1,77 @@ +package com.example.project_it207_server.controller; + +import com.example.project_it207_server.model.dto.request.AddToCartRequest; +import com.example.project_it207_server.model.dto.request.UpdateCartItemRequest; +import com.example.project_it207_server.model.dto.response.CartResponse; +import com.example.project_it207_server.service.CartService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/cart") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class CartController { + + private final CartService cartService; + + /** + * Add product to cart + * POST /api/cart/{userId} + */ + @PostMapping("/{userId}") + public ResponseEntity addToCart( + @PathVariable Long userId, + @Valid @RequestBody AddToCartRequest request) { + CartResponse response = cartService.addToCart(userId, request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + /** + * Get cart by user ID + * GET /api/cart/{userId} + */ + @GetMapping("/{userId}") + public ResponseEntity getCart(@PathVariable Long userId) { + CartResponse response = cartService.getCartByUserId(userId); + return ResponseEntity.ok(response); + } + + /** + * Update cart item quantity + * PUT /api/cart/{userId}/items/{cartItemId} + */ + @PutMapping("/{userId}/items/{cartItemId}") + public ResponseEntity updateCartItem( + @PathVariable Long userId, + @PathVariable Long cartItemId, + @Valid @RequestBody UpdateCartItemRequest request) { + CartResponse response = cartService.updateCartItem(userId, cartItemId, request); + return ResponseEntity.ok(response); + } + + /** + * Remove item from cart + * DELETE /api/cart/{userId}/items/{cartItemId} + */ + @DeleteMapping("/{userId}/items/{cartItemId}") + public ResponseEntity removeCartItem( + @PathVariable Long userId, + @PathVariable Long cartItemId) { + CartResponse response = cartService.removeCartItem(userId, cartItemId); + return ResponseEntity.ok(response); + } + + /** + * Clear all items from cart + * DELETE /api/cart/{userId}/clear + */ + @DeleteMapping("/{userId}/clear") + public ResponseEntity clearCart(@PathVariable Long userId) { + cartService.clearCart(userId); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/model/dto/request/AddToCartRequest.java b/src/main/java/com/example/project_it207_server/model/dto/request/AddToCartRequest.java new file mode 100644 index 0000000..5fb4557 --- /dev/null +++ b/src/main/java/com/example/project_it207_server/model/dto/request/AddToCartRequest.java @@ -0,0 +1,19 @@ +package com.example.project_it207_server.model.dto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AddToCartRequest { + @NotNull(message = "Product ID is required") + private Long productId; + + @NotNull(message = "Quantity is required") + @Min(value = 1, message = "Quantity must be at least 1") + private Integer quantity; +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/model/dto/request/UpdateCartItemRequest.java b/src/main/java/com/example/project_it207_server/model/dto/request/UpdateCartItemRequest.java new file mode 100644 index 0000000..e42a3ed --- /dev/null +++ b/src/main/java/com/example/project_it207_server/model/dto/request/UpdateCartItemRequest.java @@ -0,0 +1,16 @@ +package com.example.project_it207_server.model.dto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UpdateCartItemRequest { + @NotNull(message = "Quantity is required") + @Min(value = 1, message = "Quantity must be at least 1") + private Integer quantity; +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/model/dto/response/AuthResponse.java b/src/main/java/com/example/project_it207_server/model/dto/response/AuthResponse.java index e223d42..e6a2338 100644 --- a/src/main/java/com/example/project_it207_server/model/dto/response/AuthResponse.java +++ b/src/main/java/com/example/project_it207_server/model/dto/response/AuthResponse.java @@ -9,6 +9,7 @@ import lombok.Setter; @Setter @AllArgsConstructor public class AuthResponse { + private Long userId; private String token; private String email; private Role role; diff --git a/src/main/java/com/example/project_it207_server/model/dto/response/CartItemResponse.java b/src/main/java/com/example/project_it207_server/model/dto/response/CartItemResponse.java new file mode 100644 index 0000000..c19cdec --- /dev/null +++ b/src/main/java/com/example/project_it207_server/model/dto/response/CartItemResponse.java @@ -0,0 +1,19 @@ +package com.example.project_it207_server.model.dto.response; + +import lombok.*; +import java.math.BigDecimal; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CartItemResponse { + private Long cartItemId; + private Long productId; + private String productName; + private String productImage; + private Integer quantity; + private BigDecimal price; + private BigDecimal subtotal; // price * quantity +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/model/dto/response/CartResponse.java b/src/main/java/com/example/project_it207_server/model/dto/response/CartResponse.java new file mode 100644 index 0000000..e54a839 --- /dev/null +++ b/src/main/java/com/example/project_it207_server/model/dto/response/CartResponse.java @@ -0,0 +1,20 @@ +package com.example.project_it207_server.model.dto.response; + +import lombok.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CartResponse { + private Long cartId; + private Long userId; + private List items; + private BigDecimal totalAmount; // Tổng tiền của tất cả items + private Integer totalItems; // Tổng số lượng sản phẩm + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/model/entity/CartItem.java b/src/main/java/com/example/project_it207_server/model/entity/CartItem.java index f415130..ff5701e 100644 --- a/src/main/java/com/example/project_it207_server/model/entity/CartItem.java +++ b/src/main/java/com/example/project_it207_server/model/entity/CartItem.java @@ -12,7 +12,7 @@ import java.math.BigDecimal; @NoArgsConstructor @AllArgsConstructor @Builder -class CartItem { +public class CartItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long cartItemId; diff --git a/src/main/java/com/example/project_it207_server/repository/CartItemRepository.java b/src/main/java/com/example/project_it207_server/repository/CartItemRepository.java new file mode 100644 index 0000000..bb202f1 --- /dev/null +++ b/src/main/java/com/example/project_it207_server/repository/CartItemRepository.java @@ -0,0 +1,24 @@ +package com.example.project_it207_server.repository; + +import com.example.project_it207_server.model.entity.Cart; +import com.example.project_it207_server.model.entity.CartItem; +import com.example.project_it207_server.model.entity.Product; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.List; + +@Repository +public interface CartItemRepository extends JpaRepository { + + @Query("SELECT ci FROM CartItem ci WHERE ci.cart = :cart AND ci.product = :product") + List findAllByCartAndProduct(@Param("cart") Cart cart, @Param("product") Product product); + + @Query("SELECT ci FROM CartItem ci WHERE ci.cart = :cart AND ci.product = :product ORDER BY ci.cartItemId ASC LIMIT 1") + Optional findByCartAndProduct(@Param("cart") Cart cart, @Param("product") Product product); + + void deleteAllByCart(Cart cart); +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/repository/CartRepository.java b/src/main/java/com/example/project_it207_server/repository/CartRepository.java new file mode 100644 index 0000000..b15de6e --- /dev/null +++ b/src/main/java/com/example/project_it207_server/repository/CartRepository.java @@ -0,0 +1,24 @@ +// CartRepository.java +package com.example.project_it207_server.repository; + +import com.example.project_it207_server.model.entity.Cart; +import com.example.project_it207_server.model.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.List; + +@Repository +public interface CartRepository extends JpaRepository { + + // Thay đổi: Sử dụng List thay vì Optional để tránh lỗi + @Query("SELECT c FROM Cart c WHERE c.user = :user ORDER BY c.createdAt DESC") + List findAllByUser(@Param("user") User user); + + // Hoặc giữ nguyên nhưng thêm LIMIT 1 + @Query("SELECT c FROM Cart c WHERE c.user = :user ORDER BY c.createdAt DESC LIMIT 1") + Optional findByUser(@Param("user") User user); +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/service/AuthService.java b/src/main/java/com/example/project_it207_server/service/AuthService.java index 8268317..50a7663 100644 --- a/src/main/java/com/example/project_it207_server/service/AuthService.java +++ b/src/main/java/com/example/project_it207_server/service/AuthService.java @@ -51,7 +51,7 @@ public class AuthService { String redisKey = "token:" + newUser.getEmail() + ":" + token; redisService.setValueWithExpiry(redisKey, token, 1, TimeUnit.DAYS);// TTL 1 ngày - return new AuthResponse(token, newUser.getEmail(), newUser.getRole()); + return new AuthResponse(newUser.getUserId(), token, newUser.getEmail(), newUser.getRole()); } public AuthResponse login(AuthRequest req) { @@ -68,7 +68,7 @@ public class AuthService { String redisKey = "token:" + user.getEmail() + ":" + token; redisService.setValueWithExpiry(redisKey, token, 86400L, TimeUnit.SECONDS); - return new AuthResponse(token, user.getEmail(), user.getRole()); + return new AuthResponse(user.getUserId(), token, user.getEmail(), user.getRole()); } public void logout(String token) { diff --git a/src/main/java/com/example/project_it207_server/service/CartService.java b/src/main/java/com/example/project_it207_server/service/CartService.java new file mode 100644 index 0000000..9c8b55e --- /dev/null +++ b/src/main/java/com/example/project_it207_server/service/CartService.java @@ -0,0 +1,199 @@ +package com.example.project_it207_server.service; + +import com.example.project_it207_server.model.dto.request.AddToCartRequest; +import com.example.project_it207_server.model.dto.request.UpdateCartItemRequest; +import com.example.project_it207_server.model.dto.response.CartItemResponse; +import com.example.project_it207_server.model.dto.response.CartResponse; +import com.example.project_it207_server.model.entity.*; +import com.example.project_it207_server.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CartService { + + private final CartRepository cartRepository; + private final CartItemRepository cartItemRepository; + private final ProductRepository productRepository; + private final UserRepository userRepository; + + @Transactional + public CartResponse addToCart(Long userId, AddToCartRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + Product product = productRepository.findById(request.getProductId()) + .orElseThrow(() -> new RuntimeException("Product not found")); + + // Get or create cart for user + Cart cart = cartRepository.findByUser(user) + .orElseGet(() -> { + Cart newCart = Cart.builder() + .user(user) + .items(new ArrayList<>()) + .createdAt(LocalDateTime.now()) + .build(); + return cartRepository.save(newCart); + }); + + // Check if product already exists in cart + // FIX: Xử lý trường hợp có nhiều CartItem trùng lặp + List existingItems = cartItemRepository.findAllByCartAndProduct(cart, product); + + CartItem cartItem; + if (!existingItems.isEmpty()) { + // Nếu có nhiều item trùng, lấy item đầu tiên và xóa các item còn lại + cartItem = existingItems.get(0); + + // Xóa các item trùng lặp (nếu có) + for (int i = 1; i < existingItems.size(); i++) { + cartItemRepository.delete(existingItems.get(i)); + } + } else { + // Tạo mới nếu chưa có + cartItem = CartItem.builder() + .cart(cart) + .product(product) + .quantity(0) + .price(product.getPrice()) + .build(); + } + + // Update quantity + cartItem.setQuantity(cartItem.getQuantity() + request.getQuantity()); + cartItem.setPrice(product.getPrice()); + cartItemRepository.save(cartItem); + + return getCartByUserId(userId); + } + + @Transactional + public CartResponse getCartByUserId(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + // FIX: Xử lý trường hợp có nhiều cart + List carts = cartRepository.findAllByUser(user); + + Cart cart; + if (carts.isEmpty()) { + // Tạo cart mới nếu chưa có + cart = Cart.builder() + .user(user) + .items(new ArrayList<>()) + .createdAt(LocalDateTime.now()) + .build(); + cart = cartRepository.save(cart); + } else { + // Lấy cart mới nhất + cart = carts.get(0); + + // Xóa các cart cũ (nếu có nhiều cart) + for (int i = 1; i < carts.size(); i++) { + Cart oldCart = carts.get(i); + cartItemRepository.deleteAllByCart(oldCart); + cartRepository.delete(oldCart); + } + } + + List items = (cart.getItems() == null ? List.of() : cart.getItems()) + .stream() + .map(this::mapToCartItemResponse) + .collect(Collectors.toList()); + + BigDecimal totalAmount = items.stream() + .map(CartItemResponse::getSubtotal) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + Integer totalItems = items.stream() + .mapToInt(CartItemResponse::getQuantity) + .sum(); + + return CartResponse.builder() + .cartId(cart.getCartId()) + .userId(user.getUserId()) + .items(items) + .totalAmount(totalAmount) + .totalItems(totalItems) + .createdAt(cart.getCreatedAt()) + .build(); + } + + @Transactional + public CartResponse updateCartItem(Long userId, Long cartItemId, UpdateCartItemRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + CartItem cartItem = cartItemRepository.findById(cartItemId) + .orElseThrow(() -> new RuntimeException("Cart item not found")); + + // Verify cart belongs to user + if (!cartItem.getCart().getUser().getUserId().equals(user.getUserId())) { + throw new RuntimeException("Cart item does not belong to user"); + } + + cartItem.setQuantity(request.getQuantity()); + cartItemRepository.save(cartItem); + + return getCartByUserId(userId); + } + + @Transactional + public CartResponse removeCartItem(Long userId, Long cartItemId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + CartItem cartItem = cartItemRepository.findById(cartItemId) + .orElseThrow(() -> new RuntimeException("Cart item not found")); + + // Verify cart belongs to user + if (!cartItem.getCart().getUser().getUserId().equals(user.getUserId())) { + throw new RuntimeException("Cart item does not belong to user"); + } + + // Xóa item + cartItemRepository.delete(cartItem); + cartItemRepository.flush(); // Đảm bảo xóa ngay lập tức + + System.out.println("=== REMOVED CART ITEM: " + cartItemId + " ==="); + + // Trả về cart đã cập nhật + CartResponse response = getCartByUserId(userId); + System.out.println("=== CART AFTER REMOVE - Total items: " + response.getTotalItems() + " ==="); + return response; + } + + @Transactional + public void clearCart(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + Cart cart = cartRepository.findByUser(user) + .orElseThrow(() -> new RuntimeException("Cart not found")); + + cartItemRepository.deleteAllByCart(cart); + } + + private CartItemResponse mapToCartItemResponse(CartItem cartItem) { + BigDecimal subtotal = cartItem.getPrice() + .multiply(BigDecimal.valueOf(cartItem.getQuantity())); + + return CartItemResponse.builder() + .cartItemId(cartItem.getCartItemId()) + .productId(cartItem.getProduct().getProductId()) + .productName(cartItem.getProduct().getProductName()) + .productImage(cartItem.getProduct().getImageUrl()) + .quantity(cartItem.getQuantity()) + .price(cartItem.getPrice()) + .subtotal(subtotal) + .build(); + } +} \ No newline at end of file