重命名 api 文件夹为 backend
This commit is contained in:
54
backend/api/Dockerfile
Normal file
54
backend/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
backend/api/config/config.go
Normal file
117
backend/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
backend/api/go.mod
Normal file
11
backend/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
backend/api/handlers/handler.go
Normal file
17
backend/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
backend/api/handlers/health.go
Normal file
64
backend/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
backend/api/handlers/user.go
Normal file
154
backend/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
backend/api/main.go
Normal file
94
backend/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
backend/api/models/user.go
Normal file
22
backend/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
backend/api/router/router.go
Normal file
44
backend/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",
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user