Cập nhật cấu hình môi trường production và development
- Thêm file .env.prod với cấu hình chi tiết cho môi trường production - Cập nhật docker-compose.dev.yml và docker-compose.prod.yml - Tạo Dockerfile.prod với cấu hình chi tiết cho production - Bổ sung cấu hình nginx, prometheus, grafana - Thêm cấu hình backup và monitoring - Cập nhật README với hướng dẫn chi tiết
This commit is contained in:
parent
e2a219cacd
commit
86a60a7861
41
.env.prod
Normal file
41
.env.prod
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# App Configuration
|
||||||
|
PORT=8080
|
||||||
|
APP_ENV=production
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
BLUEPRINT_DB_HOST=mysql_bp
|
||||||
|
BLUEPRINT_DB_PORT=3306
|
||||||
|
BLUEPRINT_DB_DATABASE=blueprint
|
||||||
|
BLUEPRINT_DB_USERNAME=blueprint_user
|
||||||
|
BLUEPRINT_DB_PASSWORD=your_secure_password
|
||||||
|
BLUEPRINT_DB_ROOT_PASSWORD=your_secure_root_password
|
||||||
|
BLUEPRINT_DB_MAX_IDLE_CONNS=10
|
||||||
|
BLUEPRINT_DB_MAX_OPEN_CONNS=100
|
||||||
|
BLUEPRINT_DB_CONN_MAX_LIFETIME=1h
|
||||||
|
|
||||||
|
# Logger Configuration
|
||||||
|
LOGGER_LOG_LEVEL=info
|
||||||
|
LOGGER_FILE_LOG_NAME=logs/app.log
|
||||||
|
LOGGER_MAX_SIZE=1000
|
||||||
|
LOGGER_MAX_BACKUPS=7
|
||||||
|
LOGGER_MAX_AGE=30
|
||||||
|
LOGGER_COMPRESS=true
|
||||||
|
|
||||||
|
# Monitoring Configuration
|
||||||
|
GRAFANA_ADMIN_PASSWORD=admin
|
||||||
|
PROMETHEUS_RETENTION_TIME=15d
|
||||||
|
PROMETHEUS_SCRAPE_INTERVAL=15s
|
||||||
|
|
||||||
|
# Backup Configuration
|
||||||
|
BACKUP_RETENTION_DAYS=7
|
||||||
|
BACKUP_SCHEDULE="0 2 * * *"
|
||||||
|
|
||||||
|
# SSL Configuration
|
||||||
|
SSL_CERT_PATH=/etc/nginx/ssl/cert.pem
|
||||||
|
SSL_KEY_PATH=/etc/nginx/ssl/key.pem
|
||||||
|
|
||||||
|
# Network Configuration
|
||||||
|
NGINX_PORT=80
|
||||||
|
NGINX_SSL_PORT=443
|
||||||
|
GRAFANA_PORT=3000
|
||||||
|
PROMETHEUS_PORT=9090
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -32,3 +32,4 @@ main
|
|||||||
# OS X generated file
|
# OS X generated file
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
./logs/app.log
|
||||||
|
18
Dockerfile
18
Dockerfile
@ -1,18 +0,0 @@
|
|||||||
FROM golang:1.23-alpine AS build
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN go build -o main cmd/api/main.go
|
|
||||||
|
|
||||||
FROM alpine:3.20.1 AS prod
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build /app/main /app/main
|
|
||||||
EXPOSE ${PORT}
|
|
||||||
CMD ["./main"]
|
|
||||||
|
|
||||||
|
|
46
Dockerfile.prod
Normal file
46
Dockerfile.prod
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM golang:1.23.3-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache git make
|
||||||
|
|
||||||
|
# Copy go mod and sum files
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o main ./cmd/api
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy binary from builder
|
||||||
|
COPY --from=builder /app/main .
|
||||||
|
|
||||||
|
# Copy SSL certificates
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
RUN mkdir -p /app/logs
|
||||||
|
|
||||||
|
# Copy environment file
|
||||||
|
COPY .env.prod .env
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PORT=8080
|
||||||
|
ENV TZ=Asia/Ho_Chi_Minh
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["./main"]
|
524
README.md
524
README.md
@ -1,410 +1,220 @@
|
|||||||
# Senflow Server
|
# Senflow App
|
||||||
|
|
||||||
Ứng dụng backend được viết bằng Go, sử dụng MySQL làm cơ sở dữ liệu và Google Wire cho dependency injection.
|
Ứng dụng Go với Docker cho môi trường development và production.
|
||||||
|
|
||||||
## Yêu cầu hệ thống
|
## Cấu Trúc Thư Mục
|
||||||
|
|
||||||
- Go 1.21 hoặc cao hơn
|
|
||||||
- Docker và Docker Compose
|
|
||||||
- Make
|
|
||||||
|
|
||||||
## Cấu trúc dự án chi tiết
|
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── cmd/ # Điểm vào ứng dụng
|
├── Dockerfile.dev
|
||||||
│ └── api/ # API server
|
├── Dockerfile.prod
|
||||||
│ └── main.go # File main khởi động ứng dụng
|
├── docker-compose.dev.yml
|
||||||
├── global/ # Biến toàn cục và cấu trúc cấu hình
|
├── docker-compose.prod.yml
|
||||||
│ └── global.go # Định nghĩa các biến và cấu trúc toàn cục
|
├── .env.dev
|
||||||
├── internal/ # Mã nguồn nội bộ
|
├── .env.prod
|
||||||
│ ├── controllers/ # Xử lý request và response
|
├── nginx/
|
||||||
│ │ └── user_controller.go # Controller xử lý các request liên quan đến user
|
│ └── conf.d/
|
||||||
│ ├── initialize/ # Khởi tạo các thành phần của ứng dụng
|
│ └── default.conf
|
||||||
│ │ ├── loadconfig.go # Đọc cấu hình từ file .env
|
├── prometheus/
|
||||||
│ │ ├── logger.go # Khởi tạo logger
|
│ └── prometheus.yml
|
||||||
│ │ ├── mysql.go # Khởi tạo kết nối MySQL
|
└── scripts/
|
||||||
│ │ └── run.go # Điểm khởi động chính của ứng dụng
|
└── backup.sh
|
||||||
│ ├── models/ # Định nghĩa các model dữ liệu
|
|
||||||
│ │ └── user.go # Model User
|
|
||||||
│ ├── repositories/ # Tương tác với cơ sở dữ liệu
|
|
||||||
│ │ └── user_repository.go # Repository xử lý dữ liệu user
|
|
||||||
│ ├── routers/ # Định nghĩa các router
|
|
||||||
│ │ ├── router_group.go # Nhóm các router
|
|
||||||
│ │ └── user/ # Router liên quan đến user
|
|
||||||
│ │ ├── router_group.go # Nhóm các router user
|
|
||||||
│ │ └── user_router.go # Định nghĩa các endpoint user
|
|
||||||
│ ├── services/ # Xử lý logic nghiệp vụ
|
|
||||||
│ │ └── user_service.go # Service xử lý logic liên quan đến user
|
|
||||||
│ └── wire/ # Dependency injection với Google Wire
|
|
||||||
│ ├── injector.go # Định nghĩa các injector
|
|
||||||
│ ├── wire.go # Định nghĩa các provider
|
|
||||||
│ └── wire_gen.go # File được tạo tự động bởi Wire
|
|
||||||
├── logs/ # Thư mục chứa log
|
|
||||||
├── .air.toml # Cấu hình cho Air (hot-reload)
|
|
||||||
├── .env # Biến môi trường
|
|
||||||
├── Dockerfile # Cấu hình Docker cho môi trường production
|
|
||||||
├── Dockerfile.dev # Cấu hình Docker cho môi trường phát triển
|
|
||||||
├── Makefile # Các lệnh make
|
|
||||||
└── docker-compose.yml, docker-compose.dev.yml # Cấu hình Docker Compose
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quy tắc đặt tên file
|
## Yêu Cầu Hệ Thống
|
||||||
|
|
||||||
Dự án tuân theo các quy tắc đặt tên file sau để đảm bảo tính nhất quán:
|
- Docker
|
||||||
|
- Docker Compose
|
||||||
|
- Go 1.21 trở lên
|
||||||
|
|
||||||
1. **Sử dụng dấu gạch dưới (_) để phân tách các từ** trong tên file, ví dụ: `user_controller.go`, `user_repository.go`.
|
## Môi Trường Development
|
||||||
|
|
||||||
2. **Tên file mô tả rõ chức năng** của file đó:
|
### Cấu Hình
|
||||||
- Controllers: `<entity>_controller.go` (ví dụ: `user_controller.go`)
|
|
||||||
- Services: `<entity>_service.go` (ví dụ: `user_service.go`)
|
|
||||||
- Repositories: `<entity>_repository.go` (ví dụ: `user_repository.go`)
|
|
||||||
- Routers: `<entity>_router.go` (ví dụ: `user_router.go`)
|
|
||||||
- Models: `<entity>.go` (ví dụ: `user.go`)
|
|
||||||
|
|
||||||
3. **File khởi tạo và cấu hình** sử dụng tên mô tả chức năng:
|
1. Tạo file `.env.dev`:
|
||||||
- `loadconfig.go`: Đọc cấu hình
|
```env
|
||||||
- `logger.go`: Khởi tạo logger
|
PORT=8080
|
||||||
- `mysql.go`: Khởi tạo kết nối MySQL
|
APP_ENV=development
|
||||||
- `run.go`: Điểm khởi động ứng dụng
|
BLUEPRINT_DB_HOST=mysql_bp
|
||||||
|
BLUEPRINT_DB_PORT=3306
|
||||||
4. **File Wire** tuân theo quy ước của Google Wire:
|
BLUEPRINT_DB_DATABASE=blueprint
|
||||||
- `wire.go`: Định nghĩa các provider
|
BLUEPRINT_DB_USERNAME=root
|
||||||
- `injector.go`: Định nghĩa các injector
|
BLUEPRINT_DB_PASSWORD=root
|
||||||
- `wire_gen.go`: File được tạo tự động bởi Wire
|
BLUEPRINT_DB_ROOT_PASSWORD=root
|
||||||
|
BLUEPRINT_DB_MAX_IDLE_CONNS=10
|
||||||
## Kiến trúc ứng dụng
|
BLUEPRINT_DB_MAX_OPEN_CONNS=100
|
||||||
|
BLUEPRINT_DB_CONN_MAX_LIFETIME=1h
|
||||||
Ứng dụng được tổ chức theo kiến trúc phân lớp:
|
LOGGER_LOG_LEVEL=debug
|
||||||
|
LOGGER_FILE_LOG_NAME=logs/app.log
|
||||||
1. **Controllers**: Xử lý request và response, gọi các service để thực hiện logic nghiệp vụ.
|
LOGGER_MAX_SIZE=100
|
||||||
2. **Services**: Chứa logic nghiệp vụ, gọi các repository để tương tác với dữ liệu.
|
LOGGER_MAX_BACKUPS=3
|
||||||
3. **Repositories**: Tương tác trực tiếp với cơ sở dữ liệu, thực hiện các thao tác CRUD.
|
LOGGER_MAX_AGE=28
|
||||||
4. **Models**: Định nghĩa cấu trúc dữ liệu.
|
LOGGER_COMPRESS=true
|
||||||
5. **Routers**: Định nghĩa các endpoint API và kết nối với controllers.
|
|
||||||
6. **Wire**: Quản lý dependency injection, kết nối các thành phần lại với nhau.
|
|
||||||
|
|
||||||
## Dependency Injection với Google Wire
|
|
||||||
|
|
||||||
Dự án sử dụng Google Wire để quản lý dependency injection. Các thành phần chính:
|
|
||||||
|
|
||||||
1. **Provider**: Định nghĩa cách tạo các dependency (trong `wire.go`).
|
|
||||||
2. **Injector**: Định nghĩa cách kết nối các dependency (trong `injector.go`).
|
|
||||||
3. **Wire Gen**: File được tạo tự động bởi Wire, chứa code khởi tạo dependency (trong `wire_gen.go`).
|
|
||||||
|
|
||||||
Để cập nhật file `wire_gen.go` sau khi thay đổi các provider hoặc injector:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd internal/wire
|
|
||||||
go run github.com/google/wire/cmd/wire
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Môi trường phát triển (Development)
|
### Chạy Ứng Dụng
|
||||||
|
|
||||||
### Phương pháp 1: Sử dụng Docker Compose cho toàn bộ stack
|
|
||||||
|
|
||||||
Phương pháp này sử dụng Docker Compose để chạy cả ứng dụng Go và MySQL, với hot-reload được hỗ trợ bởi Air.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Khởi động môi trường phát triển
|
# Build và chạy
|
||||||
|
docker-compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
|
# Chạy ở chế độ detached
|
||||||
docker-compose -f docker-compose.dev.yml up -d
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
# Xem logs
|
# Xem logs
|
||||||
docker-compose -f docker-compose.dev.yml logs -f
|
docker-compose -f docker-compose.dev.yml logs -f
|
||||||
|
|
||||||
# Dừng môi trường phát triển
|
# Dừng ứng dụng
|
||||||
docker-compose -f docker-compose.dev.yml down
|
docker-compose -f docker-compose.dev.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
### Phương pháp 2: Chạy ứng dụng Go trực tiếp, MySQL trong Docker
|
## Môi Trường Production
|
||||||
|
|
||||||
```bash
|
### Cấu Hình
|
||||||
# Khởi động MySQL trong Docker
|
|
||||||
make docker-run
|
|
||||||
|
|
||||||
# Chạy ứng dụng với hot-reload
|
1. Tạo file `.env.prod`:
|
||||||
make watch
|
```env
|
||||||
|
|
||||||
# Hoặc chạy ứng dụng thông thường
|
|
||||||
make run
|
|
||||||
|
|
||||||
# Dừng MySQL
|
|
||||||
make docker-down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Môi trường sản xuất (Production)
|
|
||||||
|
|
||||||
### Phương pháp 1: Sử dụng Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Khởi động môi trường sản xuất
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Xem logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Dừng môi trường sản xuất
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phương pháp 2: Triển khai thủ công
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build ứng dụng
|
|
||||||
make build
|
|
||||||
|
|
||||||
# Hoặc build với Go trực tiếp
|
|
||||||
go build -o main cmd/api/main.go
|
|
||||||
|
|
||||||
# Chạy ứng dụng
|
|
||||||
./main
|
|
||||||
```
|
|
||||||
|
|
||||||
## Các lệnh Make hữu ích
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build ứng dụng
|
|
||||||
make build
|
|
||||||
|
|
||||||
# Chạy ứng dụng
|
|
||||||
make run
|
|
||||||
|
|
||||||
# Chạy tests
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Chạy integration tests
|
|
||||||
make itest
|
|
||||||
|
|
||||||
# Dọn dẹp binary
|
|
||||||
make clean
|
|
||||||
|
|
||||||
# Hot-reload trong quá trình phát triển
|
|
||||||
make watch
|
|
||||||
|
|
||||||
# Khởi động container MySQL
|
|
||||||
make docker-run
|
|
||||||
|
|
||||||
# Dừng container MySQL
|
|
||||||
make docker-down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Biến môi trường
|
|
||||||
|
|
||||||
Sao chép file `.env.example` thành `.env` và điều chỉnh các giá trị theo nhu cầu của bạn:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Cấu hình ứng dụng
|
|
||||||
APP_ENV=development # development hoặc production
|
|
||||||
PORT=8080
|
PORT=8080
|
||||||
|
APP_ENV=production
|
||||||
# Cấu hình database
|
|
||||||
BLUEPRINT_DB_HOST=mysql_bp
|
BLUEPRINT_DB_HOST=mysql_bp
|
||||||
BLUEPRINT_DB_PORT=3306
|
BLUEPRINT_DB_PORT=3306
|
||||||
BLUEPRINT_DB_DATABASE=blueprint
|
BLUEPRINT_DB_DATABASE=blueprint
|
||||||
BLUEPRINT_DB_USERNAME=user
|
BLUEPRINT_DB_USERNAME=root
|
||||||
BLUEPRINT_DB_PASSWORD=password
|
BLUEPRINT_DB_PASSWORD=your_secure_password
|
||||||
BLUEPRINT_DB_ROOT_PASSWORD=root_password
|
BLUEPRINT_DB_ROOT_PASSWORD=your_secure_root_password
|
||||||
BLUEPRINT_DB_MAX_IDLE_CONNS=10
|
BLUEPRINT_DB_MAX_IDLE_CONNS=10
|
||||||
BLUEPRINT_DB_MAX_OPEN_CONNS=100
|
BLUEPRINT_DB_MAX_OPEN_CONNS=100
|
||||||
BLUEPRINT_DB_CONN_MAX_LIFETIME=3600
|
BLUEPRINT_DB_CONN_MAX_LIFETIME=1h
|
||||||
|
LOGGER_LOG_LEVEL=info
|
||||||
# Cấu hình logger
|
LOGGER_FILE_LOG_NAME=logs/app.log
|
||||||
LOGGER_LOG_LEVEL=debug
|
LOGGER_MAX_SIZE=1000
|
||||||
LOGGER_FILE_LOG_NAME=./logs/app.log
|
LOGGER_MAX_BACKUPS=7
|
||||||
LOGGER_MAX_SIZE=10
|
|
||||||
LOGGER_MAX_BACKUPS=5
|
|
||||||
LOGGER_MAX_AGE=30
|
LOGGER_MAX_AGE=30
|
||||||
LOGGER_COMPRESS=true
|
LOGGER_COMPRESS=true
|
||||||
|
GRAFANA_ADMIN_PASSWORD=your_secure_grafana_password
|
||||||
```
|
```
|
||||||
|
|
||||||
## Graceful Shutdown
|
### Tính Năng Production
|
||||||
|
|
||||||
Ứng dụng hỗ trợ graceful shutdown để đảm bảo tất cả các request đang xử lý được hoàn thành trước khi ứng dụng dừng lại. Khi nhận tín hiệu SIGINT hoặc SIGTERM (ví dụ: khi nhấn Ctrl+C), ứng dụng sẽ:
|
1. **Load Balancing**
|
||||||
|
- Nginx làm load balancer
|
||||||
|
- 3 instance của ứng dụng
|
||||||
|
- Health check tự động
|
||||||
|
- SSL/TLS support
|
||||||
|
|
||||||
1. Dừng nhận request mới
|
2. **Monitoring**
|
||||||
2. Đợi các request đang xử lý hoàn thành (tối đa 5 giây)
|
- Prometheus cho metrics collection
|
||||||
3. Đóng kết nối đến cơ sở dữ liệu
|
- Grafana cho visualization
|
||||||
4. Hiển thị thông báo "Graceful shutdown complete."
|
- Metrics từ app và MySQL
|
||||||
|
- Persistent storage cho dữ liệu monitoring
|
||||||
|
|
||||||
## Thêm tính năng mới
|
3. **Backup**
|
||||||
|
- Backup MySQL tự động hàng ngày
|
||||||
|
- Nén backup với gzip
|
||||||
|
- Giữ backup trong 7 ngày
|
||||||
|
- Volume riêng cho backups
|
||||||
|
|
||||||
Để thêm một tính năng mới vào ứng dụng, bạn cần:
|
4. **High Availability**
|
||||||
|
- Health checks cho tất cả services
|
||||||
|
- Auto-restart policy
|
||||||
|
- Network isolation
|
||||||
|
- Replica management
|
||||||
|
|
||||||
1. Tạo model mới trong thư mục `internal/models`
|
### Chạy Production
|
||||||
2. Tạo repository mới trong thư mục `internal/repositories`
|
|
||||||
3. Tạo service mới trong thư mục `internal/services`
|
|
||||||
4. Tạo controller mới trong thư mục `internal/controllers`
|
|
||||||
5. Tạo router mới trong thư mục `internal/routers`
|
|
||||||
6. Cập nhật các provider trong `internal/wire/wire.go`
|
|
||||||
7. Cập nhật injector trong `internal/wire/injector.go`
|
|
||||||
8. Chạy lệnh wire để cập nhật `wire_gen.go`
|
|
||||||
9. Cập nhật `internal/initialize/run.go` để khởi tạo router mới
|
|
||||||
|
|
||||||
## Đóng góp
|
|
||||||
|
|
||||||
Vui lòng đảm bảo code của bạn tuân theo các quy tắc đặt tên và cấu trúc dự án đã được mô tả ở trên. Sử dụng `gofmt` để định dạng code trước khi commit.
|
|
||||||
|
|
||||||
## Chức năng của các thư mục chính
|
|
||||||
|
|
||||||
### cmd
|
|
||||||
Chứa điểm vào của ứng dụng. Thư mục `api` chứa file `main.go` - nơi khởi động ứng dụng.
|
|
||||||
|
|
||||||
### global
|
|
||||||
Chứa các biến và cấu hình toàn cục được sử dụng trong toàn bộ ứng dụng:
|
|
||||||
- `global.go`: Định nghĩa các biến toàn cục như DB, Logger
|
|
||||||
- `config.go`: Định nghĩa cấu trúc cấu hình ứng dụng
|
|
||||||
|
|
||||||
### internal
|
|
||||||
Chứa mã nguồn nội bộ của ứng dụng, được tổ chức theo kiến trúc phân lớp:
|
|
||||||
|
|
||||||
#### controllers
|
|
||||||
Xử lý HTTP request và response, gọi đến services để thực hiện logic nghiệp vụ:
|
|
||||||
- `interfaces.go`: Định nghĩa các interface cho controllers
|
|
||||||
- `controllers.go`: Struct chứa tất cả controllers để sử dụng với dependency injection
|
|
||||||
- Các file controller cụ thể: `user_controller.go`, `product_controller.go`, ...
|
|
||||||
|
|
||||||
#### services
|
|
||||||
Chứa logic nghiệp vụ của ứng dụng:
|
|
||||||
- `interfaces.go`: Định nghĩa các interface cho services
|
|
||||||
- Các file service cụ thể: `user_service.go`, `product_service.go`, ...
|
|
||||||
|
|
||||||
#### repositories
|
|
||||||
Tương tác với cơ sở dữ liệu, thực hiện các thao tác CRUD:
|
|
||||||
- `interfaces.go`: Định nghĩa các interface cho repositories
|
|
||||||
- Các file repository cụ thể: `user_repository.go`, `product_repository.go`, ...
|
|
||||||
|
|
||||||
#### models
|
|
||||||
Định nghĩa cấu trúc dữ liệu và các phương thức liên quan:
|
|
||||||
- Các file model cụ thể: `user.go`, `product.go`, ...
|
|
||||||
|
|
||||||
#### routers
|
|
||||||
Định nghĩa các routes của ứng dụng:
|
|
||||||
- `router_group.go`: Khởi tạo tất cả routes
|
|
||||||
- Các thư mục route cụ thể: `user/`, `product/`, ...
|
|
||||||
|
|
||||||
#### middleware
|
|
||||||
Chứa các middleware được sử dụng trong ứng dụng như authentication, logging, ...
|
|
||||||
|
|
||||||
#### initialize
|
|
||||||
Khởi tạo các thành phần của ứng dụng:
|
|
||||||
- `loadconfig.go`: Tải cấu hình từ file .env
|
|
||||||
- `logger.go`: Khởi tạo logger
|
|
||||||
- `mysql.go`: Khởi tạo kết nối MySQL
|
|
||||||
- `run.go`: Khởi động ứng dụng
|
|
||||||
|
|
||||||
#### wire
|
|
||||||
Quản lý dependency injection với Google Wire:
|
|
||||||
- `wire.go`: Định nghĩa các providers
|
|
||||||
- `injector.go`: Định nghĩa các injectors
|
|
||||||
- `wire_gen.go`: File được tạo tự động bởi Wire
|
|
||||||
|
|
||||||
### pkg
|
|
||||||
Chứa các thư viện và tiện ích có thể được sử dụng bởi các ứng dụng khác:
|
|
||||||
- `utils/`: Các hàm tiện ích
|
|
||||||
|
|
||||||
### configs
|
|
||||||
Chứa các file cấu hình của ứng dụng.
|
|
||||||
|
|
||||||
## Kiến trúc ứng dụng
|
|
||||||
|
|
||||||
Ứng dụng được thiết kế theo kiến trúc phân lớp:
|
|
||||||
|
|
||||||
1. **Controller Layer**: Xử lý HTTP request và response
|
|
||||||
2. **Service Layer**: Xử lý logic nghiệp vụ
|
|
||||||
3. **Repository Layer**: Tương tác với cơ sở dữ liệu
|
|
||||||
4. **Model Layer**: Định nghĩa cấu trúc dữ liệu
|
|
||||||
|
|
||||||
Mỗi lớp chỉ giao tiếp với lớp liền kề, giúp giảm sự phụ thuộc và dễ dàng thay đổi implementation mà không ảnh hưởng đến các lớp khác.
|
|
||||||
|
|
||||||
## Dependency Injection với Google Wire
|
|
||||||
|
|
||||||
Ứng dụng sử dụng Google Wire để quản lý dependency injection. Các thành phần được định nghĩa trong `wire.go` và được kết nối tự động bởi Wire.
|
|
||||||
|
|
||||||
Cấu trúc Wire:
|
|
||||||
- `RepositorySet`: Providers cho repositories
|
|
||||||
- `ServiceSet`: Providers cho services
|
|
||||||
- `ControllerSet`: Providers cho controllers
|
|
||||||
- `AppSet`: Tập hợp tất cả providers
|
|
||||||
|
|
||||||
Khi thêm một thành phần mới (ví dụ: Product, Gift), bạn cần:
|
|
||||||
1. Tạo model, repository, service, controller tương ứng
|
|
||||||
2. Thêm interface vào các file `interfaces.go`
|
|
||||||
3. Cập nhật struct `Controllers` trong `controllers.go`
|
|
||||||
4. Thêm providers vào `wire.go`
|
|
||||||
5. Chạy lệnh wire để cập nhật `wire_gen.go`
|
|
||||||
|
|
||||||
## Môi trường phát triển
|
|
||||||
|
|
||||||
### Yêu cầu
|
|
||||||
- Go 1.16+
|
|
||||||
- MySQL 8.0+
|
|
||||||
- Docker và Docker Compose (tùy chọn)
|
|
||||||
|
|
||||||
### Cài đặt và chạy
|
|
||||||
1. Clone repository:
|
|
||||||
```
|
|
||||||
git clone https://github.com/dungnt11/senflow_app.git
|
|
||||||
cd senflow_app
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Cài đặt dependencies:
|
|
||||||
```
|
|
||||||
go mod download
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Tạo file .env từ .env.example:
|
|
||||||
```
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Chạy ứng dụng:
|
|
||||||
```
|
|
||||||
go run cmd/api/main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sử dụng Docker
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
# Build và chạy
|
||||||
|
docker-compose -f docker-compose.prod.yml --env-file .env.prod up --build -d
|
||||||
|
|
||||||
|
# Xem logs
|
||||||
|
docker-compose -f docker-compose.prod.yml logs -f
|
||||||
|
|
||||||
|
# Dừng services
|
||||||
|
docker-compose -f docker-compose.prod.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Endpoints
|
### Endpoints
|
||||||
|
|
||||||
### User
|
- App instances: http://localhost:8083, http://localhost:8084, http://localhost:8085
|
||||||
- `POST /api/v1/users/register`: Đăng ký người dùng mới
|
- Nginx (Load Balancer): http://localhost:80
|
||||||
- `POST /api/v1/users/login`: Đăng nhập
|
- Grafana: http://localhost:3000
|
||||||
- `GET /api/v1/users/profile`: Lấy thông tin profile
|
- Prometheus: http://localhost:9090
|
||||||
- `PUT /api/v1/users/profile`: Cập nhật profile
|
|
||||||
|
|
||||||
### Product
|
## Bảo Mật
|
||||||
- `POST /api/v1/products`: Tạo sản phẩm mới
|
|
||||||
- `GET /api/v1/products`: Lấy tất cả sản phẩm
|
|
||||||
- `GET /api/v1/products/:id`: Lấy thông tin sản phẩm theo ID
|
|
||||||
- `PUT /api/v1/products/:id`: Cập nhật sản phẩm
|
|
||||||
- `DELETE /api/v1/products/:id`: Xóa sản phẩm
|
|
||||||
|
|
||||||
## Quy ước đặt tên
|
1. **SSL/TLS**
|
||||||
|
- Thêm certificates vào `nginx/ssl/`
|
||||||
|
- Cấu hình SSL trong Nginx
|
||||||
|
|
||||||
### Files
|
2. **Mật Khẩu**
|
||||||
- Controllers: `<entity>_controller.go`
|
- Thay đổi mật khẩu trong `.env.prod`
|
||||||
- Services: `<entity>_service.go`
|
- Sử dụng mật khẩu mạnh
|
||||||
- Repositories: `<entity>_repository.go`
|
- Không commit file .env
|
||||||
- Models: `<entity>.go`
|
|
||||||
- Routers: `<entity>_router.go`
|
|
||||||
|
|
||||||
### Interfaces
|
3. **Network**
|
||||||
- Controllers: `I<Entity>Controller`
|
- Cấu hình firewall
|
||||||
- Services: `I<Entity>Service`
|
- Giới hạn ports
|
||||||
- Repositories: `I<Entity>Repository`
|
- Sử dụng internal networks
|
||||||
|
|
||||||
## Đóng góp
|
## Monitoring
|
||||||
|
|
||||||
1. Fork repository
|
1. **Grafana Dashboards**
|
||||||
2. Tạo branch mới: `git checkout -b feature/your-feature-name`
|
- App metrics
|
||||||
3. Commit changes: `git commit -m 'Add some feature'`
|
- MySQL metrics
|
||||||
4. Push to branch: `git push origin feature/your-feature-name`
|
- System metrics
|
||||||
5. Submit pull request
|
|
||||||
|
|
||||||
## License
|
2. **Alerts**
|
||||||
|
- Cấu hình trong Prometheus
|
||||||
|
- Thông báo qua email/Slack
|
||||||
|
|
||||||
[MIT](LICENSE)
|
## Backup & Recovery
|
||||||
|
|
||||||
|
1. **Backup**
|
||||||
|
- Tự động hàng ngày
|
||||||
|
- Nén với gzip
|
||||||
|
- Giữ 7 ngày
|
||||||
|
|
||||||
|
2. **Recovery**
|
||||||
|
```bash
|
||||||
|
# Restore từ backup
|
||||||
|
gunzip -c backup_YYYYMMDD_HHMMSS.sql.gz | mysql -h mysql_bp -u root -p blueprint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
1. **Logs**
|
||||||
|
```bash
|
||||||
|
# App logs
|
||||||
|
docker-compose -f docker-compose.prod.yml logs app
|
||||||
|
|
||||||
|
# MySQL logs
|
||||||
|
docker-compose -f docker-compose.prod.yml logs mysql_bp
|
||||||
|
|
||||||
|
# Nginx logs
|
||||||
|
docker-compose -f docker-compose.prod.yml logs nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Health Checks**
|
||||||
|
- App: http://localhost:8080/health
|
||||||
|
- MySQL: docker-compose exec mysql_bp mysqladmin ping
|
||||||
|
- Nginx: http://localhost/health
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
1. **Update**
|
||||||
|
```bash
|
||||||
|
# Pull latest images
|
||||||
|
docker-compose -f docker-compose.prod.yml pull
|
||||||
|
|
||||||
|
# Rebuild và restart
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Cleanup**
|
||||||
|
```bash
|
||||||
|
# Xóa unused volumes
|
||||||
|
docker volume prune
|
||||||
|
|
||||||
|
# Xóa old images
|
||||||
|
docker image prune
|
||||||
|
```
|
@ -7,13 +7,22 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- ${PORT}:${PORT}
|
- ${PORT}:${PORT}
|
||||||
environment:
|
environment:
|
||||||
APP_ENV: local
|
APP_ENV: ${APP_ENV}
|
||||||
PORT: ${PORT}
|
PORT: ${PORT}
|
||||||
BLUEPRINT_DB_HOST: ${BLUEPRINT_DB_HOST}
|
BLUEPRINT_DB_HOST: mysql_bp
|
||||||
BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT}
|
BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT}
|
||||||
BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE}
|
BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE}
|
||||||
BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME}
|
BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME}
|
||||||
BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD}
|
BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD}
|
||||||
|
BLUEPRINT_DB_MAX_IDLE_CONNS: ${BLUEPRINT_DB_MAX_IDLE_CONNS}
|
||||||
|
BLUEPRINT_DB_MAX_OPEN_CONNS: ${BLUEPRINT_DB_MAX_OPEN_CONNS}
|
||||||
|
BLUEPRINT_DB_CONN_MAX_LIFETIME: ${BLUEPRINT_DB_CONN_MAX_LIFETIME}
|
||||||
|
LOGGER_LOG_LEVEL: ${LOGGER_LOG_LEVEL}
|
||||||
|
LOGGER_FILE_LOG_NAME: ${LOGGER_FILE_LOG_NAME}
|
||||||
|
LOGGER_MAX_SIZE: ${LOGGER_MAX_SIZE}
|
||||||
|
LOGGER_MAX_BACKUPS: ${LOGGER_MAX_BACKUPS}
|
||||||
|
LOGGER_MAX_AGE: ${LOGGER_MAX_AGE}
|
||||||
|
LOGGER_COMPRESS: ${LOGGER_COMPRESS}
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app # Mount current directory to /app in container
|
- .:/app # Mount current directory to /app in container
|
||||||
command: air # Use air for hot-reload
|
command: air # Use air for hot-reload
|
||||||
@ -24,6 +33,7 @@ services:
|
|||||||
- blueprint
|
- blueprint
|
||||||
mysql_bp:
|
mysql_bp:
|
||||||
image: mysql:latest
|
image: mysql:latest
|
||||||
|
platform: linux/arm64
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: ${BLUEPRINT_DB_DATABASE}
|
MYSQL_DATABASE: ${BLUEPRINT_DB_DATABASE}
|
||||||
@ -35,11 +45,26 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- mysql_volume_bp:/var/lib/mysql
|
- mysql_volume_bp:/var/lib/mysql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "${BLUEPRINT_DB_HOST}", "-u", "${BLUEPRINT_DB_USERNAME}", "--password=${BLUEPRINT_DB_PASSWORD}"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${BLUEPRINT_DB_USERNAME}", "--password=${BLUEPRINT_DB_PASSWORD}", "--silent"]
|
||||||
interval: 5s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 5
|
||||||
start_period: 15s
|
start_period: 40s
|
||||||
|
networks:
|
||||||
|
- blueprint
|
||||||
|
|
||||||
|
phpmyadmin:
|
||||||
|
image: arm64v8/phpmyadmin:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8081:80"
|
||||||
|
environment:
|
||||||
|
PMA_HOST: mysql_bp
|
||||||
|
PMA_PORT: 3306
|
||||||
|
MYSQL_ROOT_PASSWORD: ${BLUEPRINT_DB_ROOT_PASSWORD}
|
||||||
|
depends_on:
|
||||||
|
mysql_bp:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- blueprint
|
- blueprint
|
||||||
|
|
||||||
|
162
docker-compose.prod.yml
Normal file
162
docker-compose.prod.yml
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8083-8085:${PORT}"
|
||||||
|
environment:
|
||||||
|
APP_ENV: ${APP_ENV}
|
||||||
|
PORT: ${PORT}
|
||||||
|
GIN_MODE: release
|
||||||
|
BLUEPRINT_DB_HOST: mysql_bp
|
||||||
|
BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT}
|
||||||
|
BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE}
|
||||||
|
BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME}
|
||||||
|
BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD}
|
||||||
|
BLUEPRINT_DB_MAX_IDLE_CONNS: ${BLUEPRINT_DB_MAX_IDLE_CONNS}
|
||||||
|
BLUEPRINT_DB_MAX_OPEN_CONNS: ${BLUEPRINT_DB_MAX_OPEN_CONNS}
|
||||||
|
BLUEPRINT_DB_CONN_MAX_LIFETIME: ${BLUEPRINT_DB_CONN_MAX_LIFETIME}
|
||||||
|
LOGGER_LOG_LEVEL: ${LOGGER_LOG_LEVEL}
|
||||||
|
LOGGER_FILE_LOG_NAME: ${LOGGER_FILE_LOG_NAME}
|
||||||
|
LOGGER_MAX_SIZE: ${LOGGER_MAX_SIZE}
|
||||||
|
LOGGER_MAX_BACKUPS: ${LOGGER_MAX_BACKUPS}
|
||||||
|
LOGGER_MAX_AGE: ${LOGGER_MAX_AGE}
|
||||||
|
LOGGER_COMPRESS: ${LOGGER_COMPRESS}
|
||||||
|
depends_on:
|
||||||
|
mysql_bp:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- blueprint
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
delay: 10s
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:${PORT}/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
mysql_bp:
|
||||||
|
image: mysql:8.0
|
||||||
|
platform: linux/arm64
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: ${BLUEPRINT_DB_DATABASE}
|
||||||
|
MYSQL_USER: blueprint_user
|
||||||
|
MYSQL_PASSWORD: ${BLUEPRINT_DB_PASSWORD}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${BLUEPRINT_DB_ROOT_PASSWORD}
|
||||||
|
MYSQL_ALLOW_EMPTY_PASSWORD: "no"
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- mysql_volume_bp:/var/lib/mysql
|
||||||
|
- mysql_backup:/backup
|
||||||
|
command: >
|
||||||
|
--default-authentication-plugin=mysql_native_password
|
||||||
|
--character-set-server=utf8mb4
|
||||||
|
--collation-server=utf8mb4_unicode_ci
|
||||||
|
--skip-name-resolve
|
||||||
|
--explicit_defaults_for_timestamp=1
|
||||||
|
--max_connections=1000
|
||||||
|
--innodb_buffer_pool_size=256M
|
||||||
|
--innodb_log_buffer_size=16M
|
||||||
|
--innodb_log_file_size=256M
|
||||||
|
--innodb_write_io_threads=8
|
||||||
|
--innodb_read_io_threads=8
|
||||||
|
--innodb_thread_concurrency=0
|
||||||
|
--innodb_flush_log_at_trx_commit=2
|
||||||
|
--innodb_flush_method=O_DIRECT
|
||||||
|
--innodb_file_per_table=1
|
||||||
|
--bind-address=0.0.0.0
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${BLUEPRINT_DB_ROOT_PASSWORD}", "--connect-timeout=5"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 180s
|
||||||
|
networks:
|
||||||
|
- blueprint
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
platform: linux/arm64
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/conf.d:/etc/nginx/conf.d
|
||||||
|
- ./nginx/ssl:/etc/nginx/ssl
|
||||||
|
depends_on:
|
||||||
|
- app
|
||||||
|
networks:
|
||||||
|
- blueprint
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
platform: linux/arm64
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
volumes:
|
||||||
|
- ./prometheus:/etc/prometheus
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
command:
|
||||||
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
|
- '--storage.tsdb.path=/prometheus'
|
||||||
|
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||||
|
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||||
|
networks:
|
||||||
|
- blueprint
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
platform: linux/arm64
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
|
||||||
|
depends_on:
|
||||||
|
- prometheus
|
||||||
|
networks:
|
||||||
|
- blueprint
|
||||||
|
|
||||||
|
mysql-backup:
|
||||||
|
image: mysql:8.0
|
||||||
|
platform: linux/arm64
|
||||||
|
volumes:
|
||||||
|
- mysql_backup:/backup
|
||||||
|
environment:
|
||||||
|
MYSQL_HOST: mysql_bp
|
||||||
|
MYSQL_USER: blueprint_user
|
||||||
|
MYSQL_PASSWORD: ${BLUEPRINT_DB_PASSWORD}
|
||||||
|
MYSQL_DATABASE: ${BLUEPRINT_DB_DATABASE}
|
||||||
|
entrypoint: |
|
||||||
|
/bin/sh -c '
|
||||||
|
while true; do
|
||||||
|
mysqldump -h$$MYSQL_HOST -u$$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE > /backup/backup-$$(date +%Y%m%d-%H%M%S).sql;
|
||||||
|
find /backup -type f -mtime +7 -delete;
|
||||||
|
sleep 86400;
|
||||||
|
done
|
||||||
|
'
|
||||||
|
networks:
|
||||||
|
- blueprint
|
||||||
|
|
||||||
|
networks:
|
||||||
|
blueprint:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_volume_bp:
|
||||||
|
mysql_backup:
|
||||||
|
prometheus_data:
|
||||||
|
grafana_data:
|
@ -1,47 +0,0 @@
|
|||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
target: prod
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- ${PORT}:${PORT}
|
|
||||||
environment:
|
|
||||||
APP_ENV: ${APP_ENV}
|
|
||||||
PORT: ${PORT}
|
|
||||||
BLUEPRINT_DB_HOST: ${BLUEPRINT_DB_HOST}
|
|
||||||
BLUEPRINT_DB_PORT: ${BLUEPRINT_DB_PORT}
|
|
||||||
BLUEPRINT_DB_DATABASE: ${BLUEPRINT_DB_DATABASE}
|
|
||||||
BLUEPRINT_DB_USERNAME: ${BLUEPRINT_DB_USERNAME}
|
|
||||||
BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_PASSWORD}
|
|
||||||
depends_on:
|
|
||||||
mysql_bp:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- blueprint
|
|
||||||
mysql_bp:
|
|
||||||
image: mysql:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
MYSQL_DATABASE: ${BLUEPRINT_DB_DATABASE}
|
|
||||||
MYSQL_USER: ${BLUEPRINT_DB_USERNAME}
|
|
||||||
MYSQL_PASSWORD: ${BLUEPRINT_DB_PASSWORD}
|
|
||||||
MYSQL_ROOT_PASSWORD: ${BLUEPRINT_DB_ROOT_PASSWORD}
|
|
||||||
ports:
|
|
||||||
- "${BLUEPRINT_DB_PORT}:3306"
|
|
||||||
volumes:
|
|
||||||
- mysql_volume_bp:/var/lib/mysql
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "${BLUEPRINT_DB_HOST}", "-u", "${BLUEPRINT_DB_USERNAME}", "--password=${BLUEPRINT_DB_PASSWORD}"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 15s
|
|
||||||
networks:
|
|
||||||
- blueprint
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mysql_volume_bp:
|
|
||||||
networks:
|
|
||||||
blueprint:
|
|
@ -3,11 +3,9 @@ package initialize
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dungnt11/senflow_app/global"
|
"github.com/dungnt11/senflow_app/global"
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -63,57 +61,5 @@ func LoadConfig() error {
|
|||||||
global.Config.Logger.Compress = strings.ToLower(compress) == "true"
|
global.Config.Logger.Compress = strings.ToLower(compress) == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
printConfig()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printConfig() {
|
|
||||||
// Tạo các đối tượng màu sắc
|
|
||||||
titleColor := color.New(color.FgHiCyan, color.Bold)
|
|
||||||
sectionColor := color.New(color.FgHiYellow, color.Bold)
|
|
||||||
keyColor := color.New(color.FgHiGreen)
|
|
||||||
valueColor := color.New(color.FgHiWhite)
|
|
||||||
|
|
||||||
fmt.Println() // Thêm dòng trống ở đầu
|
|
||||||
|
|
||||||
// In tiêu đề
|
|
||||||
titleColor.Println("✨✨✨ CẤU HÌNH ỨNG DỤNG ✨✨✨")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// Sử dụng reflection để tự động in tất cả các cấu hình
|
|
||||||
configValue := reflect.ValueOf(global.Config)
|
|
||||||
configType := configValue.Type()
|
|
||||||
|
|
||||||
// Duyệt qua tất cả các trường của cấu hình
|
|
||||||
for i := 0; i < configValue.NumField(); i++ {
|
|
||||||
sectionName := configType.Field(i).Name
|
|
||||||
sectionValue := configValue.Field(i)
|
|
||||||
sectionType := sectionValue.Type()
|
|
||||||
|
|
||||||
// In tên section
|
|
||||||
sectionColor.Printf("[%s]\n", strings.ToUpper(sectionName))
|
|
||||||
|
|
||||||
// Duyệt qua tất cả các trường của section
|
|
||||||
for j := 0; j < sectionValue.NumField(); j++ {
|
|
||||||
fieldName := sectionType.Field(j).Name
|
|
||||||
fieldValue := sectionValue.Field(j).Interface()
|
|
||||||
|
|
||||||
// Ẩn mật khẩu
|
|
||||||
displayValue := fmt.Sprintf("%v", fieldValue)
|
|
||||||
if strings.Contains(strings.ToLower(fieldName), "password") {
|
|
||||||
displayValue = "******** (ẩn)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// In tên trường và giá trị
|
|
||||||
keyColor.Printf(" %-15s: ", fieldName)
|
|
||||||
valueColor.Printf("%s\n", displayValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println() // Thêm dòng trống giữa các section
|
|
||||||
}
|
|
||||||
|
|
||||||
// In thông báo thành công
|
|
||||||
titleColor.Println("✅ Cấu hình đã được tải thành công!")
|
|
||||||
fmt.Println() // Thêm dòng trống ở cuối
|
|
||||||
}
|
|
||||||
|
@ -21,6 +21,7 @@ func checkErrorPanic(err error, errString string) {
|
|||||||
// InitMysql khởi tạo kết nối đến cơ sở dữ liệu MySQL và trả về đối tượng *gorm.DB.
|
// InitMysql khởi tạo kết nối đến cơ sở dữ liệu MySQL và trả về đối tượng *gorm.DB.
|
||||||
func InitMysql() *gorm.DB {
|
func InitMysql() *gorm.DB {
|
||||||
m := global.Config.Database
|
m := global.Config.Database
|
||||||
|
|
||||||
dsn := "%s:%s@tcp(%s:%v)/%s?charset=utf8mb4&parseTime=True&loc=Local"
|
dsn := "%s:%s@tcp(%s:%v)/%s?charset=utf8mb4&parseTime=True&loc=Local"
|
||||||
s := fmt.Sprintf(dsn, m.Username, m.Password, m.Host, m.Port, m.Database)
|
s := fmt.Sprintf(dsn, m.Username, m.Password, m.Host, m.Port, m.Database)
|
||||||
db, err := gorm.Open(mysql.Open(s), &gorm.Config{
|
db, err := gorm.Open(mysql.Open(s), &gorm.Config{
|
||||||
|
@ -39,7 +39,7 @@ func Run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Khởi tạo router
|
// Khởi tạo router
|
||||||
r := gin.Default()
|
r := provideRouter()
|
||||||
|
|
||||||
// Khởi tạo controllers thông qua wire
|
// Khởi tạo controllers thông qua wire
|
||||||
controllers, err := wire.InitializeControllers()
|
controllers, err := wire.InitializeControllers()
|
||||||
@ -80,3 +80,14 @@ func Run() {
|
|||||||
|
|
||||||
global.Logger.Info("Server đã tắt thành công.")
|
global.Logger.Info("Server đã tắt thành công.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func provideRouter() *gin.Engine {
|
||||||
|
if global.Config.Server.AppEnv == "local" {
|
||||||
|
gin.SetMode(gin.DebugMode)
|
||||||
|
gin.ForceConsoleColor()
|
||||||
|
return gin.Default()
|
||||||
|
} else {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
return gin.New()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
// User đại diện cho người dùng trong hệ thống
|
// User đại diện cho người dùng trong hệ thống
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
Username string `json:"username" gorm:"uniqueIndex;not null"`
|
||||||
Password string `json:"-" gorm:"not null"` // Không trả về password trong JSON
|
Password string `json:"-" gorm:"not null"` // Không trả về password trong JSON
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
@ -6,7 +6,7 @@ import "github.com/dungnt11/senflow_app/internal/models"
|
|||||||
type IUserRepository interface {
|
type IUserRepository interface {
|
||||||
Create(user *models.User) error
|
Create(user *models.User) error
|
||||||
FindByID(id uint) (*models.User, error)
|
FindByID(id uint) (*models.User, error)
|
||||||
FindByEmail(email string) (*models.User, error)
|
FindByUsername(username string) (*models.User, error)
|
||||||
Update(user *models.User) error
|
Update(user *models.User) error
|
||||||
Delete(id uint) error
|
Delete(id uint) error
|
||||||
}
|
}
|
||||||
|
@ -32,10 +32,10 @@ func (ur *UserRepository) FindByID(id uint) (*models.User, error) {
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindByEmail tìm user theo email
|
// FindByUsername tìm user theo username
|
||||||
func (ur *UserRepository) FindByEmail(email string) (*models.User, error) {
|
func (ur *UserRepository) FindByUsername(username string) (*models.User, error) {
|
||||||
var user models.User
|
var user models.User
|
||||||
err := ur.db.Where("email = ?", email).First(&user).Error
|
err := ur.db.Where("username = ?", username).First(&user).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -28,10 +28,10 @@ func (us *UserService) Register(c *gin.Context) (map[string]interface{}, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kiểm tra email đã tồn tại chưa
|
// Kiểm tra username đã tồn tại chưa
|
||||||
existingUser, _ := us.userRepo.FindByEmail(user.Email)
|
existingUser, _ := us.userRepo.FindByUsername(user.Username)
|
||||||
if existingUser != nil {
|
if existingUser != nil {
|
||||||
return nil, errors.New("email đã được sử dụng")
|
return nil, errors.New("username đã được sử dụng")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lưu vào database thông qua repository
|
// Lưu vào database thông qua repository
|
||||||
@ -51,22 +51,22 @@ func (us *UserService) Register(c *gin.Context) (map[string]interface{}, error)
|
|||||||
func (us *UserService) Login(c *gin.Context) (map[string]interface{}, error) {
|
func (us *UserService) Login(c *gin.Context) (map[string]interface{}, error) {
|
||||||
// Đọc dữ liệu từ request
|
// Đọc dữ liệu từ request
|
||||||
var loginData struct {
|
var loginData struct {
|
||||||
Email string `json:"email"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&loginData); err != nil {
|
if err := c.ShouldBindJSON(&loginData); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tìm user theo email
|
// Tìm user theo username
|
||||||
user, err := us.userRepo.FindByEmail(loginData.Email)
|
user, err := us.userRepo.FindByUsername(loginData.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("email hoặc mật khẩu không đúng")
|
return nil, errors.New("username hoặc mật khẩu không đúng")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kiểm tra mật khẩu (giả định)
|
// Kiểm tra mật khẩu (giả định)
|
||||||
if user.Password != loginData.Password {
|
if user.Password != loginData.Password {
|
||||||
return nil, errors.New("email hoặc mật khẩu không đúng")
|
return nil, errors.New("username hoặc mật khẩu không đúng")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trả về kết quả
|
// Trả về kết quả
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/dungnt11/senflow_app/internal/controllers"
|
"github.com/dungnt11/senflow_app/internal/controllers"
|
||||||
"github.com/dungnt11/senflow_app/internal/repositories"
|
"github.com/dungnt11/senflow_app/internal/repositories"
|
||||||
"github.com/dungnt11/senflow_app/internal/services"
|
"github.com/dungnt11/senflow_app/internal/services"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -48,15 +47,3 @@ var AppSet = wire.NewSet(
|
|||||||
func ProvideDB() *gorm.DB {
|
func ProvideDB() *gorm.DB {
|
||||||
return global.Mdb
|
return global.Mdb
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProvideRouter cung cấp instance của *gin.Engine
|
|
||||||
func ProvideRouter() *gin.Engine {
|
|
||||||
if global.Config.Server.AppEnv == "local" {
|
|
||||||
gin.SetMode(gin.DebugMode)
|
|
||||||
gin.ForceConsoleColor()
|
|
||||||
return gin.Default()
|
|
||||||
} else {
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
return gin.New()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
22
logs/app.log
22
logs/app.log
@ -99,3 +99,25 @@
|
|||||||
{"level":"INFO","time":"2025-03-02T07:22:03.405Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
{"level":"INFO","time":"2025-03-02T07:22:03.405Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
{"level":"INFO","time":"2025-03-02T07:22:29.415Z","caller":"initialize/mysql.go:30","msg":"Initializing MySQL Successfully"}
|
{"level":"INFO","time":"2025-03-02T07:22:29.415Z","caller":"initialize/mysql.go:30","msg":"Initializing MySQL Successfully"}
|
||||||
{"level":"INFO","time":"2025-03-02T07:22:29.418Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
{"level":"INFO","time":"2025-03-02T07:22:29.418Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"INFO","time":"2025-03-02T10:08:13.511Z","caller":"initialize/mysql.go:30","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-02T10:08:13.512Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"INFO","time":"2025-03-02T10:10:51.138Z","caller":"initialize/mysql.go:30","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-02T10:10:51.140Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"INFO","time":"2025-03-02T10:11:01.783Z","caller":"initialize/mysql.go:30","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-02T10:11:01.784Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"INFO","time":"2025-03-02T10:11:17.742Z","caller":"initialize/mysql.go:30","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-02T10:11:17.743Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"ERROR","time":"2025-03-02T10:16:24.022Z","caller":"initialize/mysql.go:16","msg":"InitMysql initialization error","error":"dial tcp [::1]:3306: connect: connection refused","stacktrace":"github.com/dungnt11/senflow_app/internal/initialize.checkErrorPanic\n\t/app/internal/initialize/mysql.go:16\ngithub.com/dungnt11/senflow_app/internal/initialize.InitMysql\n\t/app/internal/initialize/mysql.go:29\ngithub.com/dungnt11/senflow_app/internal/initialize.Run\n\t/app/internal/initialize/run.go:35\nmain.main\n\t/app/cmd/api/main.go:9\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:272"}
|
||||||
|
{"level":"ERROR","time":"2025-03-02T10:27:48.788Z","caller":"initialize/mysql.go:16","msg":"InitMysql initialization error","error":"dial tcp [::1]:3306: connect: connection refused","stacktrace":"github.com/dungnt11/senflow_app/internal/initialize.checkErrorPanic\n\t/app/internal/initialize/mysql.go:16\ngithub.com/dungnt11/senflow_app/internal/initialize.InitMysql\n\t/app/internal/initialize/mysql.go:29\ngithub.com/dungnt11/senflow_app/internal/initialize.Run\n\t/app/internal/initialize/run.go:35\nmain.main\n\t/app/cmd/api/main.go:9\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:272"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T06:37:31.599Z","caller":"initialize/mysql.go:30","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T06:37:31.600Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T06:39:11.766Z","caller":"initialize/mysql.go:30","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T06:39:11.767Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T06:58:50.099Z","caller":"initialize/mysql.go:33","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T06:58:50.101Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T06:59:04.488Z","caller":"initialize/mysql.go:31","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T06:59:04.489Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T07:00:37.818Z","caller":"initialize/mysql.go:31","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T07:00:37.818Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T07:01:19.523Z","caller":"initialize/mysql.go:31","msg":"Initializing MySQL Successfully"}
|
||||||
|
{"level":"INFO","time":"2025-03-03T07:01:19.524Z","caller":"initialize/run.go:63","msg":"Server đang chạy trên cổng 8080"}
|
||||||
|
30
nginx/conf.d/default.conf
Normal file
30
nginx/conf.d/default.conf
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
upstream app_servers {
|
||||||
|
server app:8083;
|
||||||
|
server app:8084;
|
||||||
|
server app:8085;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://app_servers;
|
||||||
|
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;
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 4 256k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 'healthy\n';
|
||||||
|
}
|
||||||
|
}
|
25
prometheus/prometheus.yml
Normal file
25
prometheus/prometheus.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
evaluation_interval: 15s
|
||||||
|
scrape_timeout: 10s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'app'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['app:8080']
|
||||||
|
metrics_path: '/metrics'
|
||||||
|
scrape_interval: 15s
|
||||||
|
scrape_timeout: 10s
|
||||||
|
|
||||||
|
- job_name: 'mysql'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['mysql_bp:3306']
|
||||||
|
metrics_path: '/metrics'
|
||||||
|
scrape_interval: 15s
|
||||||
|
scrape_timeout: 10s
|
||||||
|
|
||||||
|
- job_name: 'prometheus'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9090']
|
||||||
|
scrape_interval: 15s
|
||||||
|
scrape_timeout: 10s
|
21
scripts/backup.sh
Normal file
21
scripts/backup.sh
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Thông tin kết nối
|
||||||
|
DB_HOST="mysql_bp"
|
||||||
|
DB_USER="root"
|
||||||
|
DB_PASS="root"
|
||||||
|
DB_NAME="blueprint"
|
||||||
|
BACKUP_DIR="/backup"
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# Tạo backup
|
||||||
|
echo "Starting backup at $DATE"
|
||||||
|
mysqldump -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME > "$BACKUP_DIR/backup_$DATE.sql"
|
||||||
|
|
||||||
|
# Nén backup
|
||||||
|
gzip "$BACKUP_DIR/backup_$DATE.sql"
|
||||||
|
|
||||||
|
# Xóa backup cũ (giữ 7 ngày)
|
||||||
|
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +7 -delete
|
||||||
|
|
||||||
|
echo "Backup completed"
|
Loading…
x
Reference in New Issue
Block a user