Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 170 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,170 @@
# huu_java
# 🍔 F&B Store — Foods & Drinks

Mock project website bán đồ ăn & thức uống, xây dựng bằng Spring Boot. Dự án gồm **3 phần** chạy chung trong 1 ứng dụng:

| Phần | Đường dẫn | Mô tả | Tài liệu chi tiết |
|---|---|---|---|
| **Admin** | `/admin/**` | Trang quản trị SSR (Thymeleaf): dashboard, quản lý sản phẩm, danh mục, đơn hàng, người dùng, góp ý | [docs/admin/README.md](docs/admin/README.md) |
| **API** | `/api/v1/**` | REST API (JWT) chia sẻ để xây trang người dùng (SPA/mobile) | [docs/api/README.md](docs/api/README.md) |
| **Web** | `/`, `/menu`, ... | Storefront SSR cho người dùng cuối: xem menu, giỏ hàng, đặt hàng, đánh giá sản phẩm | [docs/web/README.md](docs/web/README.md) |

## Tech Stack

- **Ngôn ngữ:** Java 25
- **Framework:** Spring Boot 4.x (`spring-boot-starter-parent` 4.0.7)
- **Build tool:** Maven (Maven Wrapper đi kèm)
- **Database:** MySQL 8 + Spring Data JPA / Hibernate
- **Migration:** Flyway (`src/main/resources/db/migration`)
- **View:** Thymeleaf + Thymeleaf Layout Dialect, Bootstrap 5, Vanilla JS
- **Security:** Spring Security — Form Login + OAuth2 (Google/Facebook/Twitter) cho Web, JWT (jjwt 0.12) cho API
- **Khác:** Lombok, ModelMapper, Caffeine Cache, Spring Mail (Mailtrap), Slack Incoming Webhook, springdoc-openapi 3.x (Swagger UI), Actuator

## Prerequisites

- **JDK 25** (pom.xml pin `<java.version>25</java.version>` — JDK 17/21 sẽ không build được)
- **MySQL 8.x** đang chạy tại `localhost:3306` (hoặc override qua env var)
- Không cần cài Maven — dùng Maven Wrapper (`mvnw.cmd` trên Windows, `./mvnw` trên Mac/Linux)

## Quick Start

1. **Tạo database:**

```sql
CREATE DATABASE foodsndrinks CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```

2. **Khai báo biến môi trường bắt buộc** (dev profile không có default cho DB credentials):

```bash
# Windows (PowerShell)
$env:DB_USERNAME = "root"
$env:DB_PASSWORD = "your-password"

# Mac/Linux
export DB_USERNAME=root
export DB_PASSWORD=your-password
```

3. **Chạy ứng dụng** (Flyway tự tạo bảng khi khởi động):

```bash
# Windows
mvnw.cmd spring-boot:run

# Mac/Linux
./mvnw spring-boot:run
```

4. Truy cập:
- Web (người dùng): http://localhost:8080
- Admin: http://localhost:8080/admin (cần tài khoản `ROLE_ADMIN`)
- Swagger UI (chỉ dev): http://localhost:8080/api-docs/swagger

**Các lệnh khác:**

```bash
./mvnw test # Chạy unit test
./mvnw clean package -DskipTests # Build file JAR (bỏ qua test)
./mvnw clean # Clean dự án
```

## Project Structure

Kiến trúc 3 lớp (3-Tier), package gốc `com.huuhv.foodsndrinks`:

```
src/main/java/com/huuhv/foodsndrinks/
├── config/ # SecurityConfig, WebMvcConfig, AsyncConfig, OpenApiConfig, AppConfig
├── controller/
│ ├── admin/ # Controller trang quản trị (SSR) → docs/admin/README.md
│ ├── api/ # REST controller /api/v1/** → docs/api/README.md
│ ├── web/ # Controller storefront (SSR) → docs/web/README.md
│ └── GlobalErrorController.java # Xử lý lỗi dispatch cấp servlet (/error)
├── dto/
│ ├── request/ # DTO hứng dữ liệu vào (không dùng Entity cho View/API)
│ └── response/ # DTO trả ra
├── entity/ # User, Product, ProductImage, Category, Order, OrderDetail, Rating, Suggestion
├── enums/ # Role, AuthProvider, CategoryType, ProductType, OrderStatus, SuggestionStatus
├── exception/ # AdminExceptionHandler, WebExceptionHandler, ApiExceptionHandler + custom exceptions
├── repository/ # Spring Data JPA repositories
├── security/ # JwtUtil, JwtAuthenticationFilter, CustomUserDetailsService, OAuth2 user services
├── service/ # Toàn bộ business logic (Controller chỉ gọi Service)
│ └── notification/ # Slack/Email notification, order event listener, statistics scheduler
└── utils/ # SlugUtils...

src/main/resources/
├── db/migration/ # Flyway migrations
├── templates/ # Thymeleaf (admin/, web/, error/)
├── static/ # CSS, JS, images
└── application*.yml # Cấu hình theo profile
```

**Quy ước quan trọng:** Constructor Injection qua `@RequiredArgsConstructor` + `private final` (không dùng field injection); Controller mỏng, logic nằm ở Service; query quan hệ dùng `LEFT JOIN FETCH` chống N+1. Xem thêm `CLAUDE.md`.

## Configuration

Profile mặc định là `dev` (đổi qua env `SPRING_PROFILES_ACTIVE=prod` khi deploy).

| File | Vai trò |
|---|---|
| `application.yml` | Cấu hình chung: mail (SMTP), OAuth2 client (Google/Facebook/Twitter), Slack webhook, cron thống kê |
| `application-dev.yml` | Datasource local, logging DEBUG, Swagger UI bật, JWT secret fallback, upload dir |
| `application-prod.yml` | Datasource từ env (bắt buộc), logging ra file `logs/foodsndrinks.log`, **Swagger tắt hoàn toàn** |

### Biến môi trường

| Biến | Bắt buộc | Mô tả |
|---|---|---|
| `DB_USERNAME` / `DB_PASSWORD` | ✅ | Tài khoản MySQL |
| `DB_FOODSNDRINKS_URL` | Prod | JDBC URL (dev mặc định `jdbc:mysql://localhost:3306/foodsndrinks`) |
| `JWT_SECRET` | Prod | Secret ký JWT, ≥ 32 ký tự (dev có fallback) |
| `REMEMBER_ME_KEY` | Prod | Key cho remember-me cookie (dev tự sinh random) |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Khi dùng login Google | OAuth2 credentials từ Google Console |
| `FACEBOOK_CLIENT_ID` / `FACEBOOK_CLIENT_SECRET` | Khi dùng login Facebook | OAuth2 credentials |
| `TWITTER_CLIENT_ID` / `TWITTER_CLIENT_SECRET` | Khi dùng login Twitter/X | OAuth2 credentials |
| `MAIL_HOST` / `MAIL_PORT` / `MAIL_USERNAME` / `MAIL_PASSWORD` | Khi gửi mail thật | SMTP (mặc định Mailtrap sandbox) |
| `ADMIN_NOTIFY_EMAIL` | — | Email nhận thông báo đơn hàng & báo cáo tháng |
| `SLACK_WEBHOOK_URL` / `SLACK_NOTIFY_ENABLED` | — | Slack incoming webhook thông báo đơn mới |
| `MAIL_NOTIFY_ENABLED` | — | Bật/tắt email thông báo |
| `ORDER_STATS_CRON` | — | Cron gửi báo cáo tháng (mặc định `0 0 23 L * *`, 23h ngày cuối tháng, giờ VN) |
| `UPLOAD_DIR` | — | Thư mục lưu ảnh upload (mặc định `./uploads`) |
| `SERVER_PORT` | — | Port (mặc định 8080) |

## Database Migrations

Dùng **Flyway**, tự chạy khi ứng dụng khởi động. File đặt tại `src/main/resources/db/migration` với quy ước tên `V<yyyyMMddHHmmss>__<mo_ta>.sql`:

```
V20260624150700__create_table_users.sql
V20260624150710__create_table_categories.sql
V20260624150720__create_table_products.sql
V20260624150730__create_table_product_images.sql
V20260624150740__create_table_orders.sql
V20260624150750__create_table_order_details.sql
V20260624150760__create_table_ratings.sql
V20260624150770__create_table_suggestions.sql
V20260624150780__create_indexes.sql
V20260702000001__alter_users_phone_nullable.sql
```

Quy ước: tên bảng số nhiều (`users`, `products`...). **Không sửa migration đã chạy** — muốn đổi schema thì thêm file version mới.

## Notification & Batch Job

- **Đơn hàng mới:** sau khi checkout commit thành công (`OrderPlacedEvent` + `@TransactionalEventListener`), hệ thống gửi **Slack** + **email** cho admin (chạy async qua thread pool `notify-*`).
- **Báo cáo tháng:** `OrderStatisticsScheduler` chạy theo cron (mặc định 23h ngày cuối tháng, `Asia/Ho_Chi_Minh`) — tổng hợp doanh thu, số đơn theo trạng thái và email cho admin.

## Xử Lý Lỗi

- Mỗi luồng có `@ControllerAdvice` riêng cùng package: `AdminExceptionHandler` (view lỗi admin), `WebExceptionHandler` (view lỗi web), `ApiExceptionHandler` (JSON `ErrorResponse`).
- `GlobalErrorController` (`/error`) chỉ xử lý lỗi dispatch cấp servlet (404/403/405/409 và exception thoát khỏi mọi advice) — tự chọn trả JSON hay view theo prefix URI.

## Monitoring

- Actuator: `/actuator/health` public; các endpoint khác (`info`, `metrics`, `caches` — chỉ dev) yêu cầu `ROLE_ADMIN`. Prod chỉ expose `health`.

## Tài Liệu Chi Tiết

- 🛠 [Admin — trang quản trị](docs/admin/README.md)
- 🔌 [API — REST API cho trang người dùng](docs/api/README.md)
- 🛒 [Web — storefront người dùng](docs/web/README.md)
88 changes: 88 additions & 0 deletions docs/admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 🛠 Admin — Trang Quản Trị (SSR)

Trang quản trị hệ thống, render phía server bằng Thymeleaf + Bootstrap 5. Toàn bộ đường dẫn `/admin/**` yêu cầu **`ROLE_ADMIN`**, đăng nhập bằng form login tại `/login` (session-based, hỗ trợ remember-me 7 ngày).

> Quay lại [README tổng quan](../../README.md) · Xem thêm: [API](../api/README.md) · [Web](../web/README.md)

## Tổng Quan

| Thành phần | Vị trí |
|---|---|
| Controllers | `com.huuhv.foodsndrinks.controller.admin` |
| Templates | `src/main/resources/templates/admin/` (layout: `admin/layout/base.html`, dùng Thymeleaf Layout Dialect) |
| Xử lý lỗi | `AdminExceptionHandler` (`@ControllerAdvice` scoped package admin) + view `error/admin/{404,403,405,409,500}` |
| Model advice | `AdminLayoutModelAdvice` — inject `currentPath` và các enum (categoryTypes, productTypes, orderStatuses, roles, suggestionStatuses) vào mọi view admin |

## Chức Năng & Routes

### Dashboard — `AdminController`

| Method | Path | Mô tả |
|---|---|---|
| GET | `/admin`, `/admin/dashboard` | Dashboard thống kê tổng quan |

### Quản lý sản phẩm — `ProductAdminController` (`/admin/products`)

| Method | Path | Mô tả |
|---|---|---|
| GET | `/admin/products` | Danh sách phân trang, filter theo tên / danh mục / loại / trạng thái bán |
| GET | `/admin/products/add` | Form thêm sản phẩm |
| POST | `/admin/products/add` | Tạo sản phẩm, upload nhiều ảnh (`newImages`, tối đa 10MB/file) |
| GET | `/admin/products/edit/{id}` | Form sửa sản phẩm |
| POST | `/admin/products/edit/{id}` | Cập nhật (thêm/xóa ảnh, chọn ảnh đại diện) |
| POST | `/admin/products/{id}/delete` | Xóa sản phẩm |

Ảnh upload lưu tại thư mục `UPLOAD_DIR` (mặc định `./uploads`), serve qua `/uploads/**` (cấu hình trong `WebMvcConfig`).

### Quản lý danh mục — `CategoryAdminController` (`/admin/categories`)

| Method | Path | Mô tả |
|---|---|---|
| GET | `/admin/categories` | Danh sách phân trang + filter |
| GET / POST | `/admin/categories/add` | Thêm danh mục (validate, check trùng) |
| GET / POST | `/admin/categories/edit/{id}` | Sửa danh mục |
| POST | `/admin/categories/{id}/delete` | Xóa danh mục |

### Quản lý đơn hàng — `OrderAdminController` (`/admin/orders`)

| Method | Path | Mô tả |
|---|---|---|
| GET | `/admin/orders` | Danh sách phân trang, filter theo trạng thái / keyword / mã đơn |
| GET | `/admin/orders/{id}` | Chi tiết đơn hàng |
| POST | `/admin/orders/{id}/status` | Cập nhật trạng thái đơn (`OrderStatus`) |

### Quản lý người dùng — `UserAdminController` (`/admin/users`)

| Method | Path | Mô tả |
|---|---|---|
| GET | `/admin/users` | Danh sách phân trang, filter theo keyword / role / trạng thái active |
| GET / POST | `/admin/users/{id}/edit` | Sửa thông tin người dùng |
| POST | `/admin/users/{id}/toggle-active` | Khóa / mở khóa tài khoản |

### Quản lý góp ý — `SuggestionAdminController` (`/admin/suggestions`)

| Method | Path | Mô tả |
|---|---|---|
| GET | `/admin/suggestions` | Danh sách phân trang, filter theo trạng thái / keyword |
| GET | `/admin/suggestions/{id}` | Chi tiết + form sửa |
| POST | `/admin/suggestions/{id}/update` | Lưu chỉnh sửa góp ý |
| POST | `/admin/suggestions/{id}/status` | Cập nhật trạng thái (`SuggestionStatus`) |

## Thông Báo Cho Admin

Admin nhận thông báo tự động (không cần thao tác trên UI):

- **Đơn hàng mới:** sau khi user checkout thành công → gửi **Slack** (incoming webhook) + **email** tới `ADMIN_NOTIFY_EMAIL`. Chạy async (`@Async`), fire sau khi transaction commit (`@TransactionalEventListener`).
- **Báo cáo doanh thu tháng:** `OrderStatisticsScheduler` chạy cron `ORDER_STATS_CRON` (mặc định 23h ngày cuối tháng, giờ VN) — email tổng doanh thu, số đơn hoàn thành, số đơn theo từng trạng thái.

Bật/tắt qua `SLACK_NOTIFY_ENABLED`, `MAIL_NOTIFY_ENABLED`. Xem package `service/notification/`.

## Xử Lý Lỗi

- Exception nghiệp vụ (`ResourceNotFoundException` → 404, `DuplicateResourceException` → 409, `Exception` khác → 500) được `AdminExceptionHandler` bắt và render view `error/admin/*`.
- Lỗi dispatch cấp servlet (404 URL không tồn tại, 403, 405...) do `GlobalErrorController` xử lý — với URI prefix `/admin/*` sẽ trả về view lỗi admin.
- Thông báo thao tác thành công/thất bại trên UI dùng `RedirectAttributes` (flash message).

## Monitoring

Admin đã đăng nhập có thể truy cập Actuator: `/actuator/health` (chi tiết), `/actuator/metrics`, `/actuator/info`, `/actuator/caches` (chỉ dev).
98 changes: 98 additions & 0 deletions docs/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 🔌 API — REST API Cho Trang Người Dùng

REST API dưới prefix `/api/v1`, trả JSON, xác thực bằng **JWT (Bearer token)**. Đây là API chia sẻ để xây trang người dùng dạng SPA/mobile app trong tương lai.

> Quay lại [README tổng quan](../../README.md) · Xem thêm: [Admin](../admin/README.md) · [Web](../web/README.md)

## Tổng Quan

| Thành phần | Vị trí |
|---|---|
| Controllers | `com.huuhv.foodsndrinks.controller.api` (`@RestController`) |
| Security | Chain riêng `apiSecurityFilterChain` (`@Order(2)`, matcher `/api/**`) — **stateless**, CSRF off, `JwtAuthenticationFilter` |
| JWT | `JwtUtil` (jjwt 0.12), HS256, secret từ `JWT_SECRET`, hết hạn sau **1 giờ** (`jwt.expiration: 3600000`) |
| Xử lý lỗi | `ApiExceptionHandler` (`@RestControllerAdvice`) — trả JSON `ErrorResponse` |
| OpenAPI | `OpenApiConfig` + springdoc-openapi 3.x |

## Swagger / OpenAPI

Chỉ bật ở profile **dev** (prod tắt hoàn toàn):

- **Swagger UI:** `http://localhost:8080/api-docs/swagger`
- **OpenAPI JSON:** `http://localhost:8080/api/v1/api-docs`

Swagger có nút **Authorize** (security scheme `bearerAuth`) — dán JWT lấy từ endpoint login để gọi các API cần xác thực.

## Xác Thực

### Đăng nhập lấy token

```
POST /api/v1/auth/login
Content-Type: application/json

{
"username": "user@example.com", // username hoặc email
"password": "secret"
}
```

Response (`AuthResDto`) chứa JWT. Các request sau gửi kèm header:

```
Authorization: Bearer <token>
```

Token sai/thiếu/hết hạn → **401 Unauthorized**. Tài khoản bị khóa → 401 (bắt `DisabledException`).

## Endpoints

### Auth — `AuthApiController` (`/api/v1/auth`)

| Method | Path | Auth | Mô tả |
|---|---|---|---|
| POST | `/api/v1/auth/login` | Public | Đăng nhập bằng username/email + password, trả JWT |

### Products — `ProductApiController` (`/api/v1/products`)

| Method | Path | Auth | Mô tả |
|---|---|---|---|
| GET | `/api/v1/products` | `USER` / `ADMIN` | Danh sách sản phẩm phân trang (chỉ `isAvailable=true`), filter: `name`, `categoryId`, `type` |
| GET | `/api/v1/products/{id}` | `USER` / `ADMIN` | Chi tiết sản phẩm kèm danh sách ảnh |

Response danh sách bọc trong `PageResDto` (nội dung + thông tin phân trang).

### Ví dụ với curl

```bash
# 1. Login
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"user@example.com","password":"secret"}'

# 2. Gọi API với token
curl http://localhost:8080/api/v1/products?page=0&size=10 \
-H "Authorization: Bearer <token>"
```

## Định Dạng Lỗi

`ApiExceptionHandler` trả JSON `ErrorResponse` thống nhất:

| Exception | HTTP status |
|---|---|
| `ResourceNotFoundException` | 404 |
| `IllegalArgumentException` | 400 |
| `MethodArgumentNotValidException` (validation) | 400 — kèm map `field → message` |
| `DuplicateResourceException` | 409 |
| `BadCredentialsException` / `DisabledException` | 401 |
| `Exception` khác | 500 |

Lỗi dispatch cấp servlet (ví dụ 404 URL không tồn tại) do `GlobalErrorController` xử lý — URI prefix `/api/*` vẫn nhận JSON `ErrorResponse`, không bao giờ nhận HTML.

## Quy Ước Khi Thêm API Mới

- Đặt controller trong `controller.api`, base path `/api/v1/...`, thêm `@Tag` để Swagger gom nhóm (springdoc chỉ scan package `controller.api`).
- Request/response dùng DTO trong `dto/request` / `dto/response` — **không expose Entity**.
- Khai báo rule phân quyền cho path mới trong `apiSecurityFilterChain` (`SecurityConfig`).
- Exception nghiệp vụ mới → thêm handler vào `ApiExceptionHandler`, không sửa `GlobalErrorController`.
Loading