This commit is contained in:
2025-11-14 14:43:02 +07:00
parent 907a3f5d07
commit 6631730b28
11 changed files with 402 additions and 3 deletions

View File

@@ -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<CartResponse> 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<CartResponse> 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<CartResponse> 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<CartResponse> 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<Void> clearCart(@PathVariable Long userId) {
cartService.clearCart(userId);
return ResponseEntity.noContent().build();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -9,6 +9,7 @@ import lombok.Setter;
@Setter
@AllArgsConstructor
public class AuthResponse {
private Long userId;
private String token;
private String email;
private Role role;

View File

@@ -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
}

View File

@@ -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<CartItemResponse> 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;
}

View File

@@ -12,7 +12,7 @@ import java.math.BigDecimal;
@NoArgsConstructor
@AllArgsConstructor
@Builder
class CartItem {
public class CartItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long cartItemId;

View File

@@ -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<CartItem, Long> {
@Query("SELECT ci FROM CartItem ci WHERE ci.cart = :cart AND ci.product = :product")
List<CartItem> 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<CartItem> findByCartAndProduct(@Param("cart") Cart cart, @Param("product") Product product);
void deleteAllByCart(Cart cart);
}

View File

@@ -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<Cart, Long> {
// 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<Cart> 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<Cart> findByUser(@Param("user") User user);
}

View File

@@ -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) {

View File

@@ -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<CartItem> 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<Cart> 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<CartItemResponse> items = (cart.getItems() == null ? List.<CartItem>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();
}
}