products
This commit is contained in:
@@ -35,8 +35,7 @@ public class SecurityConfig {
|
|||||||
"/api/auth/confirm-password-change",
|
"/api/auth/confirm-password-change",
|
||||||
"/public/**"
|
"/public/**"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
// ✅ Tất cả API khác yêu cầu xác thực
|
.anyRequest().permitAll()
|
||||||
.anyRequest().authenticated()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ Thêm JWT filter trước UsernamePasswordAuthenticationFilter
|
// ✅ Thêm JWT filter trước UsernamePasswordAuthenticationFilter
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package com.example.project_it207_server.controller;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.model.dto.response.ProductResponse;
|
||||||
|
import com.example.project_it207_server.service.ProductService;
|
||||||
|
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.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/products")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class ProductController {
|
||||||
|
|
||||||
|
private final ProductService productService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/products
|
||||||
|
* Lấy danh sách tất cả sản phẩm với phân trang
|
||||||
|
*
|
||||||
|
* @param page số trang (mặc định: 0)
|
||||||
|
* @param size số lượng sản phẩm mỗi trang (mặc định: 10)
|
||||||
|
* @param sortBy trường để sắp xếp (mặc định: id)
|
||||||
|
* @param sortDir hướng sắp xếp (asc/desc, mặc định: asc)
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<Map<String, Object>> getAllProducts(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
@RequestParam(defaultValue = "id") String sortBy,
|
||||||
|
@RequestParam(defaultValue = "asc") String sortDir
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
Sort sort = sortDir.equalsIgnoreCase("desc")
|
||||||
|
? Sort.by(sortBy).descending()
|
||||||
|
: Sort.by(sortBy).ascending();
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(page, size, sort);
|
||||||
|
Page<ProductResponse> productPage = productService.getAllProducts(pageable);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("products", productPage.getContent());
|
||||||
|
response.put("currentPage", productPage.getNumber());
|
||||||
|
response.put("totalItems", productPage.getTotalElements());
|
||||||
|
response.put("totalPages", productPage.getTotalPages());
|
||||||
|
response.put("hasNext", productPage.hasNext());
|
||||||
|
response.put("hasPrevious", productPage.hasPrevious());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(Map.of("error", "Failed to fetch products: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/products/{id}
|
||||||
|
* Lấy thông tin chi tiết sản phẩm theo ID
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<?> getProductById(@PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
ProductResponse product = productService.getProductById(id);
|
||||||
|
return ResponseEntity.ok(product);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(Map.of("error", "Failed to fetch product: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/products/search
|
||||||
|
* Tìm kiếm sản phẩm theo tên
|
||||||
|
*/
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ResponseEntity<Map<String, Object>> searchProducts(
|
||||||
|
@RequestParam String keyword,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
@RequestParam(defaultValue = "id") String sortBy,
|
||||||
|
@RequestParam(defaultValue = "asc") String sortDir
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
Sort sort = sortDir.equalsIgnoreCase("desc")
|
||||||
|
? Sort.by(sortBy).descending()
|
||||||
|
: Sort.by(sortBy).ascending();
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(page, size, sort);
|
||||||
|
Page<ProductResponse> productPage = productService.searchProducts(keyword, pageable);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("products", productPage.getContent());
|
||||||
|
response.put("currentPage", productPage.getNumber());
|
||||||
|
response.put("totalItems", productPage.getTotalElements());
|
||||||
|
response.put("totalPages", productPage.getTotalPages());
|
||||||
|
response.put("keyword", keyword);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(Map.of("error", "Failed to search products: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/products/available
|
||||||
|
* Lấy danh sách sản phẩm còn hàng
|
||||||
|
*/
|
||||||
|
@GetMapping("/available")
|
||||||
|
public ResponseEntity<Map<String, Object>> getAvailableProducts(
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "10") int size,
|
||||||
|
@RequestParam(defaultValue = "productId") String sortBy,
|
||||||
|
@RequestParam(defaultValue = "asc") String sortDir
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
Sort sort = sortDir.equalsIgnoreCase("desc")
|
||||||
|
? Sort.by(sortBy).descending()
|
||||||
|
: Sort.by(sortBy).ascending();
|
||||||
|
|
||||||
|
Pageable pageable = PageRequest.of(page, size, sort);
|
||||||
|
Page<ProductResponse> productPage = productService.getAvailableProducts(pageable);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("products", productPage.getContent());
|
||||||
|
response.put("currentPage", productPage.getNumber());
|
||||||
|
response.put("totalItems", productPage.getTotalElements());
|
||||||
|
response.put("totalPages", productPage.getTotalPages());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(Map.of("error", "Failed to fetch available products: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.project_it207_server.model.dto.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ProductResponse {
|
||||||
|
private Long productId;
|
||||||
|
private String productName;
|
||||||
|
private String description;
|
||||||
|
private BigDecimal price;
|
||||||
|
private Integer stockQuantity;
|
||||||
|
private String imageUrl;
|
||||||
|
private Long categoryId;
|
||||||
|
private String categoryName;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
package com.example.project_it207_server.redis;
|
package com.example.project_it207_server.redis;
|
||||||
|
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||||
|
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class RedisConfig {
|
public class RedisConfig {
|
||||||
|
|
||||||
@@ -26,4 +32,31 @@ public class RedisConfig {
|
|||||||
template.afterPropertiesSet();
|
template.afterPropertiesSet();
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
|
||||||
|
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
||||||
|
.entryTtl(Duration.ofMinutes(30)) // Cache 30 phút
|
||||||
|
.serializeKeysWith(
|
||||||
|
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
|
||||||
|
)
|
||||||
|
.serializeValuesWith(
|
||||||
|
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
|
||||||
|
)
|
||||||
|
.disableCachingNullValues();
|
||||||
|
|
||||||
|
return RedisCacheManager.builder(connectionFactory)
|
||||||
|
.cacheDefaults(config)
|
||||||
|
.withCacheConfiguration("products",
|
||||||
|
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(15)))
|
||||||
|
.withCacheConfiguration("product",
|
||||||
|
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)))
|
||||||
|
.withCacheConfiguration("productsByCategory",
|
||||||
|
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(20)))
|
||||||
|
.withCacheConfiguration("productsSearch",
|
||||||
|
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)))
|
||||||
|
.withCacheConfiguration("availableProducts",
|
||||||
|
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(15)))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,42 @@
|
|||||||
package com.example.project_it207_server.repository;
|
package com.example.project_it207_server.repository;
|
||||||
|
|
||||||
import com.example.project_it207_server.model.entity.Product;
|
import com.example.project_it207_server.model.entity.Product;
|
||||||
|
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.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Repository
|
||||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||||
|
|
||||||
|
// Tìm sản phẩm theo tên (tìm kiếm không phân biệt hoa thường)
|
||||||
|
Page<Product> findByProductNameContainingIgnoreCase(String productName, Pageable pageable);
|
||||||
|
|
||||||
|
// Lấy sản phẩm theo khoảng giá
|
||||||
|
@Query("SELECT p FROM Product p WHERE p.price BETWEEN :minPrice AND :maxPrice")
|
||||||
|
Page<Product> findByPriceRange(@Param("minPrice") BigDecimal minPrice,
|
||||||
|
@Param("maxPrice") BigDecimal maxPrice,
|
||||||
|
Pageable pageable);
|
||||||
|
|
||||||
|
// Lấy sản phẩm còn hàng
|
||||||
|
@Query("SELECT p FROM Product p WHERE p.stockQuantity > 0")
|
||||||
|
Page<Product> findAvailableProducts(Pageable pageable);
|
||||||
|
|
||||||
|
// Tìm sản phẩm theo nhiều tiêu chí
|
||||||
|
@Query("SELECT p FROM Product p WHERE " +
|
||||||
|
"(:categoryId IS NULL OR p.category.categoryId = :categoryId) AND " +
|
||||||
|
"(:keyword IS NULL OR LOWER(p.productName) LIKE LOWER(CONCAT('%', :keyword, '%'))) AND " +
|
||||||
|
"(:minPrice IS NULL OR p.price >= :minPrice) AND " +
|
||||||
|
"(:maxPrice IS NULL OR p.price <= :maxPrice)")
|
||||||
|
Page<Product> searchProductsWithFilters(
|
||||||
|
@Param("categoryId") Long categoryId,
|
||||||
|
@Param("keyword") String keyword,
|
||||||
|
@Param("minPrice") BigDecimal minPrice,
|
||||||
|
@Param("maxPrice") BigDecimal maxPrice,
|
||||||
|
Pageable pageable
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.example.project_it207_server.service;
|
||||||
|
|
||||||
|
import com.example.project_it207_server.model.entity.Product;
|
||||||
|
import com.example.project_it207_server.model.dto.response.ProductResponse;
|
||||||
|
import com.example.project_it207_server.repository.ProductRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ProductService {
|
||||||
|
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách tất cả sản phẩm với phân trang
|
||||||
|
*/
|
||||||
|
@Cacheable(value = "products", key = "#pageable.pageNumber + '-' + #pageable.pageSize")
|
||||||
|
public Page<ProductResponse> getAllProducts(Pageable pageable) {
|
||||||
|
return productRepository.findAll(pageable)
|
||||||
|
.map(this::convertToResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy chi tiết sản phẩm theo ID
|
||||||
|
*/
|
||||||
|
@Cacheable(value = "product", key = "#id")
|
||||||
|
public ProductResponse getProductById(Long id) {
|
||||||
|
Product product = productRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
|
||||||
|
return convertToResponse(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tìm kiếm sản phẩm theo tên
|
||||||
|
*/
|
||||||
|
@Cacheable(value = "productsSearch", key = "#keyword + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
|
||||||
|
public Page<ProductResponse> searchProducts(String keyword, Pageable pageable) {
|
||||||
|
return productRepository.findByProductNameContainingIgnoreCase(keyword, pageable)
|
||||||
|
.map(this::convertToResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách sản phẩm còn hàng
|
||||||
|
*/
|
||||||
|
@Cacheable(value = "availableProducts", key = "#pageable.pageNumber + '-' + #pageable.pageSize")
|
||||||
|
public Page<ProductResponse> getAvailableProducts(Pageable pageable) {
|
||||||
|
return productRepository.findAvailableProducts(pageable)
|
||||||
|
.map(this::convertToResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Product entity sang ProductResponse
|
||||||
|
*/
|
||||||
|
private ProductResponse convertToResponse(Product product) {
|
||||||
|
return ProductResponse.builder()
|
||||||
|
.productId(product.getProductId())
|
||||||
|
.productName(product.getProductName())
|
||||||
|
.description(product.getDescription())
|
||||||
|
.price(product.getPrice())
|
||||||
|
.stockQuantity(product.getStockQuantity())
|
||||||
|
.imageUrl(product.getImageUrl())
|
||||||
|
.categoryId(product.getCategory() != null ? product.getCategory().getCategoryId() : null)
|
||||||
|
.categoryName(product.getCategory() != null ? product.getCategory().getCategoryName() : null)
|
||||||
|
.createdAt(product.getCreatedAt())
|
||||||
|
.updatedAt(product.getUpdatedAt())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user