diff --git a/.gitignore b/.gitignore index 6fe10cd..f4fd8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -368,7 +368,8 @@ xcuserdata/ .LSOverride # Icon must end with two \r -Icon +Icon + # Thumbnails ._* @@ -544,3 +545,4 @@ google-services.json # Android Profiling *.hprof +api/desc.md diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..fd0eefb --- /dev/null +++ b/api/.env.example @@ -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 diff --git a/api/Makefile b/api/Makefile new file mode 100644 index 0000000..4fd4e3f --- /dev/null +++ b/api/Makefile @@ -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 服务" diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..210a13b --- /dev/null +++ b/api/README.md @@ -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. 配置适当的日志和监控 diff --git a/api/api/Dockerfile b/api/api/Dockerfile new file mode 100644 index 0000000..3775062 --- /dev/null +++ b/api/api/Dockerfile @@ -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"] diff --git a/api/api/config/config.go b/api/api/config/config.go new file mode 100644 index 0000000..0988f55 --- /dev/null +++ b/api/api/config/config.go @@ -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 +} diff --git a/api/api/go.mod b/api/api/go.mod new file mode 100644 index 0000000..e62be2a --- /dev/null +++ b/api/api/go.mod @@ -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 +) diff --git a/api/api/handlers/handler.go b/api/api/handlers/handler.go new file mode 100644 index 0000000..1b7ac60 --- /dev/null +++ b/api/api/handlers/handler.go @@ -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, + } +} diff --git a/api/api/handlers/health.go b/api/api/handlers/health.go new file mode 100644 index 0000000..9b2163a --- /dev/null +++ b/api/api/handlers/health.go @@ -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), + }) +} diff --git a/api/api/handlers/user.go b/api/api/handlers/user.go new file mode 100644 index 0000000..7c5edba --- /dev/null +++ b/api/api/handlers/user.go @@ -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": "用户已删除", + }) +} diff --git a/api/api/main.go b/api/api/main.go new file mode 100644 index 0000000..e3ca66e --- /dev/null +++ b/api/api/main.go @@ -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") +} diff --git a/api/api/models/user.go b/api/api/models/user.go new file mode 100644 index 0000000..54c3206 --- /dev/null +++ b/api/api/models/user.go @@ -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" +} diff --git a/api/api/router/router.go b/api/api/router/router.go new file mode 100644 index 0000000..be5fc86 --- /dev/null +++ b/api/api/router/router.go @@ -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", + }) + }) +} diff --git a/api/docker-compose.yml b/api/docker-compose.yml new file mode 100644 index 0000000..f4ff6ec --- /dev/null +++ b/api/docker-compose.yml @@ -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 diff --git a/api/migrations/001_init.sql b/api/migrations/001_init.sql new file mode 100644 index 0000000..663d730 --- /dev/null +++ b/api/migrations/001_init.sql @@ -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;