[Java Backend Zero to Hello] BÀI 2.12: BÀI TẬP CUỐI PHASE 2
📚 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
- Đọc danh sách đơn hàng từ file CSV
- Lọc đơn hàng theo trạng thái, khoảng thời gian
- Thống kê doanh thu theo ngày/tháng
- Tìm khách hàng VIP (tổng giá trị > 10 triệu)
- Xuất báo cáo ra file
- 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
- Đa luồng: Xử lý nhiều file CSV song song với
ExecutorService - CompletableFuture: Chain các thao tác async
- Custom Exception: Tạo
InvalidOrderException,FileProcessingException - Generic Repository: Tạo
Repository<T, ID>interface - Date/Time: Lọc đơn hàng theo khoảng thời gian với
LocalDate,LocalDateTime - 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