commit d83f6c11456f25cf0235c2e8fdcf404888fbff7e Author: koh Date: Sat Apr 12 15:25:34 2025 +0700 init diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..5a3c060 --- /dev/null +++ b/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./main" + cmd = "make build" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..0a7b279 --- /dev/null +++ b/.env.prod @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18e5f2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with "go test -c" +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +tmp/ + +# IDE specific files +.vscode +.idea + +# .env file +.env + +# Project build +main +*templ.go + +# OS X generated file +.DS_Store + +/logs/app.log diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..d642437 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,27 @@ +FROM golang:1.23.3-alpine + +WORKDIR /app + +# Install build dependencies and air +RUN apk add --no-cache git make && \ + go install github.com/cosmtrek/air@v1.49.0 + +# Sao chép file go.mod và go.sum +COPY go.mod go.sum ./ + +# Tải các dependency +RUN go mod download + +# Sao chép file .air.toml +COPY .air.toml ./ + +# Sao chép toàn bộ mã nguồn +COPY . . + +# Expose port +ARG PORT=8080 +ENV PORT=${PORT} +EXPOSE ${PORT} + +# Chạy Air để hot-reload +CMD ["air"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..942b83e --- /dev/null +++ b/Dockerfile.prod @@ -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"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9d76e0e --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +# Simple Makefile for a Go project + +# Build the application +all: build test + +build: + @echo "Building..." + + + @go build -o main cmd/api/main.go + +# Run the application +run: + @go run cmd/api/main.go +# Create DB container +docker-run: + @if docker compose up --build 2>/dev/null; then \ + : ; \ + else \ + echo "Falling back to Docker Compose V1"; \ + docker-compose up --build; \ + fi + +# Shutdown DB container +docker-down: + @if docker compose down 2>/dev/null; then \ + : ; \ + else \ + echo "Falling back to Docker Compose V1"; \ + docker-compose down; \ + fi + +# Test the application +test: + @echo "Testing..." + @go test ./... -v +# Integrations Tests for the application +itest: + @echo "Running integration tests..." + @go test ./internal/database -v + +# Clean the binary +clean: + @echo "Cleaning..." + @rm -f main + +# Live Reload +watch: + @if command -v air > /dev/null; then \ + air; \ + echo "Watching...";\ + else \ + read -p "Go's 'air' is not installed on your machine. Do you want to install it? [Y/n] " choice; \ + if [ "$$choice" != "n" ] && [ "$$choice" != "N" ]; then \ + go install github.com/air-verse/air@latest; \ + air; \ + echo "Watching...";\ + else \ + echo "You chose not to install air. Exiting..."; \ + exit 1; \ + fi; \ + fi + +.PHONY: all build run test clean watch docker-run docker-down itest diff --git a/README.md b/README.md new file mode 100644 index 0000000..feee80e --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +## Cấu trúc dự án chi tiết + +``` +. +├── cmd/ # Điểm vào ứng dụng +│ └── api/ # API server +│ └── main.go # File main khởi động ứng dụng +├── global/ # Biến toàn cục và cấu trúc cấu hình +│ └── global.go # Định nghĩa các biến và cấu trúc toàn cục +├── internal/ # Mã nguồn nội bộ +│ ├── controllers/ # Xử lý request và response +│ │ └── user_controller.go # Controller xử lý các request liên quan đến user +│ ├── initialize/ # Khởi tạo các thành phần của ứng dụng +│ │ ├── loadconfig.go # Đọc 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 # Điểm khởi động chính của ứng dụng +│ ├── 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 + +Dự án tuân theo các quy tắc đặt tên file sau để đảm bảo tính nhất quá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`. + +2. **Tên file mô tả rõ chức năng** của file đó: + - Controllers: `_controller.go` (ví dụ: `user_controller.go`) + - Services: `_service.go` (ví dụ: `user_service.go`) + - Repositories: `_repository.go` (ví dụ: `user_repository.go`) + - Routers: `_router.go` (ví dụ: `user_router.go`) + - Models: `.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: + - `loadconfig.go`: Đọc cấu hình + - `logger.go`: Khởi tạo logger + - `mysql.go`: Khởi tạo kết nối MySQL + - `run.go`: Điểm khởi động ứng dụng + +4. **File Wire** tuân theo quy ước của Google Wire: + - `wire.go`: Định nghĩa các provider + - `injector.go`: Định nghĩa các injector + - `wire_gen.go`: File được tạo tự động bởi Wire + +## Kiến trúc ứng dụng + +Ứng dụng được tổ chức theo kiến trúc phân lớp: + +1. **Controllers**: Xử lý request và response, gọi các service để thực hiện logic nghiệp vụ. +2. **Services**: Chứa logic nghiệp vụ, gọi các repository để tương tác với dữ liệu. +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. +4. **Models**: Định nghĩa cấu trúc dữ liệu. +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) + +### 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 +# Khởi động môi trường phát triển +docker-compose -f docker-compose.dev.yml up -d + +# Xem logs +docker-compose -f docker-compose.dev.yml logs -f + +# Dừng môi trường phát triển +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 + +```bash +# Khởi động MySQL trong Docker +make docker-run + +# Chạy ứng dụng với hot-reload +make watch + +# 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 +``` diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..8882c2c --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/dungnt11/todoms_golang/internal/initialize" +) + +func main() { + initialize.Run() +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..e21e9a1 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,92 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + restart: unless-stopped + ports: + - ${PORT}:${PORT} + environment: + APP_ENV: ${APP_ENV} + PORT: ${PORT} + 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} + volumes: + - .:/app # Mount current directory to /app in container + command: air # Use air for hot-reload + depends_on: + mysql_bp: + condition: service_healthy + networks: + - blueprint + mysql_bp: + image: mysql:8.0 + 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} + MYSQL_ALLOW_EMPTY_PASSWORD: "no" + ports: + - "3306:3306" + volumes: + - mysql_volume_bp:/var/lib/mysql + - ./mysql/init:/docker-entrypoint-initdb.d + 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 + + phpmyadmin: + image: 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: + - blueprint + +volumes: + mysql_volume_bp: +networks: + blueprint: \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..870a189 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,155 @@ +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: root + BLUEPRINT_DB_PASSWORD: ${BLUEPRINT_DB_ROOT_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 + restart: unless-stopped + environment: + MYSQL_DATABASE: ${BLUEPRINT_DB_DATABASE} + 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 + 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 + 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 + 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 + 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: \ No newline at end of file diff --git a/global/global.go b/global/global.go new file mode 100644 index 0000000..c33b439 --- /dev/null +++ b/global/global.go @@ -0,0 +1,44 @@ +package global + +import ( + "go.uber.org/zap" + "gorm.io/gorm" +) + +var ( + Config Configuration + Logger *zap.Logger + Mdb *gorm.DB +) + +type Configuration struct { + Server ServerConfig + Database DatabaseConfig + Logger LoggerConfig +} + +type ServerConfig struct { + Port string + AppEnv string +} + +type DatabaseConfig struct { + Host string + Port string + Database string + Username string + Password string + RootPassword string + MaxIdleConns int + MaxOpenConns int + ConnMaxLifetime int +} + +type LoggerConfig struct { + LogLevel string + FileLogName string + MaxSize int + MaxBackups int + MaxAge int + Compress bool +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e170076 --- /dev/null +++ b/go.mod @@ -0,0 +1,52 @@ +module github.com/dungnt11/todoms_golang + +go 1.23.3 + +require go.uber.org/zap v1.27.0 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.12.9 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.25.0 // indirect + github.com/go-sql-driver/mysql v1.9.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/google/subcommands v1.2.0 // indirect + github.com/google/wire v0.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/natefinch/lumberjack v2.0.0+incompatible // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/arch v0.14.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.5.7 // indirect + gorm.io/gorm v1.25.12 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3494cef --- /dev/null +++ b/go.sum @@ -0,0 +1,168 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ= +github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= +github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= +github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= +github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= +golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/controllers/controllers.go b/internal/controllers/controllers.go new file mode 100644 index 0000000..d95d464 --- /dev/null +++ b/internal/controllers/controllers.go @@ -0,0 +1,15 @@ +package controllers + +// Controllers chứa tất cả các controller của ứng dụng +type Controllers struct { + UserController IUserController + ProductController IProductController +} + +// NewControllers tạo một instance mới của Controllers +func NewControllers(userController IUserController, productController IProductController) *Controllers { + return &Controllers{ + UserController: userController, + ProductController: productController, + } +} diff --git a/internal/controllers/interfaces.go b/internal/controllers/interfaces.go new file mode 100644 index 0000000..acfdd34 --- /dev/null +++ b/internal/controllers/interfaces.go @@ -0,0 +1,22 @@ +package controllers + +import "github.com/gin-gonic/gin" + +// IUserController định nghĩa interface cho UserController +type IUserController interface { + Register(c *gin.Context) + Login(c *gin.Context) + GetProfile(c *gin.Context) + UpdateProfile(c *gin.Context) +} + +// IProductController định nghĩa interface cho ProductController +type IProductController interface { + CreateProduct(c *gin.Context) + GetProduct(c *gin.Context) + GetAllProducts(c *gin.Context) + UpdateProduct(c *gin.Context) + DeleteProduct(c *gin.Context) +} + +// Các interface khác có thể được thêm vào đây khi cần thiết diff --git a/internal/controllers/product_controller.go b/internal/controllers/product_controller.go new file mode 100644 index 0000000..0734d38 --- /dev/null +++ b/internal/controllers/product_controller.go @@ -0,0 +1,125 @@ +package controllers + +import ( + "strconv" + + "github.com/dungnt11/todoms_golang/internal/services" + "github.com/gin-gonic/gin" +) + +// ProductController triển khai IProductController +type ProductController struct { + productService services.IProductService +} + +// NewProductController tạo một instance mới của ProductController +func NewProductController(productService services.IProductService) *ProductController { + return &ProductController{ + productService: productService, + } +} + +// CreateProduct xử lý tạo sản phẩm mới +func (pc *ProductController) CreateProduct(c *gin.Context) { + // Gọi service để thực hiện logic tạo sản phẩm + result, err := pc.productService.CreateProduct(c) + + if err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(201, result) +} + +// GetProduct xử lý lấy thông tin sản phẩm theo ID +func (pc *ProductController) GetProduct(c *gin.Context) { + // Lấy ID từ param + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(400, gin.H{ + "error": "ID không hợp lệ", + }) + return + } + + // Gọi service để lấy thông tin sản phẩm + result, err := pc.productService.GetProduct(uint(id)) + + if err != nil { + c.JSON(404, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, result) +} + +// GetAllProducts xử lý lấy tất cả sản phẩm +func (pc *ProductController) GetAllProducts(c *gin.Context) { + // Gọi service để lấy tất cả sản phẩm + result, err := pc.productService.GetAllProducts() + + if err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, result) +} + +// UpdateProduct xử lý cập nhật thông tin sản phẩm +func (pc *ProductController) UpdateProduct(c *gin.Context) { + // Lấy ID từ param + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(400, gin.H{ + "error": "ID không hợp lệ", + }) + return + } + + // Gọi service để cập nhật thông tin sản phẩm + result, err := pc.productService.UpdateProduct(uint(id), c) + + if err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, result) +} + +// DeleteProduct xử lý xóa sản phẩm +func (pc *ProductController) DeleteProduct(c *gin.Context) { + // Lấy ID từ param + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + c.JSON(400, gin.H{ + "error": "ID không hợp lệ", + }) + return + } + + // Gọi service để xóa sản phẩm + result, err := pc.productService.DeleteProduct(uint(id)) + + if err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, result) +} diff --git a/internal/controllers/user_controller.go b/internal/controllers/user_controller.go new file mode 100644 index 0000000..fa3a7e3 --- /dev/null +++ b/internal/controllers/user_controller.go @@ -0,0 +1,93 @@ +package controllers + +import ( + "github.com/dungnt11/todoms_golang/internal/services" + "github.com/gin-gonic/gin" +) + +// UserController triển khai IUserController +type UserController struct { + userService services.IUserService +} + +// NewUserController tạo một instance mới của UserController +func NewUserController(userService services.IUserService) *UserController { + return &UserController{ + userService: userService, + } +} + +// Register xử lý đăng ký người dùng +func (uc *UserController) Register(c *gin.Context) { + // Gọi service để thực hiện logic đăng ký + result, err := uc.userService.Register(c) + + if err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, result) +} + +// Login xử lý đăng nhập +func (uc *UserController) Login(c *gin.Context) { + // Gọi service để thực hiện logic đăng nhập + result, err := uc.userService.Login(c) + + if err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, result) +} + +// GetProfile xử lý lấy thông tin profile +func (uc *UserController) GetProfile(c *gin.Context) { + // Lấy user ID từ token hoặc param (giả định) + userID := uint(1) // Trong thực tế, bạn sẽ lấy từ JWT token hoặc param + + // Gọi service để lấy thông tin profile + result, err := uc.userService.GetProfile(userID) + + if err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, result) +} + +// UpdateProfile xử lý cập nhật profile +func (uc *UserController) UpdateProfile(c *gin.Context) { + // Lấy user ID từ token hoặc param (giả định) + userID := uint(1) // Trong thực tế, bạn sẽ lấy từ JWT token hoặc param + + // Đọc dữ liệu từ request + var data map[string]interface{} + if err := c.ShouldBindJSON(&data); err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + return + } + + // Gọi service để cập nhật profile + result, err := uc.userService.UpdateProfile(userID, data) + + if err != nil { + c.JSON(400, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, result) +} diff --git a/internal/initialize/loadconfig.go b/internal/initialize/loadconfig.go new file mode 100644 index 0000000..3fdbfa8 --- /dev/null +++ b/internal/initialize/loadconfig.go @@ -0,0 +1,65 @@ +package initialize + +import ( + "fmt" + "os" + "strings" + + "github.com/dungnt11/todoms_golang/global" + "github.com/joho/godotenv" +) + +func LoadConfig() error { + err := godotenv.Load() + if err != nil { + return err + } + + // Cấu hình server + global.Config.Server.Port = os.Getenv("PORT") + global.Config.Server.AppEnv = os.Getenv("APP_ENV") + + // Cấu hình database + global.Config.Database.Host = os.Getenv("BLUEPRINT_DB_HOST") + global.Config.Database.Port = os.Getenv("BLUEPRINT_DB_PORT") + global.Config.Database.Database = os.Getenv("BLUEPRINT_DB_DATABASE") + global.Config.Database.Username = os.Getenv("BLUEPRINT_DB_USERNAME") + global.Config.Database.Password = os.Getenv("BLUEPRINT_DB_PASSWORD") + global.Config.Database.RootPassword = os.Getenv("BLUEPRINT_DB_ROOT_PASSWORD") + + // Chuyển đổi các giá trị cấu hình database từ string sang int + if maxIdleConns := os.Getenv("BLUEPRINT_DB_MAX_IDLE_CONNS"); maxIdleConns != "" { + fmt.Sscanf(maxIdleConns, "%d", &global.Config.Database.MaxIdleConns) + } + + if maxOpenConns := os.Getenv("BLUEPRINT_DB_MAX_OPEN_CONNS"); maxOpenConns != "" { + fmt.Sscanf(maxOpenConns, "%d", &global.Config.Database.MaxOpenConns) + } + + if connMaxLifetime := os.Getenv("BLUEPRINT_DB_CONN_MAX_LIFETIME"); connMaxLifetime != "" { + fmt.Sscanf(connMaxLifetime, "%d", &global.Config.Database.ConnMaxLifetime) + } + + // Cấu hình logger + global.Config.Logger.LogLevel = os.Getenv("LOGGER_LOG_LEVEL") + global.Config.Logger.FileLogName = os.Getenv("LOGGER_FILE_LOG_NAME") + + // Chuyển đổi từ string sang int và bool + if maxSize := os.Getenv("LOGGER_MAX_SIZE"); maxSize != "" { + fmt.Sscanf(maxSize, "%d", &global.Config.Logger.MaxSize) + } + + if maxBackups := os.Getenv("LOGGER_MAX_BACKUPS"); maxBackups != "" { + fmt.Sscanf(maxBackups, "%d", &global.Config.Logger.MaxBackups) + } + + if maxAge := os.Getenv("LOGGER_MAX_AGE"); maxAge != "" { + fmt.Sscanf(maxAge, "%d", &global.Config.Logger.MaxAge) + } + + if compress := os.Getenv("LOGGER_COMPRESS"); compress != "" { + global.Config.Logger.Compress = strings.ToLower(compress) == "true" + } + + return nil +} diff --git a/internal/initialize/logger.go b/internal/initialize/logger.go new file mode 100644 index 0000000..da74a26 --- /dev/null +++ b/internal/initialize/logger.go @@ -0,0 +1,103 @@ +package initialize + +import ( + "os" + + "github.com/dungnt11/todoms_golang/global" + "github.com/natefinch/lumberjack" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// InitLogger khởi tạo logger +func InitLogger() *zap.Logger { + // Đảm bảo thư mục logs tồn tại + ensureLogDir(global.Config.Logger.FileLogName) + + // Khởi tạo logger + return newLogger(global.Config.Logger) +} + +// ensureLogDir đảm bảo thư mục logs tồn tại +func ensureLogDir(logPath string) { + // Tìm vị trí thư mục cuối cùng trong đường dẫn + lastSlash := 0 + for i := len(logPath) - 1; i >= 0; i-- { + if logPath[i] == '/' { + lastSlash = i + break + } + } + + // Nếu không có thư mục, không cần tạo + if lastSlash == 0 { + return + } + + // Tạo thư mục nếu chưa tồn tại + dirPath := logPath[:lastSlash] + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + os.MkdirAll(dirPath, 0755) + } +} + +// newLogger tạo một logger mới +func newLogger(config global.LoggerConfig) *zap.Logger { + // Xác định level log + var level zapcore.Level + switch config.LogLevel { + case "debug": + level = zapcore.DebugLevel + case "info": + level = zapcore.InfoLevel + case "warn": + level = zapcore.WarnLevel + case "error": + level = zapcore.ErrorLevel + default: + level = zapcore.InfoLevel + } + + // Cấu hình encoder + encoder := getEncoder() + + // Cấu hình lumberjack để xoay vòng file log + hook := lumberjack.Logger{ + Filename: config.FileLogName, + MaxSize: config.MaxSize, // megabytes + MaxBackups: config.MaxBackups, + MaxAge: config.MaxAge, // days + Compress: config.Compress, + } + + // Tạo core cho zap + core := zapcore.NewCore( + encoder, + zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&hook)), + level, + ) + + // Tạo logger với các tùy chọn + return zap.New(core, + zap.AddCaller(), + zap.AddStacktrace(zapcore.ErrorLevel), + ) +} + +// getEncoder trả về encoder cho zap +func getEncoder() zapcore.Encoder { + encoderConfig := zap.NewProductionEncoderConfig() + + // Cấu hình thời gian + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + encoderConfig.TimeKey = "time" + + // Cấu hình level + encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder + + // Cấu hình caller + encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder + + // Trả về encoder JSON + return zapcore.NewJSONEncoder(encoderConfig) +} diff --git a/internal/initialize/mysql.go b/internal/initialize/mysql.go new file mode 100644 index 0000000..2eef30a --- /dev/null +++ b/internal/initialize/mysql.go @@ -0,0 +1,47 @@ +package initialize + +import ( + "fmt" + "time" + + "github.com/dungnt11/todoms_golang/global" + "go.uber.org/zap" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +// checkErrorPanic kiểm tra lỗi và gọi panic nếu có lỗi, đồng thời ghi log chi tiết. +func checkErrorPanic(err error, errString string) { + if err != nil { + global.Logger.Error(errString, zap.Error(err)) + panic(err) + } +} + +// 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 { + m := global.Config.Database + + 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) + db, err := gorm.Open(mysql.Open(s), &gorm.Config{ + SkipDefaultTransaction: false, + }) + checkErrorPanic(err, "InitMysql initialization error") + global.Logger.Info("Initializing MySQL Successfully") + setPool(db) + return db +} + +// setPool thiết lập các thông số cho connection pool của cơ sở dữ liệu. +func setPool(db *gorm.DB) { + m := global.Config.Database + sqlDb, err := db.DB() + if err != nil { + global.Logger.Error("Failed to get sql.DB from gorm.DB", zap.Error(err)) + return + } + sqlDb.SetMaxIdleConns(m.MaxIdleConns) + sqlDb.SetMaxOpenConns(m.MaxOpenConns) + sqlDb.SetConnMaxLifetime(time.Duration(m.ConnMaxLifetime) * time.Second) +} diff --git a/internal/initialize/run.go b/internal/initialize/run.go new file mode 100644 index 0000000..5a3478e --- /dev/null +++ b/internal/initialize/run.go @@ -0,0 +1,93 @@ +package initialize + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/dungnt11/todoms_golang/global" + "github.com/dungnt11/todoms_golang/internal/routers" + "github.com/dungnt11/todoms_golang/internal/wire" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// Run khởi động ứng dụng +func Run() { + // Tải cấu hình từ file .env + if err := LoadConfig(); err != nil { + fmt.Printf("Không thể tải cấu hình: %s\n", err.Error()) + return + } + + // Khởi tạo logger + global.Logger = InitLogger() + if global.Logger == nil { + fmt.Println("Khởi tạo logger thất bại") + return + } + + // Khởi tạo database + global.Mdb = InitMysql() + if global.Mdb == nil { + global.Logger.Error("Khởi tạo MySQL thất bại") + return + } + + // Khởi tạo router + r := provideRouter() + + // Khởi tạo controllers thông qua wire + controllers, err := wire.InitializeControllers() + if err != nil { + global.Logger.Error("Khởi tạo controllers thất bại", zap.Error(err)) + return + } + + // Khởi tạo routers + routers.InitRouters(r, controllers) + + // Khởi động server + port := global.Config.Server.Port + server := &http.Server{ + Addr: fmt.Sprintf(":%s", port), + Handler: r, + } + + // Khởi động server trong goroutine riêng + go func() { + global.Logger.Info(fmt.Sprintf("Server đang chạy trên cổng %s", port)) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + global.Logger.Error("Server gặp lỗi", zap.Error(err)) + } + }() + + // Graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + global.Logger.Info("Đang tắt server...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + global.Logger.Error("Server shutdown gặp lỗi", zap.Error(err)) + } + + 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() + } +} diff --git a/internal/models/product.go b/internal/models/product.go new file mode 100644 index 0000000..4eb1879 --- /dev/null +++ b/internal/models/product.go @@ -0,0 +1,16 @@ +package models + +import ( + "time" +) + +// Product đại diện cho thông tin sản phẩm +type Product struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:255;not null" json:"name"` + Description string `gorm:"type:text" json:"description"` + Price float64 `gorm:"not null" json:"price"` + Quantity int `gorm:"not null" json:"quantity"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..3837b88 --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,18 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// User đại diện cho người dùng trong hệ thống +type User struct { + ID uint `json:"id" gorm:"primaryKey"` + Username string `json:"username" gorm:"uniqueIndex;not null"` + Password string `json:"-" gorm:"not null"` // Không trả về password trong JSON + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} diff --git a/internal/repositories/interfaces.go b/internal/repositories/interfaces.go new file mode 100644 index 0000000..e485066 --- /dev/null +++ b/internal/repositories/interfaces.go @@ -0,0 +1,23 @@ +package repositories + +import "github.com/dungnt11/todoms_golang/internal/models" + +// IUserRepository định nghĩa interface cho UserRepository +type IUserRepository interface { + Create(user *models.User) error + FindByID(id uint) (*models.User, error) + FindByUsername(username string) (*models.User, error) + Update(user *models.User) error + Delete(id uint) error +} + +// IProductRepository định nghĩa interface cho ProductRepository +type IProductRepository interface { + Create(product *models.Product) error + FindByID(id uint) (*models.Product, error) + FindAll() ([]*models.Product, error) + Update(product *models.Product) error + Delete(id uint) error +} + +// Các interface khác có thể được thêm vào đây khi cần thiết diff --git a/internal/repositories/product_repository.go b/internal/repositories/product_repository.go new file mode 100644 index 0000000..77440f2 --- /dev/null +++ b/internal/repositories/product_repository.go @@ -0,0 +1,53 @@ +package repositories + +import ( + "github.com/dungnt11/todoms_golang/internal/models" + "gorm.io/gorm" +) + +// ProductRepository triển khai IProductRepository +type ProductRepository struct { + db *gorm.DB +} + +// NewProductRepository tạo một instance mới của ProductRepository +func NewProductRepository(db *gorm.DB) *ProductRepository { + return &ProductRepository{ + db: db, + } +} + +// Create tạo một sản phẩm mới +func (r *ProductRepository) Create(product *models.Product) error { + return r.db.Create(product).Error +} + +// FindByID tìm sản phẩm theo ID +func (r *ProductRepository) FindByID(id uint) (*models.Product, error) { + var product models.Product + err := r.db.First(&product, id).Error + if err != nil { + return nil, err + } + return &product, nil +} + +// FindAll lấy tất cả sản phẩm +func (r *ProductRepository) FindAll() ([]*models.Product, error) { + var products []*models.Product + err := r.db.Find(&products).Error + if err != nil { + return nil, err + } + return products, nil +} + +// Update cập nhật thông tin sản phẩm +func (r *ProductRepository) Update(product *models.Product) error { + return r.db.Save(product).Error +} + +// Delete xóa sản phẩm theo ID +func (r *ProductRepository) Delete(id uint) error { + return r.db.Delete(&models.Product{}, id).Error +} diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go new file mode 100644 index 0000000..ebceb28 --- /dev/null +++ b/internal/repositories/user_repository.go @@ -0,0 +1,53 @@ +package repositories + +import ( + "github.com/dungnt11/todoms_golang/internal/models" + "gorm.io/gorm" +) + +// UserRepository triển khai IUserRepository +type UserRepository struct { + db *gorm.DB +} + +// NewUserRepository tạo một instance mới của UserRepository +func NewUserRepository(db *gorm.DB) *UserRepository { + return &UserRepository{ + db: db, + } +} + +// Create tạo một user mới trong database +func (ur *UserRepository) Create(user *models.User) error { + return ur.db.Create(user).Error +} + +// FindByID tìm user theo ID +func (ur *UserRepository) FindByID(id uint) (*models.User, error) { + var user models.User + err := ur.db.First(&user, id).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// FindByUsername tìm user theo username +func (ur *UserRepository) FindByUsername(username string) (*models.User, error) { + var user models.User + err := ur.db.Where("username = ?", username).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// Update cập nhật thông tin user +func (ur *UserRepository) Update(user *models.User) error { + return ur.db.Save(user).Error +} + +// Delete xóa user theo ID +func (ur *UserRepository) Delete(id uint) error { + return ur.db.Delete(&models.User{}, id).Error +} diff --git a/internal/routers/product/product_router.go b/internal/routers/product/product_router.go new file mode 100644 index 0000000..3476181 --- /dev/null +++ b/internal/routers/product/product_router.go @@ -0,0 +1,18 @@ +package product + +import ( + "github.com/dungnt11/todoms_golang/internal/controllers" + "github.com/gin-gonic/gin" +) + +// InitProductRoutes khởi tạo các routes cho product +func InitProductRoutes(router *gin.RouterGroup, productController controllers.IProductController) { + productRoutes := router.Group("/products") + { + productRoutes.POST("", productController.CreateProduct) + productRoutes.GET("", productController.GetAllProducts) + productRoutes.GET("/:id", productController.GetProduct) + productRoutes.PUT("/:id", productController.UpdateProduct) + productRoutes.DELETE("/:id", productController.DeleteProduct) + } +} diff --git a/internal/routers/router_group.go b/internal/routers/router_group.go new file mode 100644 index 0000000..2eb8727 --- /dev/null +++ b/internal/routers/router_group.go @@ -0,0 +1,20 @@ +package routers + +import ( + "github.com/dungnt11/todoms_golang/internal/controllers" + "github.com/dungnt11/todoms_golang/internal/routers/product" + "github.com/dungnt11/todoms_golang/internal/routers/user" + "github.com/gin-gonic/gin" +) + +// InitRouters khởi tạo tất cả các routers +func InitRouters(r *gin.Engine, controllers *controllers.Controllers) { + // API v1 + apiV1 := r.Group("/api/v1") + + // Khởi tạo các routes + user.InitUserRoutes(apiV1, controllers.UserController) + product.InitProductRoutes(apiV1, controllers.ProductController) + + // Các routes khác có thể được thêm vào đây +} diff --git a/internal/routers/user/user_router.go b/internal/routers/user/user_router.go new file mode 100644 index 0000000..e423763 --- /dev/null +++ b/internal/routers/user/user_router.go @@ -0,0 +1,17 @@ +package user + +import ( + "github.com/dungnt11/todoms_golang/internal/controllers" + "github.com/gin-gonic/gin" +) + +// InitUserRoutes khởi tạo các routes liên quan đến user +func InitUserRoutes(router *gin.RouterGroup, userController controllers.IUserController) { + userGroup := router.Group("/users") + { + userGroup.POST("/register", userController.Register) + userGroup.POST("/login", userController.Login) + userGroup.GET("/profile", userController.GetProfile) + userGroup.PUT("/profile", userController.UpdateProfile) + } +} diff --git a/internal/services/interfaces.go b/internal/services/interfaces.go new file mode 100644 index 0000000..08587ac --- /dev/null +++ b/internal/services/interfaces.go @@ -0,0 +1,22 @@ +package services + +import "github.com/gin-gonic/gin" + +// IUserService định nghĩa interface cho UserService +type IUserService interface { + Register(c *gin.Context) (map[string]interface{}, error) + Login(c *gin.Context) (map[string]interface{}, error) + GetProfile(userID uint) (map[string]interface{}, error) + UpdateProfile(userID uint, data map[string]interface{}) (map[string]interface{}, error) +} + +// IProductService định nghĩa interface cho ProductService +type IProductService interface { + CreateProduct(c *gin.Context) (map[string]interface{}, error) + GetProduct(id uint) (map[string]interface{}, error) + GetAllProducts() (map[string]interface{}, error) + UpdateProduct(id uint, c *gin.Context) (map[string]interface{}, error) + DeleteProduct(id uint) (map[string]interface{}, error) +} + +// Các interface khác có thể được thêm vào đây khi cần thiết diff --git a/internal/services/product_service.go b/internal/services/product_service.go new file mode 100644 index 0000000..194ff24 --- /dev/null +++ b/internal/services/product_service.go @@ -0,0 +1,123 @@ +package services + +import ( + "errors" + + "github.com/dungnt11/todoms_golang/internal/models" + "github.com/dungnt11/todoms_golang/internal/repositories" + "github.com/gin-gonic/gin" +) + +// ProductService triển khai IProductService +type ProductService struct { + productRepo repositories.IProductRepository +} + +// NewProductService tạo một instance mới của ProductService +func NewProductService(productRepo repositories.IProductRepository) *ProductService { + return &ProductService{ + productRepo: productRepo, + } +} + +// CreateProduct tạo một sản phẩm mới +func (s *ProductService) CreateProduct(c *gin.Context) (map[string]interface{}, error) { + var product models.Product + if err := c.ShouldBindJSON(&product); err != nil { + return nil, err + } + + if product.Name == "" { + return nil, errors.New("tên sản phẩm không được để trống") + } + + if product.Price <= 0 { + return nil, errors.New("giá sản phẩm phải lớn hơn 0") + } + + if err := s.productRepo.Create(&product); err != nil { + return nil, err + } + + return map[string]interface{}{ + "message": "tạo sản phẩm thành công", + "product": product, + }, nil +} + +// GetProduct lấy thông tin sản phẩm theo ID +func (s *ProductService) GetProduct(id uint) (map[string]interface{}, error) { + product, err := s.productRepo.FindByID(id) + if err != nil { + return nil, errors.New("không tìm thấy sản phẩm") + } + + return map[string]interface{}{ + "product": product, + }, nil +} + +// GetAllProducts lấy tất cả sản phẩm +func (s *ProductService) GetAllProducts() (map[string]interface{}, error) { + products, err := s.productRepo.FindAll() + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "products": products, + }, nil +} + +// UpdateProduct cập nhật thông tin sản phẩm +func (s *ProductService) UpdateProduct(id uint, c *gin.Context) (map[string]interface{}, error) { + product, err := s.productRepo.FindByID(id) + if err != nil { + return nil, errors.New("không tìm thấy sản phẩm") + } + + var updatedProduct models.Product + if err := c.ShouldBindJSON(&updatedProduct); err != nil { + return nil, err + } + + // Cập nhật thông tin + if updatedProduct.Name != "" { + product.Name = updatedProduct.Name + } + if updatedProduct.Description != "" { + product.Description = updatedProduct.Description + } + if updatedProduct.Price > 0 { + product.Price = updatedProduct.Price + } + if updatedProduct.Quantity >= 0 { + product.Quantity = updatedProduct.Quantity + } + + if err := s.productRepo.Update(product); err != nil { + return nil, err + } + + return map[string]interface{}{ + "message": "cập nhật sản phẩm thành công", + "product": product, + }, nil +} + +// DeleteProduct xóa sản phẩm theo ID +func (s *ProductService) DeleteProduct(id uint) (map[string]interface{}, error) { + // Kiểm tra sản phẩm tồn tại + _, err := s.productRepo.FindByID(id) + if err != nil { + return nil, errors.New("không tìm thấy sản phẩm") + } + + if err := s.productRepo.Delete(id); err != nil { + return nil, err + } + + return map[string]interface{}{ + "message": "xóa sản phẩm thành công", + }, nil +} diff --git a/internal/services/user_service.go b/internal/services/user_service.go new file mode 100644 index 0000000..18012c5 --- /dev/null +++ b/internal/services/user_service.go @@ -0,0 +1,118 @@ +package services + +import ( + "errors" + + "github.com/dungnt11/todoms_golang/internal/models" + "github.com/dungnt11/todoms_golang/internal/repositories" + "github.com/gin-gonic/gin" +) + +// UserService triển khai IUserService +type UserService struct { + userRepo repositories.IUserRepository +} + +// NewUserService tạo một instance mới của UserService +func NewUserService(userRepo repositories.IUserRepository) *UserService { + return &UserService{ + userRepo: userRepo, + } +} + +// Register xử lý logic đăng ký người dùng +func (us *UserService) Register(c *gin.Context) (map[string]interface{}, error) { + // Đọc dữ liệu từ request + var user models.User + if err := c.ShouldBindJSON(&user); err != nil { + return nil, err + } + + // Kiểm tra username đã tồn tại chưa + existingUser, _ := us.userRepo.FindByUsername(user.Username) + if existingUser != nil { + return nil, errors.New("username đã được sử dụng") + } + + // Lưu vào database thông qua repository + err := us.userRepo.Create(&user) + if err != nil { + return nil, err + } + + // Trả về kết quả + return map[string]interface{}{ + "message": "Đăng ký thành công", + "user_id": user.ID, + }, nil +} + +// Login xử lý logic đăng nhập +func (us *UserService) Login(c *gin.Context) (map[string]interface{}, error) { + // Đọc dữ liệu từ request + var loginData struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := c.ShouldBindJSON(&loginData); err != nil { + return nil, err + } + + // Tìm user theo username + user, err := us.userRepo.FindByUsername(loginData.Username) + if err != nil { + return nil, errors.New("username hoặc mật khẩu không đúng") + } + + // Kiểm tra mật khẩu (giả định) + if user.Password != loginData.Password { + return nil, errors.New("username hoặc mật khẩu không đúng") + } + + // Trả về kết quả + return map[string]interface{}{ + "message": "Đăng nhập thành công", + "user_id": user.ID, + "token": "jwt_token_here", // Trong thực tế, bạn sẽ tạo JWT token + }, nil +} + +// GetProfile lấy thông tin profile của người dùng +func (us *UserService) GetProfile(userID uint) (map[string]interface{}, error) { + // Tìm user theo ID + user, err := us.userRepo.FindByID(userID) + if err != nil { + return nil, errors.New("không tìm thấy người dùng") + } + + // Trả về kết quả + return map[string]interface{}{ + "user": user, + }, nil +} + +// UpdateProfile cập nhật thông tin profile của người dùng +func (us *UserService) UpdateProfile(userID uint, data map[string]interface{}) (map[string]interface{}, error) { + // Tìm user theo ID + user, err := us.userRepo.FindByID(userID) + if err != nil { + return nil, errors.New("không tìm thấy người dùng") + } + + // Cập nhật thông tin (giả định) + if name, ok := data["name"].(string); ok { + user.Name = name + } + + // Lưu vào database + err = us.userRepo.Update(user) + if err != nil { + return nil, err + } + + // Trả về kết quả + return map[string]interface{}{ + "message": "Cập nhật thành công", + "user": user, + }, nil +} diff --git a/internal/wire/injector.go b/internal/wire/injector.go new file mode 100644 index 0000000..81c9db9 --- /dev/null +++ b/internal/wire/injector.go @@ -0,0 +1,17 @@ +//go:build wireinject +// +build wireinject + +package wire + +import ( + "github.com/dungnt11/todoms_golang/internal/controllers" + "github.com/google/wire" +) + +// InitializeControllers khởi tạo tất cả các controllers +func InitializeControllers() (*controllers.Controllers, error) { + wire.Build( + AppSet, + ) + return nil, nil +} diff --git a/internal/wire/wire.go b/internal/wire/wire.go new file mode 100644 index 0000000..e4b2208 --- /dev/null +++ b/internal/wire/wire.go @@ -0,0 +1,49 @@ +// Package wire chứa các định nghĩa cho dependency injection sử dụng Google Wire +package wire + +import ( + "github.com/dungnt11/todoms_golang/global" + "github.com/dungnt11/todoms_golang/internal/controllers" + "github.com/dungnt11/todoms_golang/internal/repositories" + "github.com/dungnt11/todoms_golang/internal/services" + "github.com/google/wire" + "gorm.io/gorm" +) + +// RepositorySet là tập hợp các provider cho repositories +var RepositorySet = wire.NewSet( + repositories.NewUserRepository, + wire.Bind(new(repositories.IUserRepository), new(*repositories.UserRepository)), + repositories.NewProductRepository, + wire.Bind(new(repositories.IProductRepository), new(*repositories.ProductRepository)), +) + +// ServiceSet là tập hợp các provider cho services +var ServiceSet = wire.NewSet( + services.NewUserService, + wire.Bind(new(services.IUserService), new(*services.UserService)), + services.NewProductService, + wire.Bind(new(services.IProductService), new(*services.ProductService)), +) + +// ControllerSet là tập hợp các provider cho controllers +var ControllerSet = wire.NewSet( + controllers.NewUserController, + wire.Bind(new(controllers.IUserController), new(*controllers.UserController)), + controllers.NewProductController, + wire.Bind(new(controllers.IProductController), new(*controllers.ProductController)), + controllers.NewControllers, +) + +// ProvideDB cung cấp instance của *gorm.DB +func ProvideDB() *gorm.DB { + return global.Mdb +} + +// AppSet là tập hợp tất cả các provider +var AppSet = wire.NewSet( + RepositorySet, + ServiceSet, + ControllerSet, + ProvideDB, +) diff --git a/internal/wire/wire_gen.go b/internal/wire/wire_gen.go new file mode 100644 index 0000000..e9d1523 --- /dev/null +++ b/internal/wire/wire_gen.go @@ -0,0 +1,28 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run -mod=mod github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package wire + +import ( + "github.com/dungnt11/todoms_golang/internal/controllers" + "github.com/dungnt11/todoms_golang/internal/repositories" + "github.com/dungnt11/todoms_golang/internal/services" +) + +// Injectors from injector.go: + +// InitializeControllers khởi tạo tất cả các controllers +func InitializeControllers() (*controllers.Controllers, error) { + db := ProvideDB() + userRepository := repositories.NewUserRepository(db) + userService := services.NewUserService(userRepository) + userController := controllers.NewUserController(userService) + productRepository := repositories.NewProductRepository(db) + productService := services.NewProductService(productRepository) + productController := controllers.NewProductController(productService) + controllersControllers := controllers.NewControllers(userController, productController) + return controllersControllers, nil +} diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000..95a6d4b --- /dev/null +++ b/nginx/conf.d/default.conf @@ -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'; + } +} \ No newline at end of file diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 0000000..1ee3f5e --- /dev/null +++ b/prometheus/prometheus.yml @@ -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 \ No newline at end of file diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..538a111 --- /dev/null +++ b/scripts/backup.sh @@ -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" \ No newline at end of file