0

[Java Backend Zero to Hello] BÀI 2.12: BÀI TẬP CUỐI PHASE 2

Java Backend Zero to Hello

📚 Bài viết thuộc series Java Backend Zero to Hello 📌 Phần: Phase 2: Java Core Nâng Cao | Bài 27/86


BÀI 2.12: BÀI TẬP CUỐI PHASE 2

Mục tiêu

  • Tổng hợp kiến thức Phase 2
  • Thực hành với project thực tế
  • Áp dụng Generic, Lambda, Stream, Optional, Date/Time, đa luồng

ĐỀ BÀI: HỆ THỐNG XỬ LÝ ĐƠN HÀNG

Xây dựng ứng dụng console xử lý đơn hàng với các yêu cầu:

Yêu cầu chức năng

  1. Đọc danh sách đơn hàng từ file CSV
  2. Lọc đơn hàng theo trạng thái, khoảng thời gian
  3. Thống kê doanh thu theo ngày/tháng
  4. Tìm khách hàng VIP (tổng giá trị > 10 triệu)
  5. Xuất báo cáo ra file
  6. Xử lý song song nhiều file

CẤU TRÚC PROJECT

order-system/
├── src/
│   ├── model/
│   │   ├── Order.java
│   │   ├── OrderItem.java
│   │   ├── OrderStatus.java
│   │   └── Customer.java
│   ├── service/
│   │   ├── OrderService.java
│   │   ├── ReportService.java
│   │   └── FileService.java
│   └── Main.java
├── data/
│   └── orders.csv
└── output/
    └── report.txt

CODE MẪU

1. Model

// OrderStatus.java
public enum OrderStatus {
    PENDING("Đang chờ"),
    CONFIRMED("Đã xác nhận"),
    SHIPPED("Đang giao"),
    DELIVERED("Đã giao"),
    CANCELLED("Đã hủy");

    private final String displayName;

    OrderStatus(String displayName) {
        this.displayName = displayName;
    }

    public String getDisplayName() {
        return displayName;
    }
}
// OrderItem.java
public record OrderItem(
    String productId,
    String productName,
    int quantity,
    double price
) {
    public double getTotal() {
        return quantity * price;
    }
}
// Order.java
public record Order(
    String orderId,
    String customerId,
    String customerName,
    List<OrderItem> items,
    OrderStatus status,
    LocalDateTime createdAt
) {
    public double getTotal() {
        return items.stream()
            .mapToDouble(OrderItem::getTotal)
            .sum();
    }

    public int getItemCount() {
        return items.stream()
            .mapToInt(OrderItem::quantity)
            .sum();
    }
}
// Customer.java
public record Customer(
    String id,
    String name,
    String email,
    String phone
) {}

2. Service

// OrderService.java
public class OrderService {
    private final List<Order> orders;

    public OrderService(List<Order> orders) {
        this.orders = new ArrayList<>(orders);
    }

    // Lọc theo trạng thái
    public List<Order> findByStatus(OrderStatus status) {
        return orders.stream()
            .filter(o -> o.status() == status)
            .collect(Collectors.toList());
    }

    // Lọc theo khoảng thời gian
    public List<Order> findByDateRange(LocalDate start, LocalDate end) {
        return orders.stream()
            .filter(o -> {
                LocalDate orderDate = o.createdAt().toLocalDate();
                return !orderDate.isBefore(start) && !orderDate.isAfter(end);
            })
            .collect(Collectors.toList());
    }

    // Tổng doanh thu
    public double getTotalRevenue() {
        return orders.stream()
            .filter(o -> o.status() == OrderStatus.DELIVERED)
            .mapToDouble(Order::getTotal)
            .sum();
    }

    // Doanh thu theo ngày
    public Map<LocalDate, Double> getRevenueByDate() {
        return orders.stream()
            .filter(o -> o.status() == OrderStatus.DELIVERED)
            .collect(Collectors.groupingBy(
                o -> o.createdAt().toLocalDate(),
                Collectors.summingDouble(Order::getTotal)
            ));
    }

    // Top khách hàng
    public Map<String, Double> getTopCustomers(int limit) {
        Map<String, Double> customerRevenue = orders.stream()
            .filter(o -> o.status() == OrderStatus.DELIVERED)
            .collect(Collectors.groupingBy(
                Order::customerId,
                Collectors.summingDouble(Order::getTotal)
            ));

        return customerRevenue.entrySet().stream()
            .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
            .limit(limit)
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue,
                (a, b) -> a,
                LinkedHashMap::new
            ));
    }

    // Khách hàng VIP
    public List<String> getVipCustomers(double threshold) {
        return getTopCustomers(Integer.MAX_VALUE).entrySet().stream()
            .filter(e -> e.getValue() >= threshold)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
    }

    // Sản phẩm bán chạy
    public Map<String, Long> getBestSellingProducts(int limit) {
        Map<String, Long> productSales = orders.stream()
            .filter(o -> o.status() == OrderStatus.DELIVERED)
            .flatMap(o -> o.items().stream())
            .collect(Collectors.groupingBy(
                OrderItem::productId,
                Collectors.summingLong(OrderItem::quantity)
            ));

        return productSales.entrySet().stream()
            .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
            .limit(limit)
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue,
                (a, b) -> a,
                LinkedHashMap::new
            ));
    }
}
// FileService.java
public class FileService {

    public List<Order> readOrdersFromCsv(String filePath) throws IOException {
        return Files.lines(Paths.get(filePath))
            .skip(1)  // Bỏ header
            .map(this::parseOrder)
            .filter(Optional::isPresent)
            .map(Optional::get)
            .collect(Collectors.toList());
    }

    private Optional<Order> parseOrder(String line) {
        try {
            String[] parts = line.split(",");
            // Format: orderId,customerId,customerName,status,createdAt,items
            String orderId = parts[0];
            String customerId = parts[1];
            String customerName = parts[2];
            OrderStatus status = OrderStatus.valueOf(parts[3]);
            LocalDateTime createdAt = LocalDateTime.parse(parts[4]);

            // Parse items (giả sử format đơn giản)
            List<OrderItem> items = parseItems(parts[5]);

            return Optional.of(new Order(
                orderId, customerId, customerName, items, status, createdAt
            ));
        } catch (Exception e) {
            System.err.println("Lỗi parse: " + line);
            return Optional.empty();
        }
    }

    private List<OrderItem> parseItems(String itemsStr) {
        // itemsStr: "productId:quantity:price|productId:quantity:price"
        return Arrays.stream(itemsStr.split("\\|"))
            .map(item -> {
                String[] parts = item.split(":");
                return new OrderItem(
                    parts[0],
                    parts[0],  // Tên = id (giả lập)
                    Integer.parseInt(parts[1]),
                    Double.parseDouble(parts[2])
                );
            })
            .collect(Collectors.toList());
    }

    public void writeReport(String filePath, String content) throws IOException {
        Files.writeString(Paths.get(filePath), content);
    }
}
// ReportService.java
public class ReportService {

    public String generateDailyReport(OrderService orderService, LocalDate date) {
        StringBuilder sb = new StringBuilder();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");

        sb.append("=== BÁO CÁO NGÀY ").append(date.format(formatter)).append(" ===\n\n");

        List<Order> dailyOrders = orderService.findByDateRange(date, date);

        long totalOrders = dailyOrders.size();
        long completedOrders = dailyOrders.stream()
            .filter(o -> o.status() == OrderStatus.DELIVERED)
            .count();
        double revenue = dailyOrders.stream()
            .filter(o -> o.status() == OrderStatus.DELIVERED)
            .mapToDouble(Order::getTotal)
            .sum();

        sb.append("Tổng đơn hàng: ").append(totalOrders).append("\n");
        sb.append("Đơn hoàn thành: ").append(completedOrders).append("\n");
        sb.append("Doanh thu: ").append(String.format("%,.0f", revenue)).append(" VND\n");

        return sb.toString();
    }

    public String generateCustomerReport(OrderService orderService) {
        StringBuilder sb = new StringBuilder();
        sb.append("=== TOP 10 KHÁCH HÀNG ===\n\n");

        Map<String, Double> topCustomers = orderService.getTopCustomers(10);
        int rank = 1;
        for (Map.Entry<String, Double> entry : topCustomers.entrySet()) {
            sb.append(String.format("%d. %s - %,.0f VND%n",
                rank++, entry.getKey(), entry.getValue()));
        }

        return sb.toString();
    }
}

3. Main

public class Main {
    public static void main(String[] args) throws Exception {
        // Đọc dữ liệu
        FileService fileService = new FileService();
        List<Order> orders = fileService.readOrdersFromCsv("data/orders.csv");

        OrderService orderService = new OrderService(orders);
        ReportService reportService = new ReportService();

        // Menu
        Scanner sc = new Scanner(System.in);
        boolean running = true;

        while (running) {
            System.out.println("\n=== MENU ===");
            System.out.println("1. Xem tổng doanh thu");
            System.out.println("2. Xem doanh thu theo ngày");
            System.out.println("3. Xem top khách hàng");
            System.out.println("4. Xem sản phẩm bán chạy");
            System.out.println("5. Lọc đơn hàng theo trạng thái");
            System.out.println("6. Xuất báo cáo");
            System.out.println("0. Thoát");
            System.out.print("Chọn: ");

            int choice = sc.nextInt();
            sc.nextLine();

            switch (choice) {
                case 1 -> System.out.printf("Tổng DT: %,.0f VND%n",
                    orderService.getTotalRevenue());

                case 2 -> {
                    Map<LocalDate, Double> revenue = orderService.getRevenueByDate();
                    revenue.forEach((date, amount) ->
                        System.out.printf("%s: %,.0f VND%n", date, amount));
                }

                case 3 -> {
                    Map<String, Double> top = orderService.getTopCustomers(5);
                    top.forEach((id, amount) ->
                        System.out.printf("%s: %,.0f VND%n", id, amount));
                }

                case 4 -> {
                    Map<String, Long> best = orderService.getBestSellingProducts(5);
                    best.forEach((id, qty) ->
                        System.out.printf("%s: %d sản phẩm%n", id, qty));
                }

                case 5 -> {
                    System.out.print("Trạng thái (PENDING/CONFIRMED/SHIPPED/DELIVERED/CANCELLED): ");
                    String status = sc.nextLine();
                    List<Order> filtered = orderService.findByStatus(
                        OrderStatus.valueOf(status.toUpperCase())
                    );
                    System.out.println("Tìm thấy " + filtered.size() + " đơn");
                }

                case 6 -> {
                    String report = reportService.generateCustomerReport(orderService);
                    fileService.writeReport("output/report.txt", report);
                    System.out.println("Đã xuất báo cáo");
                }

                case 0 -> running = false;
            }
        }

        sc.close();
    }
}

YÊU CẦU NÂNG CAO

  1. Đa luồng: Xử lý nhiều file CSV song song với ExecutorService
  2. CompletableFuture: Chain các thao tác async
  3. Custom Exception: Tạo InvalidOrderException, FileProcessingException
  4. Generic Repository: Tạo Repository<T, ID> interface
  5. Date/Time: Lọc đơn hàng theo khoảng thời gian với LocalDate, LocalDateTime
  6. Optional: Tránh null trong toàn bộ code

TIÊU CHÍ ĐÁNH GIÁ

  • ✅ Code chạy đúng, không exception
  • ✅ Sử dụng Stream API hiệu quả
  • ✅ Áp dụng Generic, Optional
  • ✅ Xử lý lỗi đúng cách
  • ✅ Code sạch, dễ đọc
  • ✅ Có comment giải thích logic phức tạp

Hoàn thành Phase 2! Tiếp tục với Phase 3: Công cụ & Build tools


🧭 Điều Hướng Series

⬅️ Bài trước: BÀI 2.11: JAVA 9-17 FEATURES

📋 Lộ trình tổng quan: Xem Toàn Bộ Series

➡️ Bài tiếp theo: PHASE 3: CÔNG CỤ & BUILD TOOLS - Tổng Quan & Mục Tiêu


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí