[Java Backend Zero to Hello] [Phase 8] BÀI 8.5: STATISTICS & DEPLOYMENT
📚 Series: Java Backend Zero to Hello 📂 Phân đoạn: Phase 8: Project thực tế 📖 Nội dung: BÀI 8.5: STATISTICS & DEPLOYMENT 💡 Khóa học lập trình Backend Java & Spring Boot chuẩn doanh nghiệp từ con số 0.
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
- Swagger UI: http://localhost:8080/api/swagger-ui.html
- OpenAPI JSON: http://localhost:8080/api/v3/api-docs
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 bài học
- ⬅️ **Bài trước:** [BÀI 8.4: CART & ORDER MODULE](https://viblo.asia/p/XRJ8Rd2dVGq)
- ➡️ **Bài tiếp theo:** **PHASE 8: PROJECT THỰC TẾ CUỐI KHÓA**
- 📑 **Toàn bộ lộ trình:** [Java Backend Zero to Hello](https://viblo.asia/s/java-backend-zero-to-hello-vlZL9eXdLQK)
> ⭐ *Hãy bookmark (clip) lại series để tiện theo dõi các bài học tiếp theo nhé!*
All rights reserved