0

[Java Backend Zero to Hello] BÀI 8.5: STATISTICS & DEPLOYMENT

Java Backend Zero to Hello

📚 Bài viết thuộc series Java Backend Zero to Hello 📌 Phần: Phase 8: Project Thực Tế Cuối Khóa | Bài 83/86


BÀI 8.5: STATISTICS & DEPLOYMENT

Mục tiêu

  • Implement thống kê cho admin
  • Setup CI/CD pipeline
  • Deploy lên cloud
  • Monitoring

1. STATISTICS MODULE

1.1 Dashboard Service

@Service
@RequiredArgsConstructor
@Slf4j
public class StatisticsService {

    private final OrderRepository orderRepository;
    private final UserRepository userRepository;
    private final ProductRepository productRepository;

    public DashboardResponse getDashboard() {
        long totalOrders = orderRepository.count();
        long totalUsers = userRepository.count();
        long totalProducts = productRepository.count();

        BigDecimal totalRevenue = orderRepository.sumFinalAmountByStatus(OrderStatus.DELIVERED);

        long pendingOrders = orderRepository.countByStatus(OrderStatus.PENDING);
        long todayOrders = orderRepository.countByCreatedAtBetween(
            LocalDate.now().atStartOfDay(),
            LocalDate.now().plusDays(1).atStartOfDay()
        );

        return new DashboardResponse(
            totalOrders, totalUsers, totalProducts,
            totalRevenue != null ? totalRevenue : BigDecimal.ZERO,
            pendingOrders, todayOrders
        );
    }

    public List<RevenueResponse> getRevenueByDateRange(LocalDate from, LocalDate to) {
        return orderRepository.getRevenueByDateRange(
            from.atStartOfDay(),
            to.plusDays(1).atStartOfDay(),
            OrderStatus.DELIVERED
        );
    }

    public List<ProductSalesResponse> getBestSellingProducts(int limit) {
        return orderRepository.getBestSellingProducts(limit);
    }

    public List<CustomerRevenueResponse> getTopCustomers(int limit) {
        return orderRepository.getTopCustomers(limit);
    }
}

1.2 Repository Queries

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT COALESCE(SUM(o.finalAmount), 0) FROM Order o WHERE o.status = :status")
    BigDecimal sumFinalAmountByStatus(@Param("status") OrderStatus status);

    long countByStatus(OrderStatus status);

    long countByCreatedAtBetween(LocalDateTime from, LocalDateTime to);

    @Query("""
        SELECT new com.example.dto.RevenueResponse(
            CAST(o.createdAt AS date),
            SUM(o.finalAmount)
        )
        FROM Order o
        WHERE o.createdAt BETWEEN :from AND :to AND o.status = :status
        GROUP BY CAST(o.createdAt AS date)
        ORDER BY CAST(o.createdAt AS date)
        """)
    List<RevenueResponse> getRevenueByDateRange(
        @Param("from") LocalDateTime from,
        @Param("to") LocalDateTime to,
        @Param("status") OrderStatus status
    );

    @Query("""
        SELECT new com.example.dto.ProductSalesResponse(
            p.id, p.name, SUM(oi.quantity), SUM(oi.subtotal)
        )
        FROM OrderItem oi
        JOIN oi.product p
        JOIN oi.order o
        WHERE o.status = 'DELIVERED'
        GROUP BY p.id, p.name
        ORDER BY SUM(oi.quantity) DESC
        """)
    List<ProductSalesResponse> getBestSellingProducts(Pageable pageable);

    @Query("""
        SELECT new com.example.dto.CustomerRevenueResponse(
            u.id, u.username, u.email, COUNT(o), SUM(o.finalAmount)
        )
        FROM Order o
        JOIN o.user u
        WHERE o.status = 'DELIVERED'
        GROUP BY u.id, u.username, u.email
        ORDER BY SUM(o.finalAmount) DESC
        """)
    List<CustomerRevenueResponse> getTopCustomers(Pageable pageable);
}

1.3 Controller

@RestController
@RequestMapping("/admin/statistics")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "Statistics", description = "Admin statistics APIs")
public class StatisticsController {

    private final StatisticsService statisticsService;

    @GetMapping("/dashboard")
    @Operation(summary = "Get dashboard data")
    public ApiResponse<DashboardResponse> getDashboard() {
        return ApiResponse.success(statisticsService.getDashboard());
    }

    @GetMapping("/revenue")
    @Operation(summary = "Get revenue by date range")
    public ApiResponse<List<RevenueResponse>> getRevenue(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
        return ApiResponse.success(statisticsService.getRevenueByDateRange(from, to));
    }

    @GetMapping("/best-selling-products")
    @Operation(summary = "Get best selling products")
    public ApiResponse<List<ProductSalesResponse>> getBestSelling(
            @RequestParam(defaultValue = "10") int limit) {
        return ApiResponse.success(statisticsService.getBestSellingProducts(limit));
    }

    @GetMapping("/top-customers")
    @Operation(summary = "Get top customers")
    public ApiResponse<List<CustomerRevenueResponse>> getTopCustomers(
            @RequestParam(defaultValue = "10") int limit) {
        return ApiResponse.success(statisticsService.getTopCustomers(limit));
    }
}

2. CI/CD PIPELINE

2.1 .github/workflows/ci.yml

name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: docker.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Cache Maven
        uses: actions/cache@v3
        with:
          path: ~/.m2
          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-m2-

      - name: Run tests
        run: mvn test
        env:
          SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/testdb
          SPRING_DATASOURCE_USERNAME: postgres
          SPRING_DATASOURCE_PASSWORD: test

      - name: Upload coverage
        if: always()
        uses: codecov/codecov-action@v3

  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push'

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Build JAR
        run: mvn clean package -DskipTests

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /app
            docker-compose pull
            docker-compose up -d
            docker system prune -f

3. DOCKER COMPOSE PRODUCTION

3.1 docker-compose.prod.yml

version: '3.8'

services:
  app:
    image: username/ecommerce-backend:latest
    container_name: ecommerce-app
    restart: always
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_URL: ${DB_URL}
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      - db
      - redis
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    container_name: nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    networks:
      - app-network

  db:
    image: postgres:16-alpine
    container_name: postgres
    restart: always
    environment:
      POSTGRES_DB: ecommerce
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    container_name: redis
    restart: always
    volumes:
      - redis-data:/data
    networks:
      - app-network

volumes:
  postgres-data:
  redis-data:

networks:
  app-network:
    driver: bridge

3.2 nginx.conf

events {
    worker_connections 1024;
}

http {
    upstream app {
        server app:8080;
    }

    server {
        listen 80;
        server_name example.com;
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl;
        server_name example.com;

        ssl_certificate /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;

        location / {
            proxy_pass http://app;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

4. MONITORING

4.1 Prometheus + Grafana

# docker-compose.monitoring.yml
version: '3.8'

services:
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

4.2 prometheus.yml

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'spring-boot'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['app:8080']

5. README

# E-commerce Backend

Backend API cho hệ thống thương mại điện tử.

## Tech Stack
- Java 17, Spring Boot 3.x
- PostgreSQL, Redis
- Spring Security + JWT
- Docker, Docker Compose
- GitHub Actions

## Setup

### Prerequisites
- Java 17
- Docker & Docker Compose
- Maven

### Development
```bash
# Start database
docker-compose up -d db redis

# Run app
mvn spring-boot:run

Production

docker-compose -f docker-compose.prod.yml up -d

API Documentation

Health Check

http://localhost:8080/api/actuator/health

Testing

mvn test

License

MIT


---

## 6. CHECKLIST HOÀN THÀNH

### Chức năng
- [ ] Authentication (JWT, refresh token)
- [ ] User management
- [ ] Category CRUD
- [ ] Product CRUD với upload ảnh
- [ ] Cart management
- [ ] Order creation & management
- [ ] Payment (mock)
- [ ] Statistics dashboard

### Technical
- [ ] Clean code, SOLID
- [ ] Unit test > 70% coverage
- [ ] Integration test
- [ ] API documentation (Swagger)
- [ ] Docker, Docker Compose
- [ ] CI/CD pipeline
- [ ] Deploy thành công
- [ ] Monitoring

### Documentation
- [ ] README đầy đủ
- [ ] API documentation
- [ ] Architecture diagram
- [ ] Database schema

---

## 7. BÀI TẬP CUỐI KHÓA

### Bài 1: Statistics
Implement dashboard với các thống kê.

### Bài 2: CI/CD
Setup GitHub Actions workflow.

### Bài 3: Deploy
Deploy lên Railway/Render/AWS.

### Bài 4: Monitoring
Setup Prometheus + Grafana.

---

**Chúc mừng bạn đã hoàn thành lộ trình Backend Java!**


---

### 🧭 Điều Hướng Series

⬅️ **Bài trước:** [BÀI 8.4: CART & ORDER MODULE](https://viblo.asia/s/java-backend-zero-to-hello-WR5JRNQ7JGv)

📋 **Lộ trình tổng quan:** [Xem Toàn Bộ Series](https://viblo.asia/s/java-backend-zero-to-hello-WR5JRNQ7JGv)

➡️ **Bài tiếp theo:** [Phase 8 - Đặc Tả Kỹ Thuật Dự Án E-Commerce Backend](https://viblo.asia/p/java-backend-zero-to-hello-phase-8-dac-ta-ky-thuat-du-an-e-commerce-backend-Nj4vgG0OJ6r)

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í