Xử lý nghiệp vụ giỏ hàng-đặt hàng cơ bản trong Spring Boot
Đăng lúc: 08:51 AM - 01/04/2025 bởi Charles Chung - 63Nối tiếp bài trước, sau khi đã login thành công chúng ta tiếp tục tìm hiểu về nghiệp vụ giỏ hàng và đặt hàng trên trang người dùng.

1. Giới thiệu về session
Trong Spring Boot, session (phiên làm việc) là một cơ chế lưu trữ thông tin giữa các yêu cầu HTTP từ cùng một người dùng. Vì HTTP là giao thức không trạng thái (stateless), điều này có nghĩa là mỗi yêu cầu HTTP từ người dùng sẽ không có thông tin về các yêu cầu trước đó. Session giúp giải quyết vấn đề này bằng cách duy trì thông tin người dùng giữa các yêu cầu.
- HttpSesssion: Là một đối tượng cung cấp cơ chế để lưu trữ thông tin phiên của người dùng. Nó là một phần của API Servlet, được Spring Boot sử dụng để lưu trữ các thông tin cần thiết trong suốt một phiên làm việc của người dùng. Ví dụ: Thông tin người dùng đăng nhập, giỏ hàng của người dùng, hoặc các dữ liệu tạm thời cần thiết trong suốt phiên.
- Session Cookie: Mỗi khi người dùng bắt đầu một phiên, server sẽ gửi một cookie (thường là JSESSIONID) về trình duyệt của người dùng để nhận diện phiên làm việc đó trong các yêu cầu tiếp theo.
Spring Boot cung cấp một số cách để làm việc với session, bao gồm sử dụng @SessionAttributes và HttpSession.
hoặc
Cấu hình Session Timeout: Trong Spring Boot, bạn có thể cấu hình thời gian sống của session trong tệp cấu hình application.properties hoặc application.yml.
server.servlet.session.timeout=15m
Để xử lý giỏ hàng chúng ta có thể sử dụng một trong 2 cách sau:
- Lưu trữ giỏ hàng trong database
- Lưu trữ giỏ hàng trong Session
2. Ví dụ demo
Tiếp tục bài Bảo mật trong SpringBoot, chúng ta mở rộng thêm chức năng giỏ hàng và đặt hàng với 2 bảng như hình dưới
Bổ sung mở rộng theo các bước sau
Bước 1: Tạo entities và models
Code cho Enum entities/OrderStatus
package com.bkap.entities; public enum OrderStatus { ORDER_NEW, ORDER_DELIVERING, ORDER_RECEVIED, ORDER_CANCEL }
Code cho lớp entities/order
package com.bkap.entities; import java.util.Date; import java.util.HashSet; import java.util.Set; import org.springframework.format.annotation.DateTimeFormat; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @Entity @Table(name="orders") public class Order { @Id @Column(name="orderid", length = 12) private String orderId; @DateTimeFormat(pattern = "dd/MM/yyyy") @Column(name="orderdate") private Date orderDate=new Date(); @Column(name="receivename", length = 100) private String receiveName; @Column(name="receiveaddress", length = 100) private String receiveAddress; @Column(name="receivephone", length = 100) private String receivePhone; private OrderStatus status; @Column(name="note",length = 1000) private String note; @ManyToOne @JoinColumn(name = "accountid",nullable = false) private Account account=new Account(); @OneToMany(mappedBy = "order") private Set<OrderDetail> details=new HashSet<OrderDetail>(); public String getOrderId() { return orderId; } public Set<OrderDetail> getDetails() { return details; } public void setDetails(Set<OrderDetail> details) { this.details = details; } public void setOrderId(String orderId) { this.orderId = orderId; } public Date getOrderDate() { return orderDate; } public void setOrderDate(Date orderDate) { this.orderDate = orderDate; } public String getReceiveName() { return receiveName; } public void setReceiveName(String receiveName) { this.receiveName = receiveName; } public String getReceiveAddress() { return receiveAddress; } public void setReceiveAddress(String receiveAddress) { this.receiveAddress = receiveAddress; } public String getReceivePhone() { return receivePhone; } public void setReceivePhone(String receivePhone) { this.receivePhone = receivePhone; } public OrderStatus getStatus() { return status; } public void setStatus(OrderStatus status) { this.status = status; } public Account getAccount() { return account; } public void setAccount(Account account) { this.account = account; } public String getNote() { return note; } public void setNote(String note) { this.note = note; } }
Code cho lớp entities/OrderDetail
package com.bkap.entities; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @Entity @Table(name="orderdetails") public class OrderDetail { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "orderdetailid") private Integer orderDetailId; @Column(name="price") private float price; @Column(name="quantity") private int quantity; @ManyToOne @JoinColumn(name = "orderid",nullable = false) private Order order=new Order(); @ManyToOne @JoinColumn(name = "prouctid",nullable = false) private Product product=new Product(); public OrderDetail() { // TODO Auto-generated constructor stub } public OrderDetail(float price, int quantity, Product product, Order order) { this.price = price; this.quantity = quantity; this.product = product; this.order=order; } public Integer getOrderDetailId() { return orderDetailId; } public void setOrderDetailId(Integer orderDetailId) { this.orderDetailId = orderDetailId; } public float getPrice() { return price; } public void setPrice(float price) { this.price = price; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } public Order getOrder() { return order; } public void setOrder(Order order) { this.order = order; } public Product getProduct() { return product; } public void setProduct(Product product) { this.product = product; } }
Bổ sung trường quan hệ trong lớp entities/Account
@OneToMany(mappedBy = "account") private Set<Order> orders=new HashSet<Order>(); public Set<Order> getOrders() { return orders; } public void setOrders(Set<Order> orders) { this.orders = orders; }
Bổ sung trường quan hệ trong lớp entities/Product
@OneToMany(mappedBy = "product") private Set<OrderDetail> details=new HashSet<OrderDetail>(); public Set<OrderDetail> getDetails() { return details; } public void setDetails(Set<OrderDetail> details) { this.details = details; }
Bước 2: Cấu hình session trong Properties
# Configure session cookie server.servlet.session.cookie.name=MY_SESSION_COOKIE server.servlet.session.cookie.http-only=true server.servlet.session.cookie.secure=true
Bước 3: Tạo Repositories
Code cho OrderRepository
package com.bkap.repositories; import org.springframework.data.jpa.repository.JpaRepository; import com.bkap.entities.Order; public interface OrderRepository extends JpaRepository<Order, String> { }
Code cho OrderDetailRepository
package com.bkap.repositories; import org.springframework.data.jpa.repository.JpaRepository; import com.bkap.entities.OrderDetail; public interface OrderDetailRepository extends JpaRepository<OrderDetail, Integer> { }
Bước 4: Tạo CartService
package com.bkap.services; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Iterator; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.bkap.entities.Account; import com.bkap.entities.AccountDetails; import com.bkap.entities.Order; import com.bkap.entities.OrderDetail; import com.bkap.entities.OrderStatus; import com.bkap.entities.Product; import com.bkap.models.Item; import com.bkap.repositories.AccountRepository; import com.bkap.repositories.OrderDetailRepository; import com.bkap.repositories.OrderRepository; import com.bkap.repositories.ProductRepository; @Service public class CartService { @Autowired OrderRepository orderRepository; @Autowired OrderDetailRepository detailRepository; @Autowired AccountRepository accountRepository; @Autowired ProductRepository productRepository; public List<Item> addCart(Product product, List<Item> items) { var find = false; for (Item item : items) { if (item.getProductId().equals(product.getProductId())) { item.setQuantity(item.getQuantity() + 1); item.setTotal(item.getPrice() * item.getQuantity()); find = true; break; } } if (!find) { items.add(new Item(product)); } return items; } public List<Item> removeCart(String productId, List<Item> items) { Iterator<Item> temp=items.iterator(); while(temp.hasNext()) { var item=temp.next(); if (item.getProductId().equals(productId)) { items.remove(item); break; } } return items; } public List<Item> updateCart(String[] pids, int[] qtys, List<Item> items){ for (Item item : items) { for (var i=0;i<pids.length;i++) { if(item.getProductId().equals(pids[i])) { item.setQuantity(qtys[i]); item.setTotal(item.getPrice() * item.getQuantity()); } } } return items; } @Transactional public String insertOrder(Order order, List<Item> items) { String orderId="HD"+(new SimpleDateFormat("ddMMyyhhmm").format(new Date())); order.setOrderId(orderId); order.setStatus(OrderStatus.ORDER_NEW); AccountDetails user = (AccountDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Account acc=accountRepository.findByUsername(user.getUsername()).get(); order.setAccount(acc); orderRepository.save(order); for (Item item : items) { OrderDetail detail=new OrderDetail(item.getPrice(), item.getQuantity(), productRepository.findById(item.getProductId()).get(), order); detailRepository.save(detail); } return "Đặt hàng thành công"; } }
Bước 5: Code GlobalFragmentController
package com.bkap.controller; import java.util.ArrayList; import java.util.List; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ModelAttribute; import com.bkap.models.Item; import jakarta.servlet.http.HttpSession; @ControllerAdvice public class GlobalFragmentController { @ModelAttribute("contact") public String loadInfor(Model model) { return "Charles Chung: 0968018161"; } @SuppressWarnings("unchecked") @ModelAttribute("cartitems") public List<Item> loadCartItem(Model model,HttpSession session) { List<Item> items=new ArrayList<Item>(); if(session.getAttribute("cartitems")!=null) { items=(List<Item>)session.getAttribute("cartitems"); } Double total=items.stream().mapToDouble(Item::getTotal).sum(); model.addAttribute("totalCart",total); return items; } }
Bước 6: Code cho các nghiệp vụ giỏ hàng trong client/HomeController
@GetMapping("/cart") public String showcart(Model model) { return "home/shopingcart"; } @SuppressWarnings("unchecked") @PostMapping("/addcart") public String addcart(String productId,Model model,HttpSession session) { List<Item> items=new ArrayList<>(); if(session.getAttribute("cartitems")!=null) { items=(List<Item>)session.getAttribute("cartitems"); } items=cartService.addCart(productService.getById(productId),items); session.setAttribute("cartitems", items); model.addAttribute("cartitems",items); Double total=items.stream().mapToDouble(Item::getTotal).sum(); model.addAttribute("totalCart",total); return "home/shopingcart"; } @SuppressWarnings("unchecked") @GetMapping("/removeCart/{id}") public String removeCart(@PathVariable("id") String productId,Model model,HttpSession session) { List<Item> items=new ArrayList<>(); if(session.getAttribute("cartitems")!=null) { items=(List<Item>)session.getAttribute("cartitems"); } items=cartService.removeCart(productId,items); session.setAttribute("cartitems", items); model.addAttribute("cartitems",items); Double total=items.stream().mapToDouble(Item::getTotal).sum(); model.addAttribute("totalCart",total); return "home/shopingcart"; } @SuppressWarnings("unchecked") @PostMapping("/updateCart") public String updateCart(String[] productids, int[] quantities,Model model,HttpSession session) { List<Item> items=new ArrayList<>(); if(session.getAttribute("cartitems")!=null) { items=(List<Item>)session.getAttribute("cartitems"); } items=cartService.updateCart( productids, quantities,items); session.setAttribute("cartitems", items); model.addAttribute("cartitems",items); Double total=items.stream().mapToDouble(Item::getTotal).sum(); model.addAttribute("totalCart",total); return "home/shopingcart"; } @SuppressWarnings("unchecked") @PostMapping("/checkout") public String checkout(Order order, Model model,HttpSession session) { List<Item> items=new ArrayList<>(); if(session.getAttribute("cartitems")!=null) { items=(List<Item>)session.getAttribute("cartitems"); } String msg=cartService.insertOrder(order,items); model.addAttribute("msg",msg); session.setAttribute("cartitems", new ArrayList<>()); return "redirect:/"; }
Bước 7: Xử lý giao diện
Minicart Popup (nav_bar.html)
<div class="col-4 col-sm-3 col-md-3 col-lg-2"> <div class="site-cart"> <a href="#" class="site-header__cart" title="Cart"> <i class="icon anm anm-bag-l"></i> <span id="CartCount" class="site-header__cart-count" data-cart-render="item_count" th:text="${cartitems.size()}">2</span> </a> <!--Minicart Popup--> <div id="header-cart" class="block block-cart"> <ul class="mini-products-list"> <li class="item" th:each="item:${cartitems}" th:object="${item}"> <a class="product-image" href="#"> <img th:src="*{picture}" th:alt="*{productName}" th:title="*{productName}" /> </a> <div class="product-details"> <a href="#" class="remove"><i class="anm anm-times-l" aria-hidden="true"></i></a> <a href="#" class="edit-i remove"><i class="anm anm-edit" aria-hidden="true"></i></a> <a class="pName" href="/cart" th:text="*{productName}"></a> <div class="variant-cart"><span th:text="*{color}"></span> / <span th:text="*{size}"></span></div> <div class="wrapQtyBtn"> <div class="qtyField"> <span class="label">Qty:</span> <a class="qtyBtn minus" href="javascript:void(0);"><i class="fa anm anm-minus-r" aria-hidden="true"></i></a> <input type="text" id="Quantity" name="quantity" th:value="*{quantity}" class="product-form__input qty"> <a class="qtyBtn plus" href="javascript:void(0);"><i class="fa anm anm-plus-r" aria-hidden="true"></i></a> </div> </div> <div class="priceRow"> <div class="product-price"> $<span class="money" th:text="*{price}"></span> </div> </div> </div> </li> </ul> <div class="total"> <div class="total-in"> <span class="label">Cart Subtotal:</span><span class="product-price">$<span class="money" th:text="${totalCart}">000</span></span> </div> <div class="buttonSet text-center"> <a href="/cart" class="btn btn-secondary btn--small">View Cart</a> <a href="/cart" class="btn btn-secondary btn--small">Checkout</a> </div> </div> </div> <!--End Minicart Popup--> </div> <div class="site-header__search"> <button type="button" class="search-trigger"><i class="icon anm anm-search-l"></i></button> </div> </div>
ShopingCart.html (copy từ template cung cấp)
<!DOCTYPE html> <html xmlns:layout="http://www.ultrap.net.nz/thymeleaf/layout" layout:decorate="~{home_layout}"> <head> <meta charset="UTF-8"> <title>Shoping cart</title> </head> <body> <div layout:fragment="page_content" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6"> <div id="page-content"> <!--Page Title--> <div class="page section-header text-center"> <div class="page-title"> <div class="wrapper"><h1 class="page-width">Your cart</h1><span style="color:blue" th:text="${msg}"></span></div> </div> </div> <!--End Page Title--> <div class="container"> <div class="row"> <div class="col-12 col-sm-12 col-md-8 col-lg-8 main-col"> <form action="/updateCart" method="post" class="cart style2"> <table> <thead class="cart__row cart__header"> <tr> <th colspan="2" class="text-center">Product</th> <th class="text-center">Price</th> <th class="text-center">Quantity</th> <th class="text-right">Total</th> <th class="action"> </th> </tr> </thead> <tbody> <tr class="cart__row border-bottom line1 cart-flex border-top" th:each="item:${cartitems}" th:object="${item}"> <input type="hidden" name="productids[]" th:value="*{productId}"/> <td class="cart__image-wrapper cart-flex-item"> <a href="#"><img class="cart__image" th:src="*{picture}" th:alt="*{productName}"></a> </td> <td class="cart__meta small--text-left cart-flex-item"> <div class="list-view-item__title"> <a href="#" th:text="*{productName}">Elastic Waist Dress </a> </div> <div class="cart__meta-text"> Color: <span th:text="*{color}"></span><br>Size: <span th:text="*{size}"></span><br> </div> </td> <td class="cart__price-wrapper cart-flex-item"> $<span class="money" th:text="*{price}">$735.00</span> </td> <td class="cart__update-wrapper cart-flex-item text-right"> <div class="cart__qty text-center"> <div class="qtyField"> <a class="qtyBtn minus" href="javascript:void(0);"><i class="icon icon-minus"></i></a> <input class="cart__qty-input qty" type="text" name="quantities[]" id="qty" th:value="*{quantity}" pattern="[0-9]*"> <a class="qtyBtn plus" href="javascript:void(0);"><i class="icon icon-plus"></i></a> </div> </div> </td> <td class="text-right small--hide cart-price"> <div>$<span class="money" th:text="*{total}">$735.00</span></div> </td> <td class="text-center small--hide"><a href="#" onclick="return confirm('Bạn có muốn xóa không?')" th:href="@{/removeCart/{id}(id=${item.productId})}" class="btn btn--secondary cart__remove" title="Remove tem"><i class="icon icon anm anm-times-l"></i></a></td> </tr> </tbody> <tfoot> <tr> <td colspan="3" class="text-left"><a href="/shop" class="btn--link cart-continue"><i class="icon icon-arrow-circle-left"></i> Continue shopping</a></td> <td colspan="3" class="text-right"><button type="submit" name="update" class="btn btn-primary cart-update"><i class="fa fa-refresh"></i> Update</button></td> </tr> </tfoot> </table> </form> </div> <div class="col-12 col-sm-12 col-md-4 col-lg-4 cart__footer"> <form action="/checkout" method="post"> <div class="cart-note"> <div sec:authorize="isAuthenticated()" th:remove="tag"> <div class="solid-border"> <h3>INFORMATION RECEIVER</h3> <label>FullName</label><input type="text" th:value="${#authentication.principal.fullName}" name="receiveName" /> <label>Address</label><input type="text" th:value="${#authentication.principal.address}" name="receiveAddress" /> <label>Phone</label><input type="text" th:value="${#authentication.principal.phone}" name="receivePhone" /> <label>Note</label><input type="text" name="note" /> </div> <div class="solid-border"> <div class="row"> <span class="col-12 col-sm-6 cart__subtotal-title"><strong>Subtotal</strong></span> <span class="col-12 col-sm-6 cart__subtotal-title cart__subtotal text-right">$<span class="money" th:text="${totalCart}"></span></span> </div> <div class="cart__shipping">Shipping & taxes calculated at checkout</div> <p class="cart_tearm"> <label> <input type="checkbox" name="tearm" id="cartTearm" class="checkbox" value="tearm" required=""> I agree with the terms and conditions </label> </p> <input type="submit" name="checkout" id="cartCheckout" class="btn btn--small-wide checkout" value="Checkout"> <div class="paymnet-img"> <img src="assets/images/payment-img.jpg" alt="Payment"> </div> </div> </div> <div sec:authorize="isAnonymous()" th:remove="tag"> <div class="solid-border"> <div class="row"> <span class="col-12 col-sm-6 cart__subtotal-title"><strong>Subtotal</strong></span> <span class="col-12 col-sm-6 cart__subtotal-title cart__subtotal text-right">$<span class="money" th:text="${totalCart}"></span></span> </div> <div class="cart__shipping">Shipping & taxes calculated at checkout</div> <p class="cart_tearm"> <label> <input type="checkbox" name="tearm" id="cartTearm" class="checkbox" value="tearm" required=""> I agree with the terms and conditions </label> </p> <a href="/login" id="cartCheckout" class="btn btn--small-wide checkout" >Checkout</a> <div class="paymnet-img"> <img src="assets/images/payment-img.jpg" alt="Payment"> </div> </div> </div> </div> </form> </div> </div> </div> </div> </div> </body> </html>
Sửa lại link trong các trang cho phù hợp
Bước 8: Chạy và kiểm tra kết quả
3. Video demo (quay trong buổi dạy C2308G)
thay lời cảm ơn!
Các bài cũ hơn
- Bảo mật ứng dụng web với Spring Boot (09:12 AM - 28/03/2025)
- Tìm hiểu về Layout trong Thymeleaf (10:58 AM - 25/03/2025)
- Truy xuất database sử dụng Spring Data JPA Phần 3 (03:29 PM - 24/03/2025)
- Truy xuất database sử dụng Spring Data JPA Phần 2 (02:14 PM - 23/03/2025)
- Truy xuất database sử dụng Spring Data JPA Phần 1 (02:14 PM - 20/03/2025)