【Go语言学习系列27】第二阶段项目实战:RESTful API服务

📚 原创系列: “Go语言学习系列”

🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。

🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。

📑 Go语言学习系列导航

本文是【Go语言学习系列】的第27篇,当前位于第二阶段(基础巩固篇)

🚀 第二阶段:基础巩固篇
  1. 13-包管理深入理解
  2. 14-标准库探索(一):io与文件操作
  3. 15-标准库探索(二):字符串处理
  4. 16-标准库探索(三):时间与日期
  5. 17-标准库探索(四):JSON处理
  6. 18-标准库探索(五):HTTP客户端
  7. 19-标准库探索(六):HTTP服务器
  8. 20-单元测试基础
  9. 21-基准测试与性能剖析入门
  10. 22-反射机制基础
  11. 23-Go中的面向对象编程
  12. 24-函数式编程在Go中的应用
  13. 25-context包详解
  14. 26-依赖注入与控制反转
  15. 27-第二阶段项目实战:RESTful API服务 👈 当前位置

📚 查看完整Go语言学习系列导航

📖 文章导读

在本文中,您将了解:

  • RESTful API设计原则与最佳实践
  • 使用标准库与第三方框架构建API服务
  • 数据库集成与ORM技术应用
  • 认证鉴权与中间件实现
  • 日志、监控与性能优化
  • 项目结构组织与代码质量保障

完成本阶段的学习后,我们将通过一个实际项目来巩固和应用所学知识。本文将带您从零开始构建一个完整的RESTful API服务,集成我们在前面章节中学习的各种技术,打造一个生产级别的后端应用。

1. 项目概述

1.1 项目目标

我们将构建一个简单但功能完整的图书管理系统API,具有以下功能:

  • 图书的增删改查(CRUD)操作
  • 用户注册和登录
  • 基于JWT的认证授权
  • 中间件实现(日志、恢复、跨域等)
  • 与MySQL数据库的集成

1.2 技术栈选择

本项目将使用以下技术栈:

  • Web框架Gin - 一个轻量级高性能的HTTP Web框架
  • ORMGORM - Go语言的ORM库,简化数据库操作
  • 数据库:MySQL - 广泛使用的关系型数据库
  • 认证:JWT (JSON Web Token) - 基于Token的认证机制
  • 依赖管理:Go Modules - Go官方的依赖管理工具
  • 配置管理:Viper - 灵活的配置管理工具

1.3 项目结构

我们将采用一种简化的领域驱动设计(DDD)风格组织项目结构:

bookstore-api/
  ├── cmd/
  │   └── server/
  │       └── main.go              # 应用程序入口
  ├── configs/
  │   └── config.yaml              # 配置文件
  ├── internal/
  │   ├── api/                     # API层(控制器)
  │   │   ├── handlers/            # 请求处理器
  │   │   │   ├── auth.go          # 认证相关处理器
  │   │   │   ├── book.go          # 图书相关处理器
  │   │   │   └── user.go          # 用户相关处理器
  │   │   ├── middlewares/         # 中间件
  │   │   │   ├── auth.go          # 认证中间件
  │   │   │   ├── cors.go          # 跨域中间件
  │   │   │   ├── logger.go        # 日志中间件
  │   │   │   └── recovery.go      # 恢复中间件
  │   │   └── routes/              # 路由定义
  │   │       └── routes.go        # API路由配置
  │   ├── config/                  # 配置加载
  │   │   └── config.go            # 配置结构和加载函数
  │   ├── domain/                  # 领域层
  │   │   ├── models/              # 模型定义
  │   │   │   ├── book.go          # 图书模型
  │   │   │   └── user.go          # 用户模型
  │   │   └── repositories/        # 存储接口
  │   │       ├── book.go          # 图书存储接口
  │   │       └── user.go          # 用户存储接口
  │   ├── infrastructure/          # 基础设施层
  │   │   ├── database/            # 数据库相关
  │   │   │   └── mysql.go         # MySQL连接和初始化
  │   │   └── persistence/         # 持久化实现
  │   │       ├── mysql/           # MySQL实现
  │   │       │   ├── book.go      # 图书存储实现
  │   │       │   └── user.go      # 用户存储实现
  │   │       └── cache/           # 缓存实现(可选)
  │   └── services/                # 服务层
  │       ├── auth.go              # 认证服务
  │       ├── book.go              # 图书服务
  │       └── user.go              # 用户服务
  ├── pkg/                         # 公共包
  │   ├── jwt/                     # JWT工具
  │   │   └── jwt.go               # JWT生成和验证
  │   ├── utils/                   # 工具函数
  │   │   ├── hash.go              # 密码哈希
  │   │   └── validator.go         # 数据验证
  │   └── errors/                  # 错误处理
  │       └── errors.go            # 自定义错误
  ├── go.mod                       # Go模块定义
  └── go.sum                       # 依赖校验和

这种结构清晰地分离了关注点,使代码组织更加清晰:

  • cmd 包含应用程序的入口点
  • configs 存放配置文件
  • internal 包含应用程序内部代码,不会被外部导入
  • pkg 包含可被外部项目复用的代码

2. RESTful API设计

在开始编码之前,让我们设计我们的API接口。RESTful API是一种基于HTTP协议、遵循REST原则的API设计风格。

2.1 REST原则回顾

REST (Representational State Transfer) 是一种架构风格,基于以下几个核心原则:

  1. 资源导向:API围绕资源设计,资源由URI标识
  2. HTTP方法语义:使用HTTP方法表达对资源的操作意图
    • GET:获取资源
    • POST:创建资源
    • PUT/PATCH:更新资源
    • DELETE:删除资源
  3. 表现层:资源可以有多种表现形式(如JSON、XML等)
  4. 无状态:服务器不存储客户端的状态,每个请求包含所有必要信息
  5. HATEOAS:超媒体作为应用状态引擎(可选)

2.2 API端点设计

根据REST原则,我们设计以下API端点:

认证相关
方法路径描述请求体响应
POST/api/register注册新用户{name, email, password}{id, name, email, token}
POST/api/login用户登录{email, password}{token}
用户相关
方法路径描述请求体响应
GET/api/users获取所有用户-[{id, name, email}]
GET/api/users/{id}获取特定用户-{id, name, email}
PUT/api/users/{id}更新用户信息{name, email, password}{id, name, email}
DELETE/api/users/{id}删除用户-{message}
图书相关
方法路径描述请求体响应
GET/api/books获取所有图书-[{id, title, author, …}]
GET/api/books/{id}获取特定图书-{id, title, author, …}
POST/api/books创建新图书{title, author, isbn, …}{id, title, author, …}
PUT/api/books/{id}更新图书信息{title, author, isbn, …}{id, title, author, …}
DELETE/api/books/{id}删除图书-{message}

2.3 请求和响应格式

我们将使用JSON作为数据交换格式。响应结构统一为:

{
  "success": true,
  "message": "操作成功",
  "data": {
    // 实际数据
  },
  "error": null
}

错误响应结构:

{
  "success": false,
  "message": "错误信息",
  "data": null,
  "error": {
    "code": "ERROR_CODE",
    "details": "详细错误信息"
  }
}

2.4 状态码使用

我们将遵循HTTP状态码的标准语义:

  • 200 OK:请求成功
  • 201 Created:资源创建成功
  • 400 Bad Request:请求参数错误
  • 401 Unauthorized:认证失败
  • 403 Forbidden:权限不足
  • 404 Not Found:资源不存在
  • 500 Internal Server Error:服务器内部错误

3. 项目初始化与配置

现在,让我们开始实际的项目构建,首先初始化项目并设置配置系统。

3.1 初始化Go模块

首先,创建项目目录并初始化Go模块:

mkdir -p bookstore-api
cd bookstore-api
go mod init github.com/yourusername/bookstore-api

3.2 目录结构创建

根据前面设计的项目结构,创建相应的目录:

mkdir -p cmd/server configs internal/{api/{handlers,middlewares,routes},config,domain/{models,repositories},infrastructure/{database,persistence/mysql},services} pkg/{jwt,utils,errors}

3.3 配置管理

我们使用Viper库来管理配置。首先安装依赖:

go get github.com/spf13/viper

创建配置文件 configs/config.yaml

server:
  port: 8080
  mode: debug  # debug或release

database:
  driver: mysql
  host: localhost
  port: 3306
  username: root
  password: password
  dbname: bookstore
  max_idle_conns: 10
  max_open_conns: 100
  conn_max_lifetime: 3600

jwt:
  secret: your-secret-key
  expiry: 24  # 小时

实现配置加载功能 internal/config/config.go

package config

import (
	"fmt"
	"time"

	"github.com/spf13/viper"
)

// Config 应用配置结构
type Config struct {
	Server   ServerConfig
	Database DatabaseConfig
	JWT      JWTConfig
}

// ServerConfig 服务器配置
type ServerConfig struct {
	Port int
	Mode string
}

// DatabaseConfig 数据库配置
type DatabaseConfig struct {
	Driver          string
	Host            string
	Port            int
	Username        string
	Password        string
	DBName          string
	MaxIdleConns    int           `mapstructure:"max_idle_conns"`
	MaxOpenConns    int           `mapstructure:"max_open_conns"`
	ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
}

// JWTConfig JWT配置
type JWTConfig struct {
	Secret string
	Expiry int // 过期时间(小时)
}

// LoadConfig 从文件加载配置
func LoadConfig(path string) (*Config, error) {
	viper.SetConfigFile(path)
	viper.AutomaticEnv()

	err := viper.ReadInConfig()
	if err != nil {
		return nil, fmt.Errorf("读取配置文件失败: %w", err)
	}

	var config Config
	err = viper.Unmarshal(&config)
	if err != nil {
		return nil, fmt.Errorf("解析配置失败: %w", err)
	}

	return &config, nil
}

3.4 数据库连接

接下来,我们设置数据库连接。首先安装GORM和MySQL驱动:

go get gorm.io/gorm
go get gorm.io/driver/mysql

实现数据库连接 internal/infrastructure/database/mysql.go

package database

import (
	"fmt"
	"time"

	"github.com/yourusername/bookstore-api/internal/config"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// NewMySQLConnection 创建MySQL连接
func NewMySQLConnection(cfg *config.DatabaseConfig) (*gorm.DB, error) {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		cfg.Username,
		cfg.Password,
		cfg.Host,
		cfg.Port,
		cfg.DBName,
	)

	// 配置GORM
	gormConfig := &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	}

	// 连接数据库
	db, err := gorm.Open(mysql.Open(dsn), gormConfig)
	if err != nil {
		return nil, fmt.Errorf("连接数据库失败: %w", err)
	}

	// 配置连接池
	sqlDB, err := db.DB()
	if err != nil {
		return nil, fmt.Errorf("获取底层DB连接失败: %w", err)
	}

	sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
	sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
	sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)

	return db, nil
}

3.5 主函数

最后,编写入口文件 cmd/server/main.go

package main

import (
	"log"
	"path/filepath"

	"github.com/yourusername/bookstore-api/internal/config"
	"github.com/yourusername/bookstore-api/internal/infrastructure/database"
)

func main() {
	// 加载配置
	configPath := filepath.Join("configs", "config.yaml")
	cfg, err := config.LoadConfig(configPath)
	if err != nil {
		log.Fatalf("加载配置失败: %v", err)
	}

	// 连接数据库
	db, err := database.NewMySQLConnection(&cfg.Database)
	if err != nil {
		log.Fatalf("连接数据库失败: %v", err)
	}

	log.Println("配置加载成功,数据库连接成功")

	// 接下来的代码将在后面实现...
}

到此为止,我们已经完成了项目的初始化和基础配置。在下一部分中,我们将开始实现数据模型和存储层。

4. 数据模型与存储层实现

4.1 领域模型定义

首先,我们需要定义应用的核心数据模型。

用户模型 internal/domain/models/user.go
package models

import (
	"time"

	"gorm.io/gorm"
)

// User 用户模型
type User struct {
	ID        uint           `json:"id" gorm:"primaryKey"`
	Name      string         `json:"name" gorm:"size:100;not null"`
	Email     string         `json:"email" gorm:"size:100;uniqueIndex;not null"`
	Password  string         `json:"-" gorm:"size:100;not null"` // json:"-" 确保密码不会被序列化
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 软删除支持
}
图书模型 internal/domain/models/book.go
package models

import (
	"time"

	"gorm.io/gorm"
)

// Book 图书模型
type Book struct {
	ID          uint           `json:"id" gorm:"primaryKey"`
	Title       string         `json:"title" gorm:"size:200;not null"`
	Author      string         `json:"author" gorm:"size:100;not null"`
	ISBN        string         `json:"isbn" gorm:"size:20;uniqueIndex;not null"`
	Description string         `json:"description" gorm:"type:text"`
	Price       float64        `json:"price" gorm:"type:decimal(10,2);not null"`
	PublishedAt time.Time      `json:"published_at"`
	CreatedAt   time.Time      `json:"created_at"`
	UpdatedAt   time.Time      `json:"updated_at"`
	DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"` // 软删除支持
}

4.2 数据库迁移

接下来,我们需要创建数据库迁移代码来初始化数据库模式:

internal/infrastructure/database/migrate.go

package database

import (
	"github.com/yourusername/bookstore-api/internal/domain/models"
	"gorm.io/gorm"
)

// AutoMigrate 自动迁移数据库结构
func AutoMigrate(db *gorm.DB) error {
	return db.AutoMigrate(
		&models.User{},
		&models.Book{},
	)
}

main.go中添加迁移代码:

// 在数据库连接之后添加
if err := database.AutoMigrate(db); err != nil {
    log.Fatalf("数据库迁移失败: %v", err)
}

4.3 存储层接口

存储层接口定义了模型的持久化操作。

用户存储接口 internal/domain/repositories/user.go
package repositories

import (
	"github.com/yourusername/bookstore-api/internal/domain/models"
)

// UserRepository 用户存储接口
type UserRepository interface {
	Create(user *models.User) error
	GetByID(id uint) (*models.User, error)
	GetByEmail(email string) (*models.User, error)
	Update(user *models.User) error
	Delete(id uint) error
	List() ([]*models.User, error)
}
图书存储接口 internal/domain/repositories/book.go
package repositories

import (
	"github.com/yourusername/bookstore-api/internal/domain/models"
)

// BookRepository 图书存储接口
type BookRepository interface {
	Create(book *models.Book) error
	GetByID(id uint) (*models.Book, error)
	Update(book *models.Book) error
	Delete(id uint) error
	List() ([]*models.Book, error)
}

4.4 MySQL存储实现

接下来,我们实现MySQL版本的存储接口。

用户存储实现 internal/infrastructure/persistence/mysql/user.go
package mysql

import (
	"errors"

	"github.com/yourusername/bookstore-api/internal/domain/models"
	"github.com/yourusername/bookstore-api/internal/domain/repositories"
	"gorm.io/gorm"
)

// UserRepository MySQL实现的用户存储
type UserRepository struct {
	db *gorm.DB
}

// NewUserRepository 创建用户存储实例
func NewUserRepository(db *gorm.DB) repositories.UserRepository {
	return &UserRepository{db: db}
}

// Create 创建用户
func (r *UserRepository) Create(user *models.User) error {
	return r.db.Create(user).Error
}

// GetByID 通过ID获取用户
func (r *UserRepository) GetByID(id uint) (*models.User, error) {
	var user models.User
	err := r.db.First(&user, id).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil // 用户不存在
		}
		return nil, err
	}
	return &user, nil
}

// GetByEmail 通过Email获取用户
func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
	var user models.User
	err := r.db.Where("email = ?", email).First(&user).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil // 用户不存在
		}
		return nil, err
	}
	return &user, nil
}

// Update 更新用户
func (r *UserRepository) Update(user *models.User) error {
	return r.db.Save(user).Error
}

// Delete 删除用户
func (r *UserRepository) Delete(id uint) error {
	return r.db.Delete(&models.User{}, id).Error
}

// List 列出所有用户
func (r *UserRepository) List() ([]*models.User, error) {
	var users []*models.User
	err := r.db.Find(&users).Error
	return users, err
}
图书存储实现 internal/infrastructure/persistence/mysql/book.go
package mysql

import (
	"errors"

	"github.com/yourusername/bookstore-api/internal/domain/models"
	"github.com/yourusername/bookstore-api/internal/domain/repositories"
	"gorm.io/gorm"
)

// BookRepository MySQL实现的图书存储
type BookRepository struct {
	db *gorm.DB
}

// NewBookRepository 创建图书存储实例
func NewBookRepository(db *gorm.DB) repositories.BookRepository {
	return &BookRepository{db: db}
}

// Create 创建图书
func (r *BookRepository) Create(book *models.Book) error {
	return r.db.Create(book).Error
}

// GetByID 通过ID获取图书
func (r *BookRepository) GetByID(id uint) (*models.Book, error) {
	var book models.Book
	err := r.db.First(&book, id).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil // 图书不存在
		}
		return nil, err
	}
	return &book, nil
}

// Update 更新图书
func (r *BookRepository) Update(book *models.Book) error {
	return r.db.Save(book).Error
}

// Delete 删除图书
func (r *BookRepository) Delete(id uint) error {
	return r.db.Delete(&models.Book{}, id).Error
}

// List 列出所有图书
func (r *BookRepository) List() ([]*models.Book, error) {
	var books []*models.Book
	err := r.db.Find(&books).Error
	return books, err
}

5. 服务层实现

服务层实现业务逻辑,是领域模型和API层之间的桥梁。

5.1 工具包实现

首先,实现一些必要的工具函数。

JWT工具 pkg/jwt/jwt.go
package jwt

import (
	"errors"
	"time"

	"github.com/golang-jwt/jwt/v4"
)

// Claims 自定义JWT声明
type Claims struct {
	UserID uint `json:"user_id"`
	jwt.RegisteredClaims
}

// GenerateToken 生成JWT令牌
func GenerateToken(userID uint, secret string, expiry int) (string, error) {
	// 设置过期时间
	expirationTime := time.Now().Add(time.Duration(expiry) * time.Hour)
	
	// 创建声明
	claims := &Claims{
		UserID: userID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(expirationTime),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
		},
	}
	
	// 创建令牌
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	
	// 签名令牌
	tokenString, err := token.SignedString([]byte(secret))
	if err != nil {
		return "", err
	}
	
	return tokenString, nil
}

// ValidateToken 验证JWT令牌
func ValidateToken(tokenString string, secret string) (*Claims, error) {
	// 解析令牌
	token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(secret), nil
	})
	
	if err != nil {
		return nil, err
	}
	
	// 验证令牌有效性
	if !token.Valid {
		return nil, errors.New("令牌无效")
	}
	
	// 获取声明
	claims, ok := token.Claims.(*Claims)
	if !ok {
		return nil, errors.New("无法获取令牌声明")
	}
	
	return claims, nil
}
密码哈希工具 pkg/utils/hash.go
package utils

import (
	"golang.org/x/crypto/bcrypt"
)

// HashPassword 对密码进行哈希
func HashPassword(password string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
	return string(bytes), err
}

// CheckPasswordHash 验证密码
func CheckPasswordHash(password, hash string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	return err == nil
}

5.2 认证服务实现

internal/services/auth.go

package services

import (
	"errors"

	"github.com/yourusername/bookstore-api/internal/config"
	"github.com/yourusername/bookstore-api/internal/domain/models"
	"github.com/yourusername/bookstore-api/internal/domain/repositories"
	"github.com/yourusername/bookstore-api/pkg/jwt"
	"github.com/yourusername/bookstore-api/pkg/utils"
)

// AuthService 认证服务接口
type AuthService interface {
	Register(name, email, password string) (*models.User, string, error)
	Login(email, password string) (string, error)
	GetUserByID(userID uint) (*models.User, error)
}

// authService 认证服务实现
type authService struct {
	userRepo repositories.UserRepository
	config   *config.Config
}

// NewAuthService 创建认证服务实例
func NewAuthService(userRepo repositories.UserRepository, config *config.Config) AuthService {
	return &authService{
		userRepo: userRepo,
		config:   config,
	}
}

// Register 用户注册
func (s *authService) Register(name, email, password string) (*models.User, string, error) {
	// 检查邮箱是否已存在
	existingUser, err := s.userRepo.GetByEmail(email)
	if err != nil {
		return nil, "", err
	}
	if existingUser != nil {
		return nil, "", errors.New("邮箱已被注册")
	}

	// 哈希密码
	hashedPassword, err := utils.HashPassword(password)
	if err != nil {
		return nil, "", err
	}

	// 创建用户
	user := &models.User{
		Name:     name,
		Email:    email,
		Password: hashedPassword,
	}

	if err := s.userRepo.Create(user); err != nil {
		return nil, "", err
	}

	// 生成JWT令牌
	token, err := jwt.GenerateToken(user.ID, s.config.JWT.Secret, s.config.JWT.Expiry)
	if err != nil {
		return nil, "", err
	}

	return user, token, nil
}

// Login 用户登录
func (s *authService) Login(email, password string) (string, error) {
	// 通过邮箱查找用户
	user, err := s.userRepo.GetByEmail(email)
	if err != nil {
		return "", err
	}
	if user == nil {
		return "", errors.New("用户不存在")
	}

	// 验证密码
	if !utils.CheckPasswordHash(password, user.Password) {
		return "", errors.New("密码错误")
	}

	// 生成JWT令牌
	token, err := jwt.GenerateToken(user.ID, s.config.JWT.Secret, s.config.JWT.Expiry)
	if err != nil {
		return "", err
	}

	return token, nil
}

// GetUserByID 通过ID获取用户
func (s *authService) GetUserByID(userID uint) (*models.User, error) {
	return s.userRepo.GetByID(userID)
}

5.3 用户服务实现

internal/services/user.go

package services

import (
	"github.com/yourusername/bookstore-api/internal/domain/models"
	"github.com/yourusername/bookstore-api/internal/domain/repositories"
	"github.com/yourusername/bookstore-api/pkg/utils"
)

// UserService 用户服务接口
type UserService interface {
	GetByID(id uint) (*models.User, error)
	List() ([]*models.User, error)
	Update(user *models.User, updatePassword bool) error
	Delete(id uint) error
}

// userService 用户服务实现
type userService struct {
	userRepo repositories.UserRepository
}

// NewUserService 创建用户服务实例
func NewUserService(userRepo repositories.UserRepository) UserService {
	return &userService{
		userRepo: userRepo,
	}
}

// GetByID 通过ID获取用户
func (s *userService) GetByID(id uint) (*models.User, error) {
	return s.userRepo.GetByID(id)
}

// List 获取所有用户
func (s *userService) List() ([]*models.User, error) {
	return s.userRepo.List()
}

// Update 更新用户信息
func (s *userService) Update(user *models.User, updatePassword bool) error {
	// 如果需要更新密码,先对密码进行哈希
	if updatePassword && user.Password != "" {
		hashedPassword, err := utils.HashPassword(user.Password)
		if err != nil {
			return err
		}
		user.Password = hashedPassword
	}

	return s.userRepo.Update(user)
}

// Delete 删除用户
func (s *userService) Delete(id uint) error {
	return s.userRepo.Delete(id)
}

5.4 图书服务实现

internal/services/book.go

package services

import (
	"github.com/yourusername/bookstore-api/internal/domain/models"
	"github.com/yourusername/bookstore-api/internal/domain/repositories"
)

// BookService 图书服务接口
type BookService interface {
	Create(book *models.Book) error
	GetByID(id uint) (*models.Book, error)
	List() ([]*models.Book, error)
	Update(book *models.Book) error
	Delete(id uint) error
}

// bookService 图书服务实现
type bookService struct {
	bookRepo repositories.BookRepository
}

// NewBookService 创建图书服务实例
func NewBookService(bookRepo repositories.BookRepository) BookService {
	return &bookService{
		bookRepo: bookRepo,
	}
}

// Create 创建图书
func (s *bookService) Create(book *models.Book) error {
	return s.bookRepo.Create(book)
}

// GetByID 通过ID获取图书
func (s *bookService) GetByID(id uint) (*models.Book, error) {
	return s.bookRepo.GetByID(id)
}

// List 获取所有图书
func (s *bookService) List() ([]*models.Book, error) {
	return s.bookRepo.List()
}

// Update 更新图书信息
func (s *bookService) Update(book *models.Book) error {
	return s.bookRepo.Update(book)
}

// Delete 删除图书
func (s *bookService) Delete(id uint) error {
	return s.bookRepo.Delete(id)
}

6. Web API层实现

Web API层负责处理HTTP请求,包括请求解析、参数验证、调用服务层和构造响应。

6.1 API响应结构

首先,定义统一的API响应结构 internal/api/response.go

package api

// Response API统一响应格式
type Response struct {
	Success bool        `json:"success"`
	Message string      `json:"message,omitempty"`
	Data    interface{} `json:"data,omitempty"`
	Error   *ErrorInfo  `json:"error,omitempty"`
}

// ErrorInfo 错误详情
type ErrorInfo struct {
	Code    string `json:"code,omitempty"`
	Details string `json:"details,omitempty"`
}

// SuccessResponse 创建成功响应
func SuccessResponse(data interface{}, message string) Response {
	return Response{
		Success: true,
		Message: message,
		Data:    data,
	}
}

// ErrorResponse 创建错误响应
func ErrorResponse(message string, code string, details string) Response {
	return Response{
		Success: false,
		Message: message,
		Error: &ErrorInfo{
			Code:    code,
			Details: details,
		},
	}
}

6.2 中间件实现

认证中间件 internal/api/middlewares/auth.go
package middlewares

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/bookstore-api/internal/api"
	"github.com/yourusername/bookstore-api/internal/config"
	"github.com/yourusername/bookstore-api/pkg/jwt"
)

// AuthMiddleware 认证中间件
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
	return func(c *gin.Context) {
		// 从Authorization头获取令牌
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, api.ErrorResponse(
				"未提供认证令牌",
				"UNAUTHORIZED",
				"请在Authorization头部提供JWT令牌",
			))
			return
		}

		// 去掉Bearer前缀
		tokenString := strings.TrimPrefix(authHeader, "Bearer ")
		if tokenString == authHeader {
			c.AbortWithStatusJSON(http.StatusUnauthorized, api.ErrorResponse(
				"认证格式无效",
				"INVALID_FORMAT",
				"认证头部格式应为: Bearer {token}",
			))
			return
		}

		// 验证令牌
		claims, err := jwt.ValidateToken(tokenString, cfg.JWT.Secret)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusUnauthorized, api.ErrorResponse(
				"无效的认证令牌",
				"INVALID_TOKEN",
				err.Error(),
			))
			return
		}

		// 将用户ID存储到上下文中
		c.Set("userID", claims.UserID)
		c.Next()
	}
}
日志中间件 internal/api/middlewares/logger.go
package middlewares

import (
	"time"

	"github.com/gin-gonic/gin"
)

// Logger 日志中间件
func Logger() gin.HandlerFunc {
	return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
		return fmt.Sprintf("[%s] %s | %d | %s | %s\n",
			param.TimeStamp.Format(time.RFC3339),
			param.Method,
			param.StatusCode,
			param.Path,
			param.Latency,
		)
	})
}
CORS中间件 internal/api/middlewares/cors.go
package middlewares

import (
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

// CORS 跨域资源共享中间件
func CORS() gin.HandlerFunc {
	return cors.New(cors.Config{
		AllowOrigins:     []string{"*"},
		AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
		AllowHeaders:     []string{"Origin", "Content-Type", "Accept", "Authorization"},
		ExposeHeaders:    []string{"Content-Length"},
		AllowCredentials: true,
	})
}
恢复中间件 internal/api/middlewares/recovery.go
package middlewares

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/bookstore-api/internal/api"
)

// Recovery 恢复中间件
func Recovery() gin.HandlerFunc {
	return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
		c.AbortWithStatusJSON(http.StatusInternalServerError, api.ErrorResponse(
			"服务器内部错误",
			"INTERNAL_ERROR",
			"请稍后再试或联系管理员",
		))
	})
}

6.3 处理器实现

处理器负责具体的API请求处理逻辑。

认证处理器 internal/api/handlers/auth.go
package handlers

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/bookstore-api/internal/api"
	"github.com/yourusername/bookstore-api/internal/services"
)

// RegisterRequest 注册请求结构
type RegisterRequest struct {
	Name     string `json:"name" binding:"required,min=3,max=50"`
	Email    string `json:"email" binding:"required,email"`
	Password string `json:"password" binding:"required,min=6"`
}

// LoginRequest 登录请求结构
type LoginRequest struct {
	Email    string `json:"email" binding:"required,email"`
	Password string `json:"password" binding:"required"`
}

// AuthHandler 认证处理器接口
type AuthHandler interface {
	Register(c *gin.Context)
	Login(c *gin.Context)
	GetCurrentUser(c *gin.Context)
}

// authHandler 认证处理器实现
type authHandler struct {
	authService services.AuthService
}

// NewAuthHandler 创建认证处理器实例
func NewAuthHandler(authService services.AuthService) AuthHandler {
	return &authHandler{
		authService: authService,
	}
}

// Register 处理用户注册
func (h *authHandler) Register(c *gin.Context) {
	var req RegisterRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的请求参数",
			"INVALID_REQUEST",
			err.Error(),
		))
		return
	}

	user, token, err := h.authService.Register(req.Name, req.Email, req.Password)
	if err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"注册失败",
			"REGISTRATION_FAILED",
			err.Error(),
		))
		return
	}

	c.JSON(http.StatusCreated, api.SuccessResponse(gin.H{
		"id":    user.ID,
		"name":  user.Name,
		"email": user.Email,
		"token": token,
	}, "注册成功"))
}

// Login 处理用户登录
func (h *authHandler) Login(c *gin.Context) {
	var req LoginRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的请求参数",
			"INVALID_REQUEST",
			err.Error(),
		))
		return
	}

	token, err := h.authService.Login(req.Email, req.Password)
	if err != nil {
		c.JSON(http.StatusUnauthorized, api.ErrorResponse(
			"登录失败",
			"LOGIN_FAILED",
			err.Error(),
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(gin.H{
		"token": token,
	}, "登录成功"))
}

// GetCurrentUser 获取当前登录用户
func (h *authHandler) GetCurrentUser(c *gin.Context) {
	userID, exists := c.Get("userID")
	if !exists {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"无法获取用户ID",
			"USER_ID_NOT_FOUND",
			"认证中间件未正确设置用户ID",
		))
		return
	}

	user, err := h.authService.GetUserByID(userID.(uint))
	if err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"获取用户信息失败",
			"USER_FETCH_FAILED",
			err.Error(),
		))
		return
	}

	if user == nil {
		c.JSON(http.StatusNotFound, api.ErrorResponse(
			"用户不存在",
			"USER_NOT_FOUND",
			"请求的用户不存在",
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(gin.H{
		"id":    user.ID,
		"name":  user.Name,
		"email": user.Email,
	}, "获取用户信息成功"))
}
用户处理器 internal/api/handlers/user.go
package handlers

import (
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/bookstore-api/internal/api"
	"github.com/yourusername/bookstore-api/internal/domain/models"
	"github.com/yourusername/bookstore-api/internal/services"
)

// UpdateUserRequest 更新用户请求结构
type UpdateUserRequest struct {
	Name     string `json:"name" binding:"omitempty,min=3,max=50"`
	Email    string `json:"email" binding:"omitempty,email"`
	Password string `json:"password" binding:"omitempty,min=6"`
}

// UserHandler 用户处理器接口
type UserHandler interface {
	GetByID(c *gin.Context)
	List(c *gin.Context)
	Update(c *gin.Context)
	Delete(c *gin.Context)
}

// userHandler 用户处理器实现
type userHandler struct {
	userService services.UserService
}

// NewUserHandler 创建用户处理器实例
func NewUserHandler(userService services.UserService) UserHandler {
	return &userHandler{
		userService: userService,
	}
}

// GetByID 获取指定用户
func (h *userHandler) GetByID(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的用户ID",
			"INVALID_ID",
			"用户ID必须为数字",
		))
		return
	}

	user, err := h.userService.GetByID(uint(id))
	if err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"获取用户信息失败",
			"USER_FETCH_FAILED",
			err.Error(),
		))
		return
	}

	if user == nil {
		c.JSON(http.StatusNotFound, api.ErrorResponse(
			"用户不存在",
			"USER_NOT_FOUND",
			"请求的用户不存在",
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(user, "获取用户信息成功"))
}

// List 获取所有用户
func (h *userHandler) List(c *gin.Context) {
	users, err := h.userService.List()
	if err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"获取用户列表失败",
			"USER_LIST_FAILED",
			err.Error(),
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(users, "获取用户列表成功"))
}

// Update 更新用户信息
func (h *userHandler) Update(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的用户ID",
			"INVALID_ID",
			"用户ID必须为数字",
		))
		return
	}

	var req UpdateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的请求参数",
			"INVALID_REQUEST",
			err.Error(),
		))
		return
	}

	// 获取现有用户
	user, err := h.userService.GetByID(uint(id))
	if err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"获取用户信息失败",
			"USER_FETCH_FAILED",
			err.Error(),
		))
		return
	}

	if user == nil {
		c.JSON(http.StatusNotFound, api.ErrorResponse(
			"用户不存在",
			"USER_NOT_FOUND",
			"请求的用户不存在",
		))
		return
	}

	// 更新用户字段(如果提供)
	updatePassword := false
	if req.Name != "" {
		user.Name = req.Name
	}
	if req.Email != "" {
		user.Email = req.Email
	}
	if req.Password != "" {
		user.Password = req.Password
		updatePassword = true
	}

	// 保存更新
	if err := h.userService.Update(user, updatePassword); err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"更新用户信息失败",
			"USER_UPDATE_FAILED",
			err.Error(),
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(gin.H{
		"id":    user.ID,
		"name":  user.Name,
		"email": user.Email,
	}, "更新用户信息成功"))
}

// Delete 删除用户
func (h *userHandler) Delete(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的用户ID",
			"INVALID_ID",
			"用户ID必须为数字",
		))
		return
	}

	// 检查用户是否存在
	user, err := h.userService.GetByID(uint(id))
	if err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"获取用户信息失败",
			"USER_FETCH_FAILED",
			err.Error(),
		))
		return
	}

	if user == nil {
		c.JSON(http.StatusNotFound, api.ErrorResponse(
			"用户不存在",
			"USER_NOT_FOUND",
			"请求的用户不存在",
		))
		return
	}

	// 删除用户
	if err := h.userService.Delete(uint(id)); err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"删除用户失败",
			"USER_DELETE_FAILED",
			err.Error(),
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(nil, "删除用户成功"))
}
图书处理器 internal/api/handlers/book.go
package handlers

import (
	"net/http"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/bookstore-api/internal/api"
	"github.com/yourusername/bookstore-api/internal/domain/models"
	"github.com/yourusername/bookstore-api/internal/services"
)

// BookRequest 图书请求结构
type BookRequest struct {
	Title       string     `json:"title" binding:"required"`
	Author      string     `json:"author" binding:"required"`
	ISBN        string     `json:"isbn" binding:"required"`
	Description string     `json:"description"`
	Price       float64    `json:"price" binding:"required,gt=0"`
	PublishedAt *time.Time `json:"published_at"`
}

// BookHandler 图书处理器接口
type BookHandler interface {
	Create(c *gin.Context)
	GetByID(c *gin.Context)
	List(c *gin.Context)
	Update(c *gin.Context)
	Delete(c *gin.Context)
}

// bookHandler 图书处理器实现
type bookHandler struct {
	bookService services.BookService
}

// NewBookHandler 创建图书处理器实例
func NewBookHandler(bookService services.BookService) BookHandler {
	return &bookHandler{
		bookService: bookService,
	}
}

// Create 创建图书
func (h *bookHandler) Create(c *gin.Context) {
	var req BookRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的请求参数",
			"INVALID_REQUEST",
			err.Error(),
		))
		return
	}

	// 创建图书对象
	book := &models.Book{
		Title:       req.Title,
		Author:      req.Author,
		ISBN:        req.ISBN,
		Description: req.Description,
		Price:       req.Price,
	}

	// 设置出版日期(如果提供)
	if req.PublishedAt != nil {
		book.PublishedAt = *req.PublishedAt
	} else {
		book.PublishedAt = time.Now()
	}

	// 保存图书
	if err := h.bookService.Create(book); err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"创建图书失败",
			"BOOK_CREATE_FAILED",
			err.Error(),
		))
		return
	}

	c.JSON(http.StatusCreated, api.SuccessResponse(book, "创建图书成功"))
}

// GetByID 获取指定图书
func (h *bookHandler) GetByID(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的图书ID",
			"INVALID_ID",
			"图书ID必须为数字",
		))
		return
	}

	book, err := h.bookService.GetByID(uint(id))
	if err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"获取图书信息失败",
			"BOOK_FETCH_FAILED",
			err.Error(),
		))
		return
	}

	if book == nil {
		c.JSON(http.StatusNotFound, api.ErrorResponse(
			"图书不存在",
			"BOOK_NOT_FOUND",
			"请求的图书不存在",
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(book, "获取图书信息成功"))
}

// List 获取所有图书
func (h *bookHandler) List(c *gin.Context) {
	books, err := h.bookService.List()
	if err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"获取图书列表失败",
			"BOOK_LIST_FAILED",
			err.Error(),
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(books, "获取图书列表成功"))
}

// Update 更新图书信息
func (h *bookHandler) Update(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的图书ID",
			"INVALID_ID",
			"图书ID必须为数字",
		))
		return
	}

	var req BookRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的请求参数",
			"INVALID_REQUEST",
			err.Error(),
		))
		return
	}

	// 获取现有图书
	book, err := h.bookService.GetByID(uint(id))
	if err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"获取图书信息失败",
			"BOOK_FETCH_FAILED",
			err.Error(),
		))
		return
	}

	if book == nil {
		c.JSON(http.StatusNotFound, api.ErrorResponse(
			"图书不存在",
			"BOOK_NOT_FOUND",
			"请求的图书不存在",
		))
		return
	}

	// 更新图书字段
	book.Title = req.Title
	book.Author = req.Author
	book.ISBN = req.ISBN
	book.Description = req.Description
	book.Price = req.Price
	if req.PublishedAt != nil {
		book.PublishedAt = *req.PublishedAt
	}

	// 保存更新
	if err := h.bookService.Update(book); err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"更新图书信息失败",
			"BOOK_UPDATE_FAILED",
			err.Error(),
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(book, "更新图书信息成功"))
}

// Delete 删除图书
func (h *bookHandler) Delete(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		c.JSON(http.StatusBadRequest, api.ErrorResponse(
			"无效的图书ID",
			"INVALID_ID",
			"图书ID必须为数字",
		))
		return
	}

	// 检查图书是否存在
	book, err := h.bookService.GetByID(uint(id))
	if err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"获取图书信息失败",
			"BOOK_FETCH_FAILED",
			err.Error(),
		))
		return
	}

	if book == nil {
		c.JSON(http.StatusNotFound, api.ErrorResponse(
			"图书不存在",
			"BOOK_NOT_FOUND",
			"请求的图书不存在",
		))
		return
	}

	// 删除图书
	if err := h.bookService.Delete(uint(id)); err != nil {
		c.JSON(http.StatusInternalServerError, api.ErrorResponse(
			"删除图书失败",
			"BOOK_DELETE_FAILED",
			err.Error(),
		))
		return
	}

	c.JSON(http.StatusOK, api.SuccessResponse(nil, "删除图书成功"))
}

6.4 路由设置

创建路由配置文件 internal/api/routes/routes.go

package routes

import (
	"github.com/gin-gonic/gin"
	"github.com/yourusername/bookstore-api/internal/api/handlers"
	"github.com/yourusername/bookstore-api/internal/api/middlewares"
	"github.com/yourusername/bookstore-api/internal/config"
)

// SetupRoutes 设置API路由
func SetupRoutes(
	router *gin.Engine,
	cfg *config.Config,
	authHandler handlers.AuthHandler,
	userHandler handlers.UserHandler,
	bookHandler handlers.BookHandler,
) {
	// 全局中间件
	router.Use(middlewares.Logger())
	router.Use(middlewares.Recovery())
	router.Use(middlewares.CORS())

	// 健康检查
	router.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{"status": "ok"})
	})

	// API路由组
	api := router.Group("/api")
	{
		// 认证相关路由(不需要认证)
		api.POST("/register", authHandler.Register)
		api.POST("/login", authHandler.Login)

		// 需要认证的路由
		authenticated := api.Group("")
		authenticated.Use(middlewares.AuthMiddleware(cfg))
		{
			// 当前用户
			authenticated.GET("/me", authHandler.GetCurrentUser)

			// 用户管理
			users := authenticated.Group("/users")
			{
				users.GET("", userHandler.List)
				users.GET("/:id", userHandler.GetByID)
				users.PUT("/:id", userHandler.Update)
				users.DELETE("/:id", userHandler.Delete)
			}

			// 图书管理
			books := authenticated.Group("/books")
			{
				books.POST("", bookHandler.Create)
				books.GET("", bookHandler.List)
				books.GET("/:id", bookHandler.GetByID)
				books.PUT("/:id", bookHandler.Update)
				books.DELETE("/:id", bookHandler.Delete)
			}
		}
	}
}

7. 主程序实现

现在,让我们完成 cmd/server/main.go 文件,将所有组件连接起来:

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/bookstore-api/internal/api/handlers"
	"github.com/yourusername/bookstore-api/internal/api/routes"
	"github.com/yourusername/bookstore-api/internal/config"
	"github.com/yourusername/bookstore-api/internal/infrastructure/database"
	"github.com/yourusername/bookstore-api/internal/infrastructure/persistence/mysql"
	"github.com/yourusername/bookstore-api/internal/services"
)

func main() {
	// 加载配置
	configPath := filepath.Join("configs", "config.yaml")
	cfg, err := config.LoadConfig(configPath)
	if err != nil {
		log.Fatalf("加载配置失败: %v", err)
	}

	// 设置Gin模式
	if cfg.Server.Mode == "release" {
		gin.SetMode(gin.ReleaseMode)
	}

	// 连接数据库
	db, err := database.NewMySQLConnection(&cfg.Database)
	if err != nil {
		log.Fatalf("连接数据库失败: %v", err)
	}

	// 数据库迁移
	if err := database.AutoMigrate(db); err != nil {
		log.Fatalf("数据库迁移失败: %v", err)
	}

	// 初始化存储层
	userRepo := mysql.NewUserRepository(db)
	bookRepo := mysql.NewBookRepository(db)

	// 初始化服务层
	authService := services.NewAuthService(userRepo, cfg)
	userService := services.NewUserService(userRepo)
	bookService := services.NewBookService(bookRepo)

	// 初始化处理器
	authHandler := handlers.NewAuthHandler(authService)
	userHandler := handlers.NewUserHandler(userService)
	bookHandler := handlers.NewBookHandler(bookService)

	// 创建Gin引擎
	router := gin.Default()

	// 设置路由
	routes.SetupRoutes(router, cfg, authHandler, userHandler, bookHandler)

	// 启动服务器
	serverAddr := fmt.Sprintf(":%d", cfg.Server.Port)
	log.Printf("服务器启动在 %s", serverAddr)
	if err := router.Run(serverAddr); err != nil {
		log.Fatalf("启动服务器失败: %v", err)
	}
}

8. 项目测试

现在我们可以测试我们的API了。首先,确保MySQL已安装并运行,然后启动服务器:

go run cmd/server/main.go

以下是一些测试API的cURL命令:

用户注册

curl -X POST http://localhost:8080/api/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "测试用户",
    "email": "test@example.com",
    "password": "password123"
  }'

用户登录

curl -X POST http://localhost:8080/api/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123"
  }'

登录成功后,你将获得一个JWT令牌。在后续请求中使用这个令牌:

创建图书

curl -X POST http://localhost:8080/api/books \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -d '{
    "title": "Go语言编程",
    "author": "许式伟",
    "isbn": "9787111558422",
    "description": "系统介绍Go语言的特性和使用方法",
    "price": 79.00
  }'

获取所有图书

curl -X GET http://localhost:8080/api/books \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

获取特定图书

curl -X GET http://localhost:8080/api/books/1 \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

更新图书

curl -X PUT http://localhost:8080/api/books/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -d '{
    "title": "Go语言编程(第2版)",
    "author": "许式伟",
    "isbn": "9787111558422",
    "description": "全面更新的Go语言编程指南",
    "price": 89.00
  }'

删除图书

curl -X DELETE http://localhost:8080/api/books/1 \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

9. 性能优化与扩展

在实际应用中,我们可能需要考虑以下性能优化和扩展:

9.1 添加缓存层

对于频繁访问但很少更改的数据,可以添加缓存层:

// 示例:使用Redis缓存图书数据
func (r *bookRepository) GetByID(id uint) (*models.Book, error) {
    // 尝试从缓存获取
    cacheKey := fmt.Sprintf("book:%d", id)
    if cached, found := cache.Get(cacheKey); found {
        return cached.(*models.Book), nil
    }
    
    // 从数据库获取
    var book models.Book
    err := r.db.First(&book, id).Error
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, nil
        }
        return nil, err
    }
    
    // 存入缓存
    cache.Set(cacheKey, &book, time.Minute*10)
    
    return &book, nil
}

9.2 分页实现

添加分页支持:

// 图书处理器中添加分页支持
func (h *bookHandler) List(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
    
    books, total, err := h.bookService.ListPaginated(page, pageSize)
    if err != nil {
        c.JSON(http.StatusInternalServerError, api.ErrorResponse(
            "获取图书列表失败",
            "BOOK_LIST_FAILED",
            err.Error(),
        ))
        return
    }
    
    c.JSON(http.StatusOK, api.SuccessResponse(gin.H{
        "books": books,
        "pagination": gin.H{
            "current_page": page,
            "page_size":    pageSize,
            "total_items":  total,
            "total_pages":  (total + pageSize - 1) / pageSize,
        },
    }, "获取图书列表成功"))
}

9.3 搜索功能

添加搜索功能:

// 图书服务中添加搜索方法
func (s *bookService) Search(query string, page, pageSize int) ([]*models.Book, int64, error) {
    var books []*models.Book
    var total int64
    
    db := s.bookRepo.(*mysql.BookRepository).DB()
    
    // 计算总数
    countDB := db.Model(&models.Book{}).Where(
        "title LIKE ? OR author LIKE ? OR description LIKE ?",
        "%"+query+"%", "%"+query+"%", "%"+query+"%",
    )
    if err := countDB.Count(&total).Error; err != nil {
        return nil, 0, err
    }
    
    // 获取分页数据
    err := db.Where(
        "title LIKE ? OR author LIKE ? OR description LIKE ?",
        "%"+query+"%", "%"+query+"%", "%"+query+"%",
    ).Offset((page - 1) * pageSize).Limit(pageSize).Find(&books).Error
    
    return books, total, err
}

9.4 日志记录与监控

添加结构化日志和指标收集:

// 使用zap日志库替代默认日志
func setupLogger() *zap.Logger {
    config := zap.NewProductionConfig()
    config.OutputPaths = []string{"stdout", "logs/app.log"}
    logger, _ := config.Build()
    return logger
}

// 添加Prometheus指标中间件
func MetricsMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        
        c.Next()
        
        duration := time.Since(start).Seconds()
        status := c.Writer.Status()
        
        // 记录请求指标
        httpRequestsTotal.WithLabelValues(path, c.Request.Method, strconv.Itoa(status)).Inc()
        httpRequestDuration.WithLabelValues(path).Observe(duration)
    }
}

总结

在本文中,我们构建了一个完整的RESTful API服务,包括:

  1. 项目结构设计:采用了类似DDD的架构,清晰地分离关注点
  2. RESTful API设计:设计了符合REST原则的API端点和响应格式
  3. 数据模型与存储层:使用GORM实现了数据模型和数据库交互
  4. 服务层实现:实现了业务逻辑,如用户认证、图书管理等
  5. Web API层:使用Gin框架处理HTTP请求和响应
  6. 配置管理:使用Viper管理配置
  7. 性能优化与扩展:讨论了缓存、分页、搜索和监控等扩展点

这个项目展示了如何使用Go语言构建一个可扩展的RESTful API服务,涵盖了从项目结构设计到具体实现的各个方面。你可以将这个项目作为基础,添加更多功能和优化,如文件上传、WebSocket支持、更复杂的权限系统等。

👨‍💻 关于作者与Gopher部落

"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。

🌟 为什么关注我们?

  1. 系统化学习路径:第二阶段15篇文章深入讲解Go核心概念与实践
  2. 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
  3. 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
  4. 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长

📱 关注方式

  1. 微信公众号:搜索 “Gopher部落”“GopherTribe”
  2. CSDN专栏:点击页面右上角"关注"按钮

💡 读者福利

关注公众号回复 “REST” 即可获取:

  • RESTful API设计最佳实践PDF
  • 完整项目源代码
  • Go项目架构设计指南

期待与您在Go语言的学习旅程中共同成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gopher部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值