添加 API 模块和更新忽略配置
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -370,6 +370,7 @@ xcuserdata/
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
@@ -544,3 +545,4 @@ google-services.json
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
api/desc.md
|
||||
|
||||
17
api/.env.example
Normal file
17
api/.env.example
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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
|
||||
86
api/Makefile
Normal file
86
api/Makefile
Normal file
@@ -0,0 +1,86 @@
|
||||
# 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 服务"
|
||||
163
api/README.md
Normal file
163
api/README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 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. 配置适当的日志和监控
|
||||
54
api/api/Dockerfile
Normal file
54
api/api/Dockerfile
Normal file
@@ -0,0 +1,54 @@
|
||||
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"]
|
||||
117
api/api/config/config.go
Normal file
117
api/api/config/config.go
Normal file
@@ -0,0 +1,117 @@
|
||||
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
|
||||
}
|
||||
11
api/api/go.mod
Normal file
11
api/api/go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
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
|
||||
)
|
||||
17
api/api/handlers/handler.go
Normal file
17
api/api/handlers/handler.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api/config"
|
||||
)
|
||||
|
||||
// Handler 处理器
|
||||
type Handler struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// New 创建处理器实例
|
||||
func New(cfg *config.Config) *Handler {
|
||||
return &Handler{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
64
api/api/handlers/health.go
Normal file
64
api/api/handlers/health.go
Normal file
@@ -0,0 +1,64 @@
|
||||
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),
|
||||
})
|
||||
}
|
||||
154
api/api/handlers/user.go
Normal file
154
api/api/handlers/user.go
Normal file
@@ -0,0 +1,154 @@
|
||||
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": "用户已删除",
|
||||
})
|
||||
}
|
||||
94
api/api/main.go
Normal file
94
api/api/main.go
Normal file
@@ -0,0 +1,94 @@
|
||||
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")
|
||||
}
|
||||
22
api/api/models/user.go
Normal file
22
api/api/models/user.go
Normal file
@@ -0,0 +1,22 @@
|
||||
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"
|
||||
}
|
||||
44
api/api/router/router.go
Normal file
44
api/api/router/router.go
Normal file
@@ -0,0 +1,44 @@
|
||||
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",
|
||||
})
|
||||
})
|
||||
}
|
||||
80
api/docker-compose.yml
Normal file
80
api/docker-compose.yml
Normal file
@@ -0,0 +1,80 @@
|
||||
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
|
||||
20
api/migrations/001_init.sql
Normal file
20
api/migrations/001_init.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- 初始化数据库
|
||||
-- 创建用户表
|
||||
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;
|
||||
Reference in New Issue
Block a user