重新组织项目结构,创建基础目录
This commit is contained in:
0
app/README.md
Normal file
0
app/README.md
Normal file
@@ -1,17 +0,0 @@
|
|||||||
# Application
|
|
||||||
APP_ENV=development
|
|
||||||
APP_PORT=8080
|
|
||||||
|
|
||||||
# Database
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_USER=postgres
|
|
||||||
DB_PASSWORD=your_secure_password
|
|
||||||
DB_NAME=appdb
|
|
||||||
DB_SSL_MODE=disable
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
REDIS_HOST=localhost
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=
|
|
||||||
REDIS_DB=0
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Makefile
|
|
||||||
|
|
||||||
.PHONY: all build up down restart logs clean tidy test help
|
|
||||||
|
|
||||||
# 默认目标
|
|
||||||
all: build up
|
|
||||||
|
|
||||||
# 构建镜像
|
|
||||||
build:
|
|
||||||
docker-compose build
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
up:
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# 停止服务
|
|
||||||
down:
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# 完全清理(包括数据卷)
|
|
||||||
clean:
|
|
||||||
docker-compose down -v
|
|
||||||
docker system prune -f
|
|
||||||
|
|
||||||
# 重启服务
|
|
||||||
restart:
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# 查看日志
|
|
||||||
logs:
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# 查看 API 日志
|
|
||||||
logs-api:
|
|
||||||
docker-compose logs -f api
|
|
||||||
|
|
||||||
# 查看数据库日志
|
|
||||||
logs-db:
|
|
||||||
docker-compose logs -f postgres
|
|
||||||
|
|
||||||
# 查看 Redis 日志
|
|
||||||
logs-redis:
|
|
||||||
docker-compose logs -f redis
|
|
||||||
|
|
||||||
# 进入 API 容器
|
|
||||||
shell-api:
|
|
||||||
docker-compose exec api sh
|
|
||||||
|
|
||||||
# 进入数据库容器
|
|
||||||
shell-db:
|
|
||||||
docker-compose exec postgres psql -U postgres -d appdb
|
|
||||||
|
|
||||||
# 进入 Redis 容器
|
|
||||||
shell-redis:
|
|
||||||
docker-compose exec redis redis-cli
|
|
||||||
|
|
||||||
# 下载依赖
|
|
||||||
tidy:
|
|
||||||
cd api && go mod tidy
|
|
||||||
|
|
||||||
# 运行测试
|
|
||||||
test:
|
|
||||||
cd api && go test -v ./...
|
|
||||||
|
|
||||||
# 本地运行
|
|
||||||
run:
|
|
||||||
cd api && go run main.go
|
|
||||||
|
|
||||||
# 帮助
|
|
||||||
help:
|
|
||||||
@echo "Available targets:"
|
|
||||||
@echo " make build - 构建 Docker 镜像"
|
|
||||||
@echo " make up - 启动所有服务"
|
|
||||||
@echo " make down - 停止所有服务"
|
|
||||||
@echo " make restart - 重启所有服务"
|
|
||||||
@echo " make logs - 查看所有服务日志"
|
|
||||||
@echo " make logs-api - 查看 API 服务日志"
|
|
||||||
@echo " make logs-db - 查看数据库日志"
|
|
||||||
@echo " make logs-redis - 查看 Redis 日志"
|
|
||||||
@echo " make shell-api - 进入 API 容器"
|
|
||||||
@echo " make shell-db - 进入数据库容器"
|
|
||||||
@echo " make shell-redis- 进入 Redis 容器"
|
|
||||||
@echo " make clean - 清理所有容器和数据卷"
|
|
||||||
@echo " make tidy - 整理 Go 依赖"
|
|
||||||
@echo " make test - 运行测试"
|
|
||||||
@echo " make run - 本地运行 API 服务"
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# RESTful API 工程
|
|
||||||
|
|
||||||
基于 Golang + Redis + Postgres 的单机多服务 RESTful API 工程。
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- **Golang**: 1.25.8-alpine3.23 - API 服务
|
|
||||||
- **Postgres**: 18.3-alpine3.23 - 关系型数据库
|
|
||||||
- **Redis**: 8.6.2-alpine - 缓存服务
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── api/ # API 服务代码
|
|
||||||
│ ├── config/ # 配置管理
|
|
||||||
│ ├── handlers/ # HTTP 处理器
|
|
||||||
│ ├── models/ # 数据模型
|
|
||||||
│ ├── router/ # 路由配置
|
|
||||||
│ ├── Dockerfile # API 服务镜像
|
|
||||||
│ ├── go.mod # Go 模块定义
|
|
||||||
│ └── main.go # 入口文件
|
|
||||||
├── migrations/ # 数据库迁移脚本
|
|
||||||
├── docker-compose.yml # Docker Compose 配置
|
|
||||||
├── Makefile # 常用命令
|
|
||||||
├── .env # 环境变量(本地开发)
|
|
||||||
└── .env.example # 环境变量示例
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 1. 克隆项目并进入目录
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 复制环境变量文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
# 根据需要编辑 .env 文件
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 启动服务
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make up
|
|
||||||
# 或者
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 验证服务
|
|
||||||
|
|
||||||
- API: http://localhost:8080
|
|
||||||
- Health Check: http://localhost:8080/health
|
|
||||||
- API Docs: http://localhost:8080/api/v1/ping
|
|
||||||
|
|
||||||
### 5. 查看日志
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make logs
|
|
||||||
# 或者单独查看
|
|
||||||
make logs-api
|
|
||||||
make logs-db
|
|
||||||
make logs-redis
|
|
||||||
```
|
|
||||||
|
|
||||||
## API 接口
|
|
||||||
|
|
||||||
### 健康检查
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8080/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### 用户接口
|
|
||||||
|
|
||||||
| 方法 | 路径 | 描述 |
|
|
||||||
|------|------|------|
|
|
||||||
| GET | /api/v1/ping | 测试接口 |
|
|
||||||
| GET | /api/v1/users | 获取用户列表 |
|
|
||||||
| POST | /api/v1/users | 创建用户 |
|
|
||||||
| GET | /api/v1/users/:id | 获取单个用户 |
|
|
||||||
| PUT | /api/v1/users/:id | 更新用户 |
|
|
||||||
| DELETE | /api/v1/users/:id | 删除用户 |
|
|
||||||
|
|
||||||
### 示例请求
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 创建用户
|
|
||||||
curl -X POST http://localhost:8080/api/v1/users \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"name":"王五","email":"wangwu@example.com"}'
|
|
||||||
|
|
||||||
# 获取用户列表
|
|
||||||
curl http://localhost:8080/api/v1/users
|
|
||||||
|
|
||||||
# 获取单个用户
|
|
||||||
curl http://localhost:8080/api/v1/users/1
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常用命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 构建镜像
|
|
||||||
make build
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
make up
|
|
||||||
|
|
||||||
# 停止服务
|
|
||||||
make down
|
|
||||||
|
|
||||||
# 重启服务
|
|
||||||
make restart
|
|
||||||
|
|
||||||
# 查看日志
|
|
||||||
make logs
|
|
||||||
|
|
||||||
# 进入容器
|
|
||||||
make shell-api # 进入 API 容器
|
|
||||||
make shell-db # 进入数据库
|
|
||||||
make shell-redis # 进入 Redis
|
|
||||||
|
|
||||||
# 本地开发
|
|
||||||
make tidy # 整理依赖
|
|
||||||
make test # 运行测试
|
|
||||||
make run # 本地运行
|
|
||||||
|
|
||||||
# 清理环境
|
|
||||||
make clean # 清理所有容器和数据
|
|
||||||
```
|
|
||||||
|
|
||||||
## 环境变量
|
|
||||||
|
|
||||||
| 变量名 | 默认值 | 描述 |
|
|
||||||
|--------|--------|------|
|
|
||||||
| APP_ENV | development | 应用环境 |
|
|
||||||
| APP_PORT | 8080 | API 端口 |
|
|
||||||
| DB_HOST | localhost | 数据库主机 |
|
|
||||||
| DB_PORT | 5432 | 数据库端口 |
|
|
||||||
| DB_USER | postgres | 数据库用户 |
|
|
||||||
| DB_PASSWORD | postgres | 数据库密码 |
|
|
||||||
| DB_NAME | appdb | 数据库名 |
|
|
||||||
| REDIS_HOST | localhost | Redis 主机 |
|
|
||||||
| REDIS_PORT | 6379 | Redis 端口 |
|
|
||||||
| REDIS_PASSWORD | | Redis 密码 |
|
|
||||||
|
|
||||||
## 开发说明
|
|
||||||
|
|
||||||
1. **添加新接口**: 在 `api/handlers/` 下添加处理器,在 `api/router/router.go` 中注册路由
|
|
||||||
2. **添加模型**: 在 `api/models/` 下添加数据模型
|
|
||||||
3. **数据库迁移**: 在 `migrations/` 下添加 SQL 文件
|
|
||||||
|
|
||||||
## 部署
|
|
||||||
|
|
||||||
生产环境部署前请确保:
|
|
||||||
|
|
||||||
1. 修改 `.env` 中的敏感信息(密码等)
|
|
||||||
2. 设置 `APP_ENV=production`
|
|
||||||
3. 使用 HTTPS
|
|
||||||
4. 配置适当的日志和监控
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
FROM golang:1.25.8-alpine3.23 AS builder
|
|
||||||
|
|
||||||
# 安装构建依赖
|
|
||||||
RUN apk add --no-cache git ca-certificates tzdata
|
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 复制依赖文件
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
|
|
||||||
# 下载依赖
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# 复制源代码
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# 构建应用
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
|
||||||
-ldflags="-w -s" \
|
|
||||||
-o /app/server \
|
|
||||||
main.go
|
|
||||||
|
|
||||||
# 使用更小的基础镜像
|
|
||||||
FROM alpine:3.23
|
|
||||||
|
|
||||||
# 安装运行时依赖
|
|
||||||
RUN apk --no-cache add ca-certificates tzdata
|
|
||||||
|
|
||||||
# 创建非 root 用户
|
|
||||||
RUN addgroup -g 1000 -S appgroup && \
|
|
||||||
adduser -u 1000 -S appuser -G appgroup
|
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 从 builder 复制二进制文件
|
|
||||||
COPY --from=builder /app/server /app/server
|
|
||||||
|
|
||||||
# 更改文件所有者
|
|
||||||
RUN chown -R appuser:appgroup /app
|
|
||||||
|
|
||||||
# 切换到非 root 用户
|
|
||||||
USER appuser
|
|
||||||
|
|
||||||
# 暴露端口
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# 健康检查
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
|
||||||
|
|
||||||
# 启动应用
|
|
||||||
ENTRYPOINT ["/app/server"]
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config 应用配置
|
|
||||||
type Config struct {
|
|
||||||
DB *gorm.DB
|
|
||||||
Redis *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// New 创建配置实例
|
|
||||||
func New() *Config {
|
|
||||||
return &Config{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitDB 初始化数据库连接
|
|
||||||
func (c *Config) InitDB() error {
|
|
||||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s",
|
|
||||||
getEnv("DB_HOST", "localhost"),
|
|
||||||
getEnv("DB_USER", "postgres"),
|
|
||||||
getEnv("DB_PASSWORD", "postgres"),
|
|
||||||
getEnv("DB_NAME", "appdb"),
|
|
||||||
getEnv("DB_PORT", "5432"),
|
|
||||||
getEnv("DB_SSL_MODE", "disable"),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 配置 GORM
|
|
||||||
config := &gorm.Config{
|
|
||||||
Logger: logger.Default.LogMode(logger.Info),
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("APP_ENV") == "production" {
|
|
||||||
config.Logger = logger.Default.LogMode(logger.Silent)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.Open(dsn), config)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取底层的 sqlDB
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get sqlDB: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置连接池
|
|
||||||
sqlDB.SetMaxIdleConns(10)
|
|
||||||
sqlDB.SetMaxOpenConns(100)
|
|
||||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
|
||||||
|
|
||||||
c.DB = db
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseDB 关闭数据库连接
|
|
||||||
func (c *Config) CloseDB() error {
|
|
||||||
if c.DB != nil {
|
|
||||||
sqlDB, err := c.DB.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return sqlDB.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitRedis 初始化 Redis 连接
|
|
||||||
func (c *Config) InitRedis() error {
|
|
||||||
opt := &redis.Options{
|
|
||||||
Addr: fmt.Sprintf("%s:%s",
|
|
||||||
getEnv("REDIS_HOST", "localhost"),
|
|
||||||
getEnv("REDIS_PORT", "6379"),
|
|
||||||
),
|
|
||||||
Password: getEnv("REDIS_PASSWORD", ""),
|
|
||||||
DB: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
client := redis.NewClient(opt)
|
|
||||||
|
|
||||||
// 测试连接
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := client.Ping(ctx).Err(); err != nil {
|
|
||||||
return fmt.Errorf("failed to connect to redis: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Redis = client
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseRedis 关闭 Redis 连接
|
|
||||||
func (c *Config) CloseRedis() error {
|
|
||||||
if c.Redis != nil {
|
|
||||||
return c.Redis.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getEnv 获取环境变量,如果不存在则返回默认值
|
|
||||||
func getEnv(key, defaultValue string) string {
|
|
||||||
if value := os.Getenv(key); value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module api
|
|
||||||
|
|
||||||
go 1.25
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gin-gonic/gin v1.10.0
|
|
||||||
github.com/redis/go-redis/v9 v9.7.3
|
|
||||||
github.com/joho/godotenv v1.5.1
|
|
||||||
gorm.io/driver/postgres v1.5.11
|
|
||||||
gorm.io/gorm v1.25.12
|
|
||||||
)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"api/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handler 处理器
|
|
||||||
type Handler struct {
|
|
||||||
cfg *config.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// New 创建处理器实例
|
|
||||||
func New(cfg *config.Config) *Handler {
|
|
||||||
return &Handler{
|
|
||||||
cfg: cfg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HealthResponse 健康检查响应
|
|
||||||
type HealthResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Time string `json:"time"`
|
|
||||||
Services map[string]string `json:"services"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HealthCheck 健康检查
|
|
||||||
func (h *Handler) HealthCheck(c *gin.Context) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
response := HealthResponse{
|
|
||||||
Status: "ok",
|
|
||||||
Time: time.Now().Format(time.RFC3339),
|
|
||||||
Services: make(map[string]string),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查数据库连接
|
|
||||||
sqlDB, err := h.cfg.DB.DB()
|
|
||||||
if err != nil {
|
|
||||||
response.Services["database"] = "error: " + err.Error()
|
|
||||||
response.Status = "degraded"
|
|
||||||
} else {
|
|
||||||
if err := sqlDB.PingContext(ctx); err != nil {
|
|
||||||
response.Services["database"] = "error: " + err.Error()
|
|
||||||
response.Status = "degraded"
|
|
||||||
} else {
|
|
||||||
response.Services["database"] = "ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 Redis 连接
|
|
||||||
if err := h.cfg.Redis.Ping(ctx).Err(); err != nil {
|
|
||||||
response.Services["redis"] = "error: " + err.Error()
|
|
||||||
response.Status = "degraded"
|
|
||||||
} else {
|
|
||||||
response.Services["redis"] = "ok"
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.Status == "ok" {
|
|
||||||
c.JSON(http.StatusOK, response)
|
|
||||||
} else {
|
|
||||||
c.JSON(http.StatusServiceUnavailable, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping 简单的 ping 测试
|
|
||||||
func (h *Handler) Ping(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"message": "pong",
|
|
||||||
"time": time.Now().Format(time.RFC3339),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UserRequest 用户请求
|
|
||||||
type UserRequest struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Email string `json:"email" binding:"required,email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserResponse 用户响应
|
|
||||||
type UserResponse struct {
|
|
||||||
ID uint `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListUsers 获取用户列表
|
|
||||||
func (h *Handler) ListUsers(c *gin.Context) {
|
|
||||||
// TODO: 实现从数据库获取用户列表
|
|
||||||
users := []UserResponse{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
Name: "张三",
|
|
||||||
Email: "zhangsan@example.com",
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
Name: "李四",
|
|
||||||
Email: "lisi@example.com",
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"data": users,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUser 获取单个用户
|
|
||||||
func (h *Handler) GetUser(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
|
|
||||||
// TODO: 实现从数据库获取用户
|
|
||||||
// 尝试从缓存获取
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
cached, err := h.cfg.Redis.Get(ctx, "user:"+id).Result()
|
|
||||||
if err == nil && cached != "" {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"data": cached,
|
|
||||||
"cached": true,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟从数据库获取
|
|
||||||
user := UserResponse{
|
|
||||||
ID: 1,
|
|
||||||
Name: "张三",
|
|
||||||
Email: "zhangsan@example.com",
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"data": user,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateUser 创建用户
|
|
||||||
func (h *Handler) CreateUser(c *gin.Context) {
|
|
||||||
var req UserRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
|
||||||
"code": 400,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 实现保存到数据库
|
|
||||||
|
|
||||||
user := UserResponse{
|
|
||||||
ID: 1,
|
|
||||||
Name: req.Name,
|
|
||||||
Email: req.Email,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"data": user,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUser 更新用户
|
|
||||||
func (h *Handler) UpdateUser(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
|
|
||||||
var req UserRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
|
||||||
"code": 400,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 实现更新数据库
|
|
||||||
|
|
||||||
_ = id
|
|
||||||
user := UserResponse{
|
|
||||||
ID: 1,
|
|
||||||
Name: req.Name,
|
|
||||||
Email: req.Email,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"data": user,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteUser 删除用户
|
|
||||||
func (h *Handler) DeleteUser(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
|
|
||||||
// TODO: 实现从数据库删除
|
|
||||||
_ = id
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"code": 0,
|
|
||||||
"message": "用户已删除",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"api/config"
|
|
||||||
"api/router"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// 加载环境变量
|
|
||||||
if err := godotenv.Load(); err != nil {
|
|
||||||
log.Println("No .env file found, using environment variables")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置运行模式
|
|
||||||
if os.Getenv("APP_ENV") == "production" {
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
} else {
|
|
||||||
gin.SetMode(gin.DebugMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化配置
|
|
||||||
cfg := config.New()
|
|
||||||
|
|
||||||
// 初始化数据库连接
|
|
||||||
if err := cfg.InitDB(); err != nil {
|
|
||||||
log.Fatalf("Failed to initialize database: %v", err)
|
|
||||||
}
|
|
||||||
defer cfg.CloseDB()
|
|
||||||
|
|
||||||
// 初始化 Redis 连接
|
|
||||||
if err := cfg.InitRedis(); err != nil {
|
|
||||||
log.Fatalf("Failed to initialize redis: %v", err)
|
|
||||||
}
|
|
||||||
defer cfg.CloseRedis()
|
|
||||||
|
|
||||||
// 创建 Gin 引擎
|
|
||||||
r := gin.New()
|
|
||||||
|
|
||||||
// 使用中间件
|
|
||||||
r.Use(gin.Logger())
|
|
||||||
r.Use(gin.Recovery())
|
|
||||||
|
|
||||||
// 注册路由
|
|
||||||
router.RegisterRoutes(r, cfg)
|
|
||||||
|
|
||||||
// 获取端口
|
|
||||||
port := os.Getenv("APP_PORT")
|
|
||||||
if port == "" {
|
|
||||||
port = "8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 HTTP 服务器
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: fmt.Sprintf(":%s", port),
|
|
||||||
Handler: r,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优雅启停
|
|
||||||
go func() {
|
|
||||||
log.Printf("Server starting on port %s", port)
|
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Fatalf("Failed to start server: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待中断信号
|
|
||||||
quit := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-quit
|
|
||||||
|
|
||||||
log.Println("Shutting down server...")
|
|
||||||
|
|
||||||
// 设置关闭超时
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
|
||||||
log.Fatalf("Server forced to shutdown: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Server exited")
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// User 用户模型
|
|
||||||
type User struct {
|
|
||||||
ID uint `gorm:"primaryKey" json:"id"`
|
|
||||||
Name string `gorm:"size:100;not null" json:"name"`
|
|
||||||
Email string `gorm:"size:100;uniqueIndex;not null" json:"email"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableName 指定表名
|
|
||||||
func (User) TableName() string {
|
|
||||||
return "users"
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"api/config"
|
|
||||||
"api/handlers"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RegisterRoutes 注册路由
|
|
||||||
func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
|
|
||||||
// 创建处理器
|
|
||||||
h := handlers.New(cfg)
|
|
||||||
|
|
||||||
// 健康检查
|
|
||||||
r.GET("/health", h.HealthCheck)
|
|
||||||
|
|
||||||
// API v1 路由组
|
|
||||||
v1 := r.Group("/api/v1")
|
|
||||||
{
|
|
||||||
// 示例路由
|
|
||||||
v1.GET("/ping", h.Ping)
|
|
||||||
|
|
||||||
// 用户相关路由
|
|
||||||
user := v1.Group("/users")
|
|
||||||
{
|
|
||||||
user.GET("", h.ListUsers)
|
|
||||||
user.POST("", h.CreateUser)
|
|
||||||
user.GET("/:id", h.GetUser)
|
|
||||||
user.PUT("/:id", h.UpdateUser)
|
|
||||||
user.DELETE("/:id", h.DeleteUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 404 处理
|
|
||||||
r.NoRoute(func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusNotFound, gin.H{
|
|
||||||
"code": 404,
|
|
||||||
"message": "Not Found",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
# API 服务
|
|
||||||
api:
|
|
||||||
build:
|
|
||||||
context: ./api
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: api_service
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
environment:
|
|
||||||
- APP_ENV=development
|
|
||||||
- APP_PORT=8080
|
|
||||||
- DB_HOST=postgres
|
|
||||||
- DB_PORT=5432
|
|
||||||
- DB_USER=${DB_USER:-postgres}
|
|
||||||
- DB_PASSWORD=${DB_PASSWORD:-postgres}
|
|
||||||
- DB_NAME=${DB_NAME:-appdb}
|
|
||||||
- DB_SSL_MODE=disable
|
|
||||||
- REDIS_HOST=redis
|
|
||||||
- REDIS_PORT=6379
|
|
||||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
|
||||||
- REDIS_DB=0
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# PostgreSQL 数据库
|
|
||||||
postgres:
|
|
||||||
image: postgres:18.3-alpine3.23
|
|
||||||
container_name: postgres_db
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=${DB_USER:-postgres}
|
|
||||||
- POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
|
|
||||||
- POSTGRES_DB=${DB_NAME:-appdb}
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
- ./migrations:/docker-entrypoint-initdb.d
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-appdb}"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# Redis 缓存
|
|
||||||
redis:
|
|
||||||
image: redis:8.6.2-alpine
|
|
||||||
container_name: redis_cache
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
command: redis-server --appendonly yes
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
redis_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
app-network:
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
-- 初始化数据库
|
|
||||||
-- 创建用户表
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
email VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
deleted_at TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 创建索引
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
|
||||||
|
|
||||||
-- 插入测试数据
|
|
||||||
INSERT INTO users (name, email) VALUES
|
|
||||||
('张三', 'zhangsan@example.com'),
|
|
||||||
('李四', 'lisi@example.com')
|
|
||||||
ON CONFLICT (email) DO NOTHING;
|
|
||||||
0
frontend/README.md
Normal file
0
frontend/README.md
Normal file
Reference in New Issue
Block a user