This commit is contained in:
2025-11-18 16:19:16 +07:00
parent 6631730b28
commit 7395c32e26
16 changed files with 573 additions and 56 deletions

View File

@@ -0,0 +1,200 @@
package com.example.project_it207_server.controller;
import com.example.project_it207_server.model.dto.request.CreateOrderRequest;
import com.example.project_it207_server.model.dto.response.OrderProgressResponse;
import com.example.project_it207_server.model.dto.response.OrderResponse;
import com.example.project_it207_server.service.AuthService;
import com.example.project_it207_server.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final AuthService authService;
/**
* Tạo đơn hàng mới
* POST /api/orders
*/
@PostMapping("/{userId}")
public ResponseEntity<Map<String, Object>> createOrder(
@PathVariable Long userId,
@RequestBody CreateOrderRequest request) {
try {
OrderResponse order = orderService.createOrder(userId, request);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "Order created successfully");
response.put("data", order);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* Lấy danh sách tất cả đơn hàng
* GET /api/orders
*/
@GetMapping
public ResponseEntity<Map<String, Object>> getAllOrders(Authentication authentication) {
try {
// Lấy email từ token
String email = authentication.getName();
// Lấy userId từ email
Long userId = authService.getUserIdByEmail(email);
List<OrderResponse> orders = orderService.getAllOrders(userId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", orders);
response.put("total", orders.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* Lọc đơn hàng theo trạng thái
* GET /api/orders/filter?status={status}
* Status: PENDING, CONFIRMED, SHIPPING, DELIVERED, CANCELLED
*/
@GetMapping("/filter")
public ResponseEntity<Map<String, Object>> getOrdersByStatus(
@RequestParam String status,
Authentication authentication) {
try {
// Lấy email từ token
String email = authentication.getName();
// Lấy userId từ email
Long userId = authService.getUserIdByEmail(email);
List<OrderResponse> orders = orderService.getOrdersByStatus(userId, status.toUpperCase());
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", orders);
response.put("status", status);
response.put("total", orders.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* Xem chi tiết đơn hàng
* GET /api/orders/{orderId}
*/
@GetMapping("/{orderId}")
public ResponseEntity<Map<String, Object>> getOrderDetail(
@PathVariable Long orderId,
Authentication authentication) {
try {
// Lấy email từ token
String email = authentication.getName();
// Lấy userId từ email
Long userId = authService.getUserIdByEmail(email);
OrderResponse order = orderService.getOrderDetail(orderId, userId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", order);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* Xem tiến độ đơn hàng
* GET /api/orders/{orderId}/progress
*/
@GetMapping("/{orderId}/progress")
public ResponseEntity<Map<String, Object>> getOrderProgress(
@PathVariable Long orderId,
Authentication authentication) {
try {
// Lấy email từ token
String email = authentication.getName();
// Lấy userId từ email
Long userId = authService.getUserIdByEmail(email);
OrderProgressResponse progress = orderService.getOrderProgress(orderId, userId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", progress);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* Hủy đơn hàng
* PUT /api/orders/{orderId}/cancel
*/
@PutMapping("/{orderId}/cancel")
public ResponseEntity<Map<String, Object>> cancelOrder(
@PathVariable Long orderId,
Authentication authentication) {
try {
// Lấy email từ token
String email = authentication.getName();
// Lấy userId từ email
Long userId = authService.getUserIdByEmail(email);
OrderResponse order = orderService.cancelOrder(orderId, userId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "Order cancelled successfully");
response.put("data", order);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
}

View File

@@ -0,0 +1,10 @@
package com.example.project_it207_server.model.dto.request;
import lombok.*;
import java.util.List;
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class CreateOrderRequest {
private String shippingAddress;
private List<OrderItemRequest> items;
}

View File

@@ -0,0 +1,9 @@
package com.example.project_it207_server.model.dto.request;
import lombok.*;
@Getter @Setter @NoArgsConstructor @AllArgsConstructor
public class OrderItemRequest {
private Long productId;
private int quantity;
}

View File

@@ -10,10 +10,22 @@ import java.math.BigDecimal;
@Builder @Builder
public class CartItemResponse { public class CartItemResponse {
private Long cartItemId; private Long cartItemId;
private Integer quantity;
// Gộp lại thành object product để UI dùng item.product.xxx
private ProductInCart product;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class ProductInCart {
private Long productId; private Long productId;
private String productName; private String productName;
private String productImage; private String imageUrl;
private Integer quantity;
private BigDecimal price; private BigDecimal price;
private BigDecimal subtotal; // price * quantity }
private BigDecimal subtotal;
} }

View File

@@ -0,0 +1,14 @@
package com.example.project_it207_server.model.dto.response;
import lombok.*;
import java.math.BigDecimal;
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class OrderItemResponse {
private Long orderItemId;
private Long productId;
private String productName;
private int quantity;
private BigDecimal price;
private BigDecimal subtotal;
}

View File

@@ -0,0 +1,11 @@
package com.example.project_it207_server.model.dto.response;
import lombok.*;
import java.util.List;
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class OrderProgressResponse {
private Long orderId;
private String orderStatus;
private List<StatusTimeline> timeline;
}

View File

@@ -0,0 +1,19 @@
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 OrderResponse {
private Long orderId;
private Long userId;
private String userName;
private BigDecimal totalPrice;
private String orderStatus;
private String shippingAddress;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private List<OrderItemResponse> orderItems;
}

View File

@@ -0,0 +1,11 @@
package com.example.project_it207_server.model.dto.response;
import lombok.*;
import java.time.LocalDateTime;
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class StatusTimeline {
private String status;
private LocalDateTime timestamp;
private boolean completed;
}

View File

@@ -21,9 +21,24 @@ public class Order {
private BigDecimal totalPrice; private BigDecimal totalPrice;
private String orderStatus = "PENDING"; private String orderStatus = "PENDING";
private String shippingAddress; private String shippingAddress;
private LocalDateTime createdAt = LocalDateTime.now();
private LocalDateTime updatedAt = LocalDateTime.now();
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL) @Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems; private List<OrderItem> orderItems;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
} }

View File

@@ -11,16 +11,16 @@ import java.math.BigDecimal;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
class OrderItem { public class OrderItem {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderItemId; private Long orderItemId;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id") @JoinColumn(name = "order_id")
private Order order; private Order order;
@ManyToOne @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id") @JoinColumn(name = "product_id")
private Product product; private Product product;

View File

@@ -21,4 +21,8 @@ public interface CartItemRepository extends JpaRepository<CartItem, Long> {
Optional<CartItem> findByCartAndProduct(@Param("cart") Cart cart, @Param("product") Product product); Optional<CartItem> findByCartAndProduct(@Param("cart") Cart cart, @Param("product") Product product);
void deleteAllByCart(Cart cart); void deleteAllByCart(Cart cart);
List<CartItem> findByCart_CartId(Long cartId);
void deleteByCart_CartId(Long cartId);
} }

View File

@@ -1,4 +1,3 @@
// CartRepository.java
package com.example.project_it207_server.repository; package com.example.project_it207_server.repository;
import com.example.project_it207_server.model.entity.Cart; import com.example.project_it207_server.model.entity.Cart;
@@ -14,11 +13,19 @@ import java.util.List;
@Repository @Repository
public interface CartRepository extends JpaRepository<Cart, Long> { public interface CartRepository extends JpaRepository<Cart, Long> {
// Thay đổi: Sử dụng List thay vì Optional để tránh lỗi // Lấy tất cả giỏ hàng của user (có thể có nhiều)
@Query("SELECT c FROM Cart c WHERE c.user = :user ORDER BY c.createdAt DESC") @Query("SELECT c FROM Cart c WHERE c.user = :user ORDER BY c.createdAt DESC")
List<Cart> findAllByUser(@Param("user") User user); List<Cart> findAllByUser(@Param("user") User user);
// Hoặc giữ nguyên nhưng thêm LIMIT 1 // Lấy giỏ hàng mới nhất của user
@Query("SELECT c FROM Cart c WHERE c.user = :user ORDER BY c.createdAt DESC 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); Optional<Cart> findByUser(@Param("user") User user);
// Tìm giỏ hàng theo userId
@Query("SELECT c FROM Cart c WHERE c.user.userId = :userId ORDER BY c.createdAt DESC LIMIT 1")
Optional<Cart> findByUserId(@Param("userId") Long userId);
// Kiểm tra user có giỏ hàng không
@Query("SELECT COUNT(c) > 0 FROM Cart c WHERE c.user.userId = :userId")
boolean existsByUserId(@Param("userId") Long userId);
} }

View File

@@ -2,5 +2,22 @@ package com.example.project_it207_server.repository;
import com.example.project_it207_server.model.entity.Order; import com.example.project_it207_server.model.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
public interface OrderRepository extends JpaRepository<Order, Long> {} import java.util.List;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
// Lấy tất cả đơn hàng của user, sắp xếp theo ngày tạo giảm dần
List<Order> findByUser_UserIdOrderByCreatedAtDesc(Long userId);
// Lọc đơn hàng theo trạng thái
List<Order> findByUser_UserIdAndOrderStatusOrderByCreatedAtDesc(Long userId, String orderStatus);
// Đếm số đơn hàng theo trạng thái
Long countByUser_UserIdAndOrderStatus(Long userId, String orderStatus);
// Kiểm tra đơn hàng có tồn tại không
boolean existsByOrderIdAndUser_UserId(Long orderId, Long userId);
}

View File

@@ -76,4 +76,10 @@ public class AuthService {
String redisKey = "token:" + username + ":" + token; String redisKey = "token:" + username + ":" + token;
redisService.deleteKey(redisKey); redisService.deleteKey(redisKey);
} }
public Long getUserIdByEmail(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("User not found"));
return user.getUserId();
}
} }

View File

@@ -77,52 +77,37 @@ public class CartService {
@Transactional @Transactional
public CartResponse getCartByUserId(Long userId) { public CartResponse getCartByUserId(Long userId) {
User user = userRepository.findById(userId) Cart cart = cartRepository.findByUserId(userId)
.orElseThrow(() -> new RuntimeException("User not found")); .orElseThrow(() -> new RuntimeException("Cart not found"));
// FIX: Xử lý trường hợp có nhiều cart List<CartItemResponse> items = cart.getItems().stream()
List<Cart> carts = cartRepository.findAllByUser(user); .map(item -> CartItemResponse.builder()
.cartItemId(item.getCartItemId())
Cart cart; .quantity(item.getQuantity())
if (carts.isEmpty()) { .product(
// Tạo cart mới nếu chưa có CartItemResponse.ProductInCart.builder()
cart = Cart.builder() .productId(item.getProduct().getProductId())
.user(user) .productName(item.getProduct().getProductName())
.items(new ArrayList<>()) .imageUrl(item.getProduct().getImageUrl())
.createdAt(LocalDateTime.now()) .price(item.getProduct().getPrice())
.build(); .build()
cart = cartRepository.save(cart); )
} else { .subtotal(item.getProduct().getPrice()
// Lấy cart mới nhất .multiply(BigDecimal.valueOf(item.getQuantity())))
cart = carts.get(0); .build()
)
// Xóa các cart cũ (nếu có nhiều cart) .toList();
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() BigDecimal totalAmount = items.stream()
.map(CartItemResponse::getSubtotal) .map(CartItemResponse::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
Integer totalItems = items.stream()
.mapToInt(CartItemResponse::getQuantity)
.sum();
return CartResponse.builder() return CartResponse.builder()
.cartId(cart.getCartId()) .cartId(cart.getCartId())
.userId(user.getUserId()) .userId(cart.getUser().getUserId())
.items(items) .items(items)
.totalItems(items.size())
.totalAmount(totalAmount) .totalAmount(totalAmount)
.totalItems(totalItems)
.createdAt(cart.getCreatedAt()) .createdAt(cart.getCreatedAt())
.build(); .build();
} }
@@ -188,11 +173,15 @@ public class CartService {
return CartItemResponse.builder() return CartItemResponse.builder()
.cartItemId(cartItem.getCartItemId()) .cartItemId(cartItem.getCartItemId())
.quantity(cartItem.getQuantity())
.product(
CartItemResponse.ProductInCart.builder()
.productId(cartItem.getProduct().getProductId()) .productId(cartItem.getProduct().getProductId())
.productName(cartItem.getProduct().getProductName()) .productName(cartItem.getProduct().getProductName())
.productImage(cartItem.getProduct().getImageUrl()) .imageUrl(cartItem.getProduct().getImageUrl())
.quantity(cartItem.getQuantity()) .price(cartItem.getProduct().getPrice())
.price(cartItem.getPrice()) .build()
)
.subtotal(subtotal) .subtotal(subtotal)
.build(); .build();
} }

View File

@@ -0,0 +1,193 @@
package com.example.project_it207_server.service;
import com.example.project_it207_server.model.dto.request.CreateOrderRequest;
import com.example.project_it207_server.model.dto.response.*;
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.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final UserRepository userRepository;
private final CartRepository cartRepository;
private final CartItemRepository cartItemRepository;
@Transactional
public OrderResponse createOrder(Long userId, CreateOrderRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
Order order = Order.builder()
.user(user)
.shippingAddress(request.getShippingAddress())
.orderStatus("PENDING")
.totalPrice(BigDecimal.ZERO)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
List<OrderItem> orderItems = new ArrayList<>();
BigDecimal totalPrice = BigDecimal.ZERO;
for (var itemReq : request.getItems()) {
Product product = productRepository.findById(itemReq.getProductId())
.orElseThrow(() -> new RuntimeException("Product not found: " + itemReq.getProductId()));
BigDecimal itemPrice = product.getPrice().multiply(new BigDecimal(itemReq.getQuantity()));
totalPrice = totalPrice.add(itemPrice);
OrderItem orderItem = OrderItem.builder()
.order(order)
.product(product)
.quantity(itemReq.getQuantity())
.price(product.getPrice())
.build();
orderItems.add(orderItem);
}
order.setTotalPrice(totalPrice);
order.setOrderItems(orderItems);
Order savedOrder = orderRepository.save(order);
// Clear cart after creating order
Optional<Cart> cartOpt = cartRepository.findByUser(user);
if (cartOpt.isPresent()) {
Cart cart = cartOpt.get();
if (cart.getItems() != null && !cart.getItems().isEmpty()) {
cartItemRepository.deleteAll(cart.getItems());
}
}
return mapToOrderResponse(savedOrder);
}
public List<OrderResponse> getAllOrders(Long userId) {
List<Order> orders = orderRepository.findByUser_UserIdOrderByCreatedAtDesc(userId);
return orders.stream()
.map(this::mapToOrderResponse)
.collect(Collectors.toList());
}
public List<OrderResponse> getOrdersByStatus(Long userId, String status) {
List<Order> orders = orderRepository.findByUser_UserIdAndOrderStatusOrderByCreatedAtDesc(userId, status);
return orders.stream()
.map(this::mapToOrderResponse)
.collect(Collectors.toList());
}
public OrderResponse getOrderDetail(Long orderId, Long userId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found"));
if (!order.getUser().getUserId().equals(userId)) {
throw new RuntimeException("Unauthorized access to order");
}
return mapToOrderResponse(order);
}
public OrderProgressResponse getOrderProgress(Long orderId, Long userId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found"));
if (!order.getUser().getUserId().equals(userId)) {
throw new RuntimeException("Unauthorized access to order");
}
List<StatusTimeline> timeline = Arrays.asList(
StatusTimeline.builder()
.status("PENDING")
.timestamp(order.getCreatedAt())
.completed(true)
.build(),
StatusTimeline.builder()
.status("CONFIRMED")
.timestamp(order.getOrderStatus().equals("CONFIRMED") ||
order.getOrderStatus().equals("SHIPPING") ||
order.getOrderStatus().equals("DELIVERED") ?
order.getUpdatedAt() : null)
.completed(order.getOrderStatus().equals("CONFIRMED") ||
order.getOrderStatus().equals("SHIPPING") ||
order.getOrderStatus().equals("DELIVERED"))
.build(),
StatusTimeline.builder()
.status("SHIPPING")
.timestamp(order.getOrderStatus().equals("SHIPPING") ||
order.getOrderStatus().equals("DELIVERED") ?
order.getUpdatedAt() : null)
.completed(order.getOrderStatus().equals("SHIPPING") ||
order.getOrderStatus().equals("DELIVERED"))
.build(),
StatusTimeline.builder()
.status("DELIVERED")
.timestamp(order.getOrderStatus().equals("DELIVERED") ?
order.getUpdatedAt() : null)
.completed(order.getOrderStatus().equals("DELIVERED"))
.build()
);
return OrderProgressResponse.builder()
.orderId(order.getOrderId())
.orderStatus(order.getOrderStatus())
.timeline(timeline)
.build();
}
@Transactional
public OrderResponse cancelOrder(Long orderId, Long userId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found"));
if (!order.getUser().getUserId().equals(userId)) {
throw new RuntimeException("Unauthorized access to order");
}
if (order.getOrderStatus().equals("DELIVERED") || order.getOrderStatus().equals("CANCELLED")) {
throw new RuntimeException("Cannot cancel order with status: " + order.getOrderStatus());
}
order.setOrderStatus("CANCELLED");
order.setUpdatedAt(LocalDateTime.now());
Order updatedOrder = orderRepository.save(order);
return mapToOrderResponse(updatedOrder);
}
private OrderResponse mapToOrderResponse(Order order) {
List<OrderItemResponse> itemResponses = order.getOrderItems().stream()
.map(item -> OrderItemResponse.builder()
.orderItemId(item.getOrderItemId())
.productId(item.getProduct().getProductId())
.productName(item.getProduct().getProductName())
.quantity(item.getQuantity())
.price(item.getPrice())
.subtotal(item.getPrice().multiply(new BigDecimal(item.getQuantity())))
.build())
.collect(Collectors.toList());
return OrderResponse.builder()
.orderId(order.getOrderId())
.userId(order.getUser().getUserId())
.userName(order.getUser().getLastName())
.totalPrice(order.getTotalPrice())
.orderStatus(order.getOrderStatus())
.shippingAddress(order.getShippingAddress())
.createdAt(order.getCreatedAt())
.updatedAt(order.getUpdatedAt())
.orderItems(itemResponses)
.build();
}
}