重命名 api 文件夹为 backend

This commit is contained in:
fish
2026-04-08 21:50:48 +08:00
parent 4055747c6e
commit 2063a2d757
15 changed files with 9 additions and 0 deletions

54
backend/api/Dockerfile Normal file
View 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"]

View 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
View 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
)

View 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,
}
}

View 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),
})
}

View 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
View 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")
}

View 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"
}

View 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",
})
})
}