diff --git a/src/main/java/com/example/project_it207_server/controller/WishlistController.java b/src/main/java/com/example/project_it207_server/controller/WishlistController.java new file mode 100644 index 0000000..a10ea1c --- /dev/null +++ b/src/main/java/com/example/project_it207_server/controller/WishlistController.java @@ -0,0 +1,132 @@ +package com.example.project_it207_server.controller; + +import com.example.project_it207_server.model.dto.request.AddWishlistRequest; +import com.example.project_it207_server.model.dto.response.WishlistPaginatedResponse; +import com.example.project_it207_server.model.dto.response.WishlistResponse; +import com.example.project_it207_server.service.AuthService; +import com.example.project_it207_server.service.WishlistService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/wishlists") +@RequiredArgsConstructor +public class WishlistController { + + private final WishlistService wishlistService; + private final AuthService authService; + + @PostMapping("/add") + @PreAuthorize("isAuthenticated()") + public ResponseEntity addToWishlist( + @RequestBody AddWishlistRequest request, + Authentication authentication) { + try { + // Lấy email từ token + String email = authentication.getName(); + + // Lấy userId từ email + Long userId = authService.getUserIdByEmail(email); + WishlistResponse response = wishlistService.addToWishlist(userId, request); + + Map result = new HashMap<>(); + result.put("success", true); + result.put("message", "Thêm vào danh sách ưu thích thành công"); + result.put("data", response); + + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); + } + } + + @DeleteMapping("/remove/{productId}") + @PreAuthorize("isAuthenticated()") + public ResponseEntity removeFromWishlist( + @PathVariable Long productId, + Authentication authentication) { + try { + // Lấy email từ token + String email = authentication.getName(); + + // Lấy userId từ email + Long userId = authService.getUserIdByEmail(email); + wishlistService.removeFromWishlist(userId, productId); + + Map result = new HashMap<>(); + result.put("success", true); + result.put("message", "Xóa khỏi danh sách ưu thích thành công"); + + return ResponseEntity.ok(result); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); + } + } + + @GetMapping + @PreAuthorize("isAuthenticated()") + public ResponseEntity getWishlist( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "createdAt") String sortBy, + Authentication authentication) { + try { + // Lấy email từ token + String email = authentication.getName(); + + // Lấy userId từ email + Long userId = authService.getUserIdByEmail(email); + WishlistPaginatedResponse response = wishlistService.getWishlist(userId, page, size, sortBy); + + Map result = new HashMap<>(); + result.put("success", true); + result.put("message", "Lấy danh sách ưu thích thành công"); + result.put("data", response); + + return ResponseEntity.ok(result); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + @GetMapping("/count") + @PreAuthorize("isAuthenticated()") + public ResponseEntity getWishlistCount(Authentication authentication) { + try { + // Lấy email từ token + String email = authentication.getName(); + + // Lấy userId từ email + Long userId = authService.getUserIdByEmail(email); + long count = wishlistService.getWishlistCount(userId); + + Map result = new HashMap<>(); + result.put("success", true); + result.put("message", "Lấy số lượng danh sách ưu thích thành công"); + result.put("data", count); + + return ResponseEntity.ok(result); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/model/dto/request/AddWishlistRequest.java b/src/main/java/com/example/project_it207_server/model/dto/request/AddWishlistRequest.java new file mode 100644 index 0000000..7a50b8f --- /dev/null +++ b/src/main/java/com/example/project_it207_server/model/dto/request/AddWishlistRequest.java @@ -0,0 +1,14 @@ +package com.example.project_it207_server.model.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AddWishlistRequest { + private Long productId; +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/model/dto/response/WishlistPaginatedResponse.java b/src/main/java/com/example/project_it207_server/model/dto/response/WishlistPaginatedResponse.java new file mode 100644 index 0000000..812f9e4 --- /dev/null +++ b/src/main/java/com/example/project_it207_server/model/dto/response/WishlistPaginatedResponse.java @@ -0,0 +1,18 @@ +package com.example.project_it207_server.model.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WishlistPaginatedResponse { + private java.util.List wishlists; + private int totalPages; + private long totalElements; + private int currentPage; + private int pageSize; +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/model/dto/response/WishlistResponse.java b/src/main/java/com/example/project_it207_server/model/dto/response/WishlistResponse.java new file mode 100644 index 0000000..07604a9 --- /dev/null +++ b/src/main/java/com/example/project_it207_server/model/dto/response/WishlistResponse.java @@ -0,0 +1,38 @@ +package com.example.project_it207_server.model.dto.response; + +import com.example.project_it207_server.model.entity.Product; +import com.example.project_it207_server.model.entity.Wishlist; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class WishlistResponse { + private Long wishlistId; + private Long productId; + private String productName; + private String productImage; + private BigDecimal price; + private String description; + private LocalDateTime addedAt; + + public static WishlistResponse fromEntity(Wishlist wishlist) { + Product product = wishlist.getProduct(); + return WishlistResponse.builder() + .wishlistId(wishlist.getWishlistId()) + .productId(product.getProductId()) + .productName(product.getProductName()) + .productImage(product.getImageUrl()) + .price(product.getPrice()) + .description(product.getDescription()) + .addedAt(wishlist.getCreatedAt()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/model/entity/Wishlist.java b/src/main/java/com/example/project_it207_server/model/entity/Wishlist.java new file mode 100644 index 0000000..16b047c --- /dev/null +++ b/src/main/java/com/example/project_it207_server/model/entity/Wishlist.java @@ -0,0 +1,40 @@ +package com.example.project_it207_server.model.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + +@Entity +@Table(name = "wishlist", uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "product_id"}) +}) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Wishlist { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long wishlistId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", nullable = false) + private Product product; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/repository/WishlistRepository.java b/src/main/java/com/example/project_it207_server/repository/WishlistRepository.java new file mode 100644 index 0000000..b7cfe04 --- /dev/null +++ b/src/main/java/com/example/project_it207_server/repository/WishlistRepository.java @@ -0,0 +1,31 @@ +package com.example.project_it207_server.repository; + +import com.example.project_it207_server.model.entity.Wishlist; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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; + +@Repository +public interface WishlistRepository extends JpaRepository { + + @Query("SELECT w FROM Wishlist w WHERE w.user.userId = :userId ORDER BY w.createdAt DESC") + Page findByUserId(@Param("userId") Long userId, Pageable pageable); + + @Query("SELECT w FROM Wishlist w WHERE w.user.userId = :userId AND w.product.productId = :productId") + Optional findByUserIdAndProductId(@Param("userId") Long userId, @Param("productId") Long productId); + + @Query("SELECT CASE WHEN COUNT(w) > 0 THEN true ELSE false END " + + "FROM Wishlist w WHERE w.user.userId = :userId AND w.product.productId = :productId") + boolean existsByUserIdAndProductId(@Param("userId") Long userId, @Param("productId") Long productId); + + @Query("DELETE FROM Wishlist w WHERE w.user.userId = :userId AND w.product.productId = :productId") + void deleteByUserIdAndProductId(@Param("userId") Long userId, @Param("productId") Long productId); + + @Query("SELECT COUNT(w) FROM Wishlist w WHERE w.user.userId = :userId") + long countByUserId(@Param("userId") Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/example/project_it207_server/service/WishlistService.java b/src/main/java/com/example/project_it207_server/service/WishlistService.java new file mode 100644 index 0000000..1f5febd --- /dev/null +++ b/src/main/java/com/example/project_it207_server/service/WishlistService.java @@ -0,0 +1,89 @@ +package com.example.project_it207_server.service; + +import com.example.project_it207_server.model.dto.request.AddWishlistRequest; +import com.example.project_it207_server.model.dto.response.WishlistPaginatedResponse; +import com.example.project_it207_server.model.dto.response.WishlistResponse; +import com.example.project_it207_server.model.entity.Product; +import com.example.project_it207_server.model.entity.User; +import com.example.project_it207_server.model.entity.Wishlist; +import com.example.project_it207_server.repository.ProductRepository; +import com.example.project_it207_server.repository.UserRepository; +import com.example.project_it207_server.repository.WishlistRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class WishlistService { + + private final WishlistRepository wishlistRepository; + private final UserRepository userRepository; + private final ProductRepository productRepository; + + @Transactional + public WishlistResponse addToWishlist(Long userId, AddWishlistRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("Người dùng không tồn tại")); + + Product product = productRepository.findById(request.getProductId()) + .orElseThrow(() -> new RuntimeException("Sản phẩm không tồn tại")); + + boolean exists = wishlistRepository.existsByUserIdAndProductId(userId, request.getProductId()); + if (exists) { + throw new RuntimeException("Sản phẩm đã có trong danh sách ưu thích"); + } + + Wishlist wishlist = Wishlist.builder() + .user(user) + .product(product) + .build(); + + Wishlist saved = wishlistRepository.save(wishlist); + return WishlistResponse.fromEntity(saved); + } + + @Transactional + public void removeFromWishlist(Long userId, Long productId) { + Wishlist wishlist = wishlistRepository.findByUserIdAndProductId(userId, productId) + .orElseThrow(() -> new RuntimeException("Sản phẩm không trong danh sách ưu thích")); + + wishlistRepository.delete(wishlist); + } + + @Transactional(readOnly = true) + public WishlistPaginatedResponse getWishlist(Long userId, int page, int size, String sortBy) { + Sort.Direction direction = Sort.Direction.DESC; + if ("name".equalsIgnoreCase(sortBy)) { + sortBy = "product.name"; + } else if ("price".equalsIgnoreCase(sortBy)) { + sortBy = "product.price"; + } else { + sortBy = "createdAt"; + } + + Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy)); + Page wishlistPage = wishlistRepository.findByUserId(userId, pageable); + + return WishlistPaginatedResponse.builder() + .wishlists(wishlistPage.getContent().stream() + .map(WishlistResponse::fromEntity) + .collect(Collectors.toList())) + .totalPages(wishlistPage.getTotalPages()) + .totalElements(wishlistPage.getTotalElements()) + .currentPage(page) + .pageSize(size) + .build(); + } + + @Transactional(readOnly = true) + public long getWishlistCount(Long userId) { + return wishlistRepository.countByUserId(userId); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0385e92..b86edf3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,11 +7,16 @@ spring.datasource.username=root spring.datasource.password= spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +# JPA & Hibernate Configuration spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.properties.hibernate.format_sql=true +# Redis Configuration spring.redis.host=localhost spring.redis.port=6379 +# JWT Configuration jwt.secret-key=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970 jwt.expiration=86400000 \ No newline at end of file