重命名 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

17
backend/.env.example Normal file
View 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
backend/Makefile Normal file
View 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
backend/README.md Normal file
View 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
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",
})
})
}

9
backend/desc.md Normal file
View File

@@ -0,0 +1,9 @@
我需要你搭建一个后台 restful api 的单机多服务工程,涉及技术:
- golang:1.25.8-alpine3.23
- redis:8.6.2-alpine
- postgres:18.3-alpine3.23
先搭建好工程,不用考虑业务逻辑。

View 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

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