Golang-16项目实战_从0到1开发RESTful后端
·
Golang项目实战:从0到1开发RESTful后端(分层架构、DDD、单元测试、Docker化)
1. 项目架构设计
1.1 分层架构概述
1.2 项目结构
ecommerce-api/
├── cmd/
│ ├── api/
│ │ └── main.go
│ └── migrate/
│ └── main.go
├── internal/
│ ├── domain/
│ │ ├── entity/
│ │ ├── valueobject/
│ │ ├── repository/
│ │ └── service/
│ ├── application/
│ │ ├── dto/
│ │ ├── usecase/
│ │ └── mapper/
│ ├── infrastructure/
│ │ ├── persistence/
│ │ ├── http/
│ │ ├── config/
│ │ └── logger/
│ └── interface/
│ ├── http/
│ ├── grpc/
│ └── websocket/
├── pkg/
│ ├── errors/
│ ├── validator/
│ ├── jwt/
│ └── response/
├── migrations/
├── scripts/
├── tests/
├── docker/
├── config/
├── Makefile
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
└── README.md
2. 领域驱动设计 (DDD) 实现
2.1 领域模型定义
// internal/domain/entity/user.go
package entity
import (
"time"
"errors"
"regexp"
"github.com/google/uuid"
)
// User 聚合根
type User struct {
id uuid.UUID
username string
email string
password string
profile *UserProfile
status UserStatus
createdAt time.Time
updatedAt time.Time
orders []Order // 关联订单
}
// UserProfile 值对象
type UserProfile struct {
firstName string
lastName string
avatar string
phone string
address *Address
}
// Address 值对象
type Address struct {
street string
city string
state string
country string
postalCode string
}
// UserStatus 枚举
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusInactive UserStatus = "inactive"
UserStatusBanned UserStatus = "banned"
)
// 构造函数
func NewUser(username, email, password string) (*User, error) {
if err := validateUsername(username); err != nil {
return nil, err
}
if err := validateEmail(email); err != nil {
return nil, err
}
if err := validatePassword(password); err != nil {
return nil, err
}
hashedPassword, err := hashPassword(password)
if err != nil {
return nil, err
}
now := time.Now()
return &User{
id: uuid.New(),
username: username,
email: email,
password: hashedPassword,
profile: nil,
status: UserStatusActive,
createdAt: now,
updatedAt: now,
orders: make([]Order, 0),
}, nil
}
// 领域方法
func (u *User) UpdateProfile(profile *UserProfile) error {
if profile == nil {
return errors.New("profile cannot be nil")
}
u.profile = profile
u.updatedAt = time.Now()
return nil
}
func (u *User) ChangeEmail(newEmail string) error {
if err := validateEmail(newEmail); err != nil {
return err
}
if u.email == newEmail {
return errors.New("email is the same")
}
u.email = newEmail
u.updatedAt = time.Now()
return nil
}
func (u *User) Activate() error {
if u.status == UserStatusActive {
return errors.New("user is already active")
}
u.status = UserStatusActive
u.updatedAt = time.Now()
return nil
}
func (u *User) Deactivate() error {
if u.status == UserStatusInactive {
return errors.New("user is already inactive")
}
u.status = UserStatusInactive
u.updatedAt = time.Now()
return nil
}
func (u *User) AddOrder(order Order) {
u.orders = append(u.orders, order)
u.updatedAt = time.Now()
}
// 获取器
func (u *User) ID() uuid.UUID { return u.id }
func (u *User) Username() string { return u.username }
func (u *User) Email() string { return u.email }
func (u *User) Password() string { return u.password }
func (u *User) Profile() *UserProfile { return u.profile }
func (u *User) Status() UserStatus { return u.status }
func (u *User) CreatedAt() time.Time { return u.createdAt }
func (u *User) UpdatedAt() time.Time { return u.updatedAt }
func (u *User) Orders() []Order { return u.orders }
// 验证函数
func validateUsername(username string) error {
if len(username) < 3 || len(username) > 20 {
return errors.New("username must be between 3 and 20 characters")
}
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, username)
if !matched {
return errors.New("username can only contain letters, numbers, and underscores")
}
return nil
}
func validateEmail(email string) error {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(email) {
return errors.New("invalid email format")
}
return nil
}
func validatePassword(password string) error {
if len(password) < 8 {
return errors.New("password must be at least 8 characters long")
}
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasDigit := regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial := regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(password)
if !hasUpper || !hasLower || !hasDigit || !hasSpecial {
return errors.New("password must contain uppercase, lowercase, digit, and special character")
}
return nil
}
func hashPassword(password string) (string, error) {
// 实际实现中会使用bcrypt等库
return password, nil
}
2.2 领域服务
// internal/domain/service/user_service.go
package service
import (
"context"
"errors"
"ecommerce-api/internal/domain/entity"
"ecommerce-api/internal/domain/repository"
"ecommerce-api/internal/domain/valueobject"
)
// UserService 领域服务
type UserService struct {
userRepo repository.UserRepository
eventBus EventBus
}
func NewUserService(userRepo repository.UserRepository, eventBus EventBus) *UserService {
return &UserService{
userRepo: userRepo,
eventBus: eventBus,
}
}
// RegisterUser 用户注册
func (s *UserService) RegisterUser(ctx context.Context, username, email, password string) (*entity.User, error) {
// 检查用户名是否已存在
existingUser, err := s.userRepo.FindByUsername(ctx, username)
if err != nil {
return nil, err
}
if existingUser != nil {
return nil, errors.New("username already exists")
}
// 检查邮箱是否已存在
existingUser, err = s.userRepo.FindByEmail(ctx, email)
if err != nil {
return nil, err
}
if existingUser != nil {
return nil, errors.New("email already exists")
}
// 创建新用户
user, err := entity.NewUser(username, email, password)
if err != nil {
return nil, err
}
// 保存用户
if err := s.userRepo.Save(ctx, user); err != nil {
return nil, err
}
// 发布用户注册事件
event := valueobject.NewUserRegisteredEvent(user.ID(), user.Username(), user.Email())
if err := s.eventBus.Publish(ctx, event); err != nil {
// 记录错误但不影响主流程
log.Printf("Failed to publish user registered event: %v", err)
}
return user, nil
}
// UpdateUserProfile 更新用户资料
func (s *UserService) UpdateUserProfile(ctx context.Context, userID string, profile *entity.UserProfile) error {
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return err
}
if user == nil {
return errors.New("user not found")
}
if err := user.UpdateProfile(profile); err != nil {
return err
}
if err := s.userRepo.Update(ctx, user); err != nil {
return err
}
// 发布用户资料更新事件
event := valueobject.NewUserProfileUpdatedEvent(userID)
if err := s.eventBus.Publish(ctx, event); err != nil {
log.Printf("Failed to publish user profile updated event: %v", err)
}
return nil
}
// ActivateUser 激活用户
func (s *UserService) ActivateUser(ctx context.Context, userID string) error {
user, err := s.userRepo.FindByID(ctx, userID)
if err != nil {
return err
}
if user == nil {
return errors.New("user not found")
}
if err := user.Activate(); err != nil {
return err
}
if err := s.userRepo.Update(ctx, user); err != nil {
return err
}
return nil
}
// GetUserWithOrders 获取用户及其订单
func (s *UserService) GetUserWithOrders(ctx context.Context, userID string) (*entity.User, error) {
user, err := s.userRepo.FindByIDWithOrders(ctx, userID)
if err != nil {
return nil, err
}
if user == nil {
return nil, errors.New("user not found")
}
return user, nil
}
// ValidateCredentials 验证用户凭据
func (s *UserService) ValidateCredentials(ctx context.Context, username, password string) (*entity.User, error) {
user, err := s.userRepo.FindByUsername(ctx, username)
if err != nil {
return nil, err
}
if user == nil {
return nil, errors.New("invalid credentials")
}
// 验证密码
if !verifyPassword(password, user.Password()) {
return nil, errors.New("invalid credentials")
}
return user, nil
}
func verifyPassword(password, hash string) bool {
// 实际实现中会使用bcrypt.CompareHashAndPassword
return password == hash
}
2.3 仓储接口定义
// internal/domain/repository/user_repository.go
package repository
import (
"context"
"github.com/google/uuid"
"ecommerce-api/internal/domain/entity"
)
// UserRepository 用户仓储接口
type UserRepository interface {
// 基本CRUD操作
Save(ctx context.Context, user *entity.User) error
Update(ctx context.Context, user *entity.User) error
Delete(ctx context.Context, userID string) error
FindByID(ctx context.Context, userID string) (*entity.User, error)
// 查询方法
FindByUsername(ctx context.Context, username string) (*entity.User, error)
FindByEmail(ctx context.Context, email string) (*entity.User, error)
FindAll(ctx context.Context, limit, offset int) ([]*entity.User, error)
FindByStatus(ctx context.Context, status entity.UserStatus, limit, offset int) ([]*entity.User, error)
// 复杂查询
FindByIDWithOrders(ctx context.Context, userID string) (*entity.User, error)
FindByCreatedAtRange(ctx context.Context, start, end time.Time) ([]*entity.User, error)
// 统计
Count(ctx context.Context) (int64, error)
CountByStatus(ctx context.Context, status entity.UserStatus) (int64, error)
// 事务支持
WithTx(ctx context.Context, fn func(ctx context.Context) error) error
}
// 查询规格模式
type UserSpecification interface {
IsSatisfiedBy(user *entity.User) bool
ToSQL() string
Parameters() []interface{}
}
// 组合规格
type AndSpecification struct {
specs []UserSpecification
}
func NewAndSpecification(specs ...UserSpecification) *AndSpecification {
return &AndSpecification{specs: specs}
}
func (s *AndSpecification) IsSatisfiedBy(user *entity.User) bool {
for _, spec := range s.specs {
if !spec.IsSatisfiedBy(user) {
return false
}
}
return true
}
func (s *AndSpecification) ToSQL() string {
// 实现SQL生成逻辑
return ""
}
func (s *AndSpecification) Parameters() []interface{} {
// 实现参数提取逻辑
return nil
}
// 具体规格实现
type ActiveUserSpecification struct{}
func (s *ActiveUserSpecification) IsSatisfiedBy(user *entity.User) bool {
return user.Status() == entity.UserStatusActive
}
func (s *ActiveUserSpecification) ToSQL() string {
return "status = ?"
}
func (s *ActiveUserSpecification) Parameters() []interface{} {
return []interface{}{entity.UserStatusActive}
}
type EmailDomainSpecification struct {
domain string
}
func NewEmailDomainSpecification(domain string) *EmailDomainSpecification {
return &EmailDomainSpecification{domain: domain}
}
func (s *EmailDomainSpecification) IsSatisfiedBy(user *entity.User) bool {
// 简化的邮箱域名检查
return len(user.Email()) > len(s.domain) &&
user.Email()[len(user.Email())-len(s.domain):] == s.domain
}
func (s *EmailDomainSpecification) ToSQL() string {
return "email LIKE ?"
}
func (s *EmailDomainSpecification) Parameters() []interface{} {
return []interface{}{"%" + s.domain}
}
3. 应用层实现
3.1 DTO定义
// internal/application/dto/user_dto.go
package dto
import (
"time"
"github.com/google/uuid"
)
// CreateUserDTO 创建用户DTO
type CreateUserDTO struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
FirstName string `json:"first_name" validate:"max=50"`
LastName string `json:"last_name" validate:"max=50"`
Phone string `json:"phone" validate:"omitempty,e164"`
}
// UpdateUserDTO 更新用户DTO
type UpdateUserDTO struct {
UserID string `json:"user_id" validate:"required,uuid"`
Username string `json:"username,omitempty" validate:"omitempty,min=3,max=20"`
Email string `json:"email,omitempty" validate:"omitempty,email"`
FirstName string `json:"first_name,omitempty" validate:"omitempty,max=50"`
LastName string `json:"last_name,omitempty" validate:"omitempty,max=50"`
Phone string `json:"phone,omitempty" validate:"omitempty,e164"`
Avatar string `json:"avatar,omitempty" validate:"omitempty,url"`
}
// UserResponseDTO 用户响应DTO
type UserResponseDTO struct {
UserID string `json:"user_id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"`
Phone string `json:"phone"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ListUsersDTO 用户列表DTO
type ListUsersDTO struct {
Page int `json:"page" validate:"min=1"`
PageSize int `json:"page_size" validate:"min=1,max=100"`
Status string `json:"status,omitempty" validate:"omitempty,oneof=active inactive banned"`
Search string `json:"search,omitempty" validate:"omitempty,max=100"`
}
// ListUsersResponseDTO 用户列表响应DTO
type ListUsersResponseDTO struct {
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Users []UserResponseDTO `json:"users"`
}
// LoginDTO 登录DTO
type LoginDTO struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
// LoginResponseDTO 登录响应DTO
type LoginResponseDTO struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresAt time.Time `json:"expires_at"`
User UserResponseDTO `json:"user"`
}
// ErrorDTO 错误响应DTO
type ErrorDTO struct {
Code string `json:"code"`
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
Path string `json:"path,omitempty"`
Method string `json:"method,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// ValidationErrorDTO 验证错误DTO
type ValidationErrorDTO struct {
Field string `json:"field"`
Message string `json:"message"`
Value interface{} `json:"value,omitempty"`
}
3.2 Use Case实现
// internal/application/usecase/user_usecase.go
package usecase
import (
"context"
"errors"
"github.com/google/uuid"
"ecommerce-api/internal/application/dto"
"ecommerce-api/internal/application/mapper"
"ecommerce-api/internal/domain/entity"
"ecommerce-api/internal/domain/repository"
"ecommerce-api/internal/domain/service"
"ecommerce-api/internal/infrastructure/logger"
)
// UserUseCase 用户用例
type UserUseCase struct {
userService service.UserService
userRepo repository.UserRepository
logger logger.Logger
}
func NewUserUseCase(userService service.UserService, userRepo repository.UserRepository, logger logger.Logger) *UserUseCase {
return &UserUseCase{
userService: userService,
userRepo: userRepo,
logger: logger,
}
}
// CreateUser 创建用户
func (uc *UserUseCase) CreateUser(ctx context.Context, req dto.CreateUserDTO) (*dto.UserResponseDTO, error) {
uc.logger.Info("Creating user", "username", req.Username, "email", req.Email)
// 调用领域服务创建用户
user, err := uc.userService.RegisterUser(ctx, req.Username, req.Email, req.Password)
if err != nil {
uc.logger.Error("Failed to create user", "error", err)
return nil, err
}
// 如果有额外的profile信息,更新用户资料
if req.FirstName != "" || req.LastName != "" || req.Phone != "" {
profile := &entity.UserProfile{
FirstName: req.FirstName,
LastName: req.LastName,
Phone: req.Phone,
}
if err := uc.userService.UpdateUserProfile(ctx, user.ID().String(), profile); err != nil {
uc.logger.Error("Failed to update user profile", "error", err)
// 不返回错误,因为用户已经创建成功
}
}
// 转换为响应DTO
response := mapper.UserToResponseDTO(user)
uc.logger.Info("User created successfully", "user_id", user.ID().String())
return response, nil
}
// GetUser 获取用户信息
func (uc *UserUseCase) GetUser(ctx context.Context, userID string) (*dto.UserResponseDTO, error) {
uc.logger.Info("Getting user", "user_id", userID)
user, err := uc.userRepo.FindByID(ctx, userID)
if err != nil {
uc.logger.Error("Failed to get user", "user_id", userID, "error", err)
return nil, err
}
if user == nil {
uc.logger.Warn("User not found", "user_id", userID)
return nil, errors.New("user not found")
}
response := mapper.UserToResponseDTO(user)
uc.logger.Info("User retrieved successfully", "user_id", userID)
return response, nil
}
// UpdateUser 更新用户信息
func (uc *UserUseCase) UpdateUser(ctx context.Context, req dto.UpdateUserDTO) (*dto.UserResponseDTO, error) {
uc.logger.Info("Updating user", "user_id", req.UserID)
// 检查用户是否存在
existingUser, err := uc.userRepo.FindByID(ctx, req.UserID)
if err != nil {
uc.logger.Error("Failed to find user", "user_id", req.UserID, "error", err)
return nil, err
}
if existingUser == nil {
uc.logger.Warn("User not found for update", "user_id", req.UserID)
return nil, errors.New("user not found")
}
// 如果有需要更新的字段
if req.Username != "" || req.Email != "" {
if req.Username != "" {
existingUser.UpdateUsername(req.Username)
}
if req.Email != "" {
if err := existingUser.ChangeEmail(req.Email); err != nil {
return nil, err
}
}
if err := uc.userRepo.Update(ctx, existingUser); err != nil {
uc.logger.Error("Failed to update user", "user_id", req.UserID, "error", err)
return nil, err
}
}
// 如果有profile信息需要更新
if req.FirstName != "" || req.LastName != "" || req.Phone != "" || req.Avatar != "" {
profile := &entity.UserProfile{
FirstName: req.FirstName,
LastName: req.LastName,
Phone: req.Phone,
Avatar: req.Avatar,
}
if err := uc.userService.UpdateUserProfile(ctx, req.UserID, profile); err != nil {
uc.logger.Error("Failed to update user profile", "user_id", req.UserID, "error", err)
return nil, err
}
}
// 重新获取更新后的用户
updatedUser, err := uc.userRepo.FindByID(ctx, req.UserID)
if err != nil {
uc.logger.Error("Failed to get updated user", "user_id", req.UserID, "error", err)
return nil, err
}
response := mapper.UserToResponseDTO(updatedUser)
uc.logger.Info("User updated successfully", "user_id", req.UserID)
return response, nil
}
// ListUsers 获取用户列表
func (uc *UserUseCase) ListUsers(ctx context.Context, req dto.ListUsersDTO) (*dto.ListUsersResponseDTO, error) {
uc.logger.Info("Listing users", "page", req.Page, "page_size", req.PageSize, "status", req.Status)
limit := req.PageSize
offset := (req.Page - 1) * req.PageSize
var users []*entity.User
var total int64
var err error
// 根据状态筛选
if req.Status != "" {
status := entity.UserStatus(req.Status)
users, err = uc.userRepo.FindByStatus(ctx, status, limit, offset)
if err != nil {
uc.logger.Error("Failed to find users by status", "status", req.Status, "error", err)
return nil, err
}
total, err = uc.userRepo.CountByStatus(ctx, status)
} else {
users, err = uc.userRepo.FindAll(ctx, limit, offset)
if err != nil {
uc.logger.Error("Failed to find all users", "error", err)
return nil, err
}
total, err = uc.userRepo.Count(ctx)
}
if err != nil {
uc.logger.Error("Failed to count users", "error", err)
return nil, err
}
// 转换为响应DTO
userDTOs := make([]dto.UserResponseDTO, len(users))
for i, user := range users {
userDTOs[i] = *mapper.UserToResponseDTO(user)
}
response := &dto.ListUsersResponseDTO{
Total: total,
Page: req.Page,
PageSize: req.PageSize,
Users: userDTOs,
}
uc.logger.Info("Users listed successfully", "count", len(users), "total", total)
return response, nil
}
// DeleteUser 删除用户
func (uc *UserUseCase) DeleteUser(ctx context.Context, userID string) error {
uc.logger.Info("Deleting user", "user_id", userID)
user, err := uc.userRepo.FindByID(ctx, userID)
if err != nil {
uc.logger.Error("Failed to find user", "user_id", userID, "error", err)
return err
}
if user == nil {
uc.logger.Warn("User not found for deletion", "user_id", userID)
return errors.New("user not found")
}
// 检查是否有未完成的订单
if len(user.Orders()) > 0 {
uc.logger.Warn("Cannot delete user with orders", "user_id", userID, "order_count", len(user.Orders()))
return errors.New("cannot delete user with existing orders")
}
if err := uc.userRepo.Delete(ctx, userID); err != nil {
uc.logger.Error("Failed to delete user", "user_id", userID, "error", err)
return err
}
uc.logger.Info("User deleted successfully", "user_id", userID)
return nil
}
// Login 用户登录
func (uc *UserUseCase) Login(ctx context.Context, req dto.LoginDTO) (*dto.LoginResponseDTO, error) {
uc.logger.Info("User login attempt", "username", req.Username)
// 验证用户凭据
user, err := uc.userService.ValidateCredentials(ctx, req.Username, req.Password)
if err != nil {
uc.logger.Warn("Invalid login credentials", "username", req.Username, "error", err)
return nil, errors.New("invalid credentials")
}
// 检查用户状态
if user.Status() != entity.UserStatusActive {
uc.logger.Warn("Login attempt with inactive user", "username", req.Username, "status", user.Status())
return nil, errors.New("user account is not active")
}
// 生成token
accessToken, refreshToken, expiresAt, err := uc.generateTokens(user.ID().String(), user.Username())
if err != nil {
uc.logger.Error("Failed to generate tokens", "username", req.Username, "error", err)
return nil, err
}
response := &dto.LoginResponseDTO{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresAt: expiresAt,
User: *mapper.UserToResponseDTO(user),
}
uc.logger.Info("User logged in successfully", "username", req.Username, "user_id", user.ID().String())
return response, nil
}
func (uc *UserUseCase) generateTokens(userID, username string) (accessToken, refreshToken string, expiresAt time.Time, err error) {
// 实际实现中会使用JWT库生成token
accessToken = "access-token-" + userID
refreshToken = "refresh-token-" + userID
expiresAt = time.Now().Add(24 * time.Hour)
return accessToken, refreshToken, expiresAt, nil
}
4. 基础设施层实现
4.1 数据库模型和映射
// internal/infrastructure/persistence/model/user_model.go
package model
import (
"time"
"gorm.io/gorm"
"github.com/google/uuid"
)
// UserModel GORM模型
type UserModel struct {
ID uuid.UUID `gorm:"type:uuid;primary_key"`
Username string `gorm:"type:varchar(20);uniqueIndex;not null"`
Email string `gorm:"type:varchar(100);uniqueIndex;not null"`
PasswordHash string `gorm:"type:varchar(255);not null"`
FirstName string `gorm:"type:varchar(50)"`
LastName string `gorm:"type:varchar(50)"`
Avatar string `gorm:"type:varchar(255)"`
Phone string `gorm:"type:varchar(20)"`
Status string `gorm:"type:varchar(20);not null;default:active"`
CreatedAt time.Time `gorm:"not null"`
UpdatedAt time.Time `gorm:"not null"`
DeletedAt gorm.DeletedAt `gorm:"index"`
// 关联
Orders []OrderModel `gorm:"foreignKey:UserID"`
}
// TableName 表名
func (UserModel) TableName() string {
return "users"
}
// BeforeCreate 创建前钩子
func (u *UserModel) BeforeCreate(tx *gorm.DB) error {
if u.ID == uuid.Nil {
u.ID = uuid.New()
}
return nil
}
// OrderModel 订单模型
type OrderModel struct {
ID uuid.UUID `gorm:"type:uuid;primary_key"`
UserID uuid.UUID `gorm:"type:uuid;index;not null"`
OrderNumber string `gorm:"type:varchar(50);uniqueIndex;not null"`
TotalAmount float64 `gorm:"type:decimal(10,2);not null"`
Status string `gorm:"type:varchar(20);not null"`
CreatedAt time.Time `gorm:"not null"`
UpdatedAt time.Time `gorm:"not null"`
// 关联
User UserModel `gorm:"foreignKey:UserID"`
}
func (OrderModel) TableName() string {
return "orders"
}
4.2 仓储实现
// internal/infrastructure/persistence/user_repository.go
package persistence
import (
"context"
"fmt"
"gorm.io/gorm"
"github.com/google/uuid"
"ecommerce-api/internal/domain/entity"
"ecommerce-api/internal/domain/repository"
"ecommerce-api/internal/infrastructure/persistence/model"
"ecommerce-api/internal/infrastructure/persistence/mapper"
)
// UserRepositoryImpl 用户仓储实现
type UserRepositoryImpl struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) repository.UserRepository {
return &UserRepositoryImpl{db: db}
}
func (r *UserRepositoryImpl) Save(ctx context.Context, user *entity.User) error {
userModel := mapper.UserToModel(user)
if err := r.db.WithContext(ctx).Create(userModel).Error; err != nil {
return fmt.Errorf("failed to save user: %w", err)
}
return nil
}
func (r *UserRepositoryImpl) Update(ctx context.Context, user *entity.User) error {
userModel := mapper.UserToModel(user)
if err := r.db.WithContext(ctx).Save(userModel).Error; err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
return nil
}
func (r *UserRepositoryImpl) Delete(ctx context.Context, userID string) error {
id, err := uuid.Parse(userID)
if err != nil {
return fmt.Errorf("invalid user ID: %w", err)
}
if err := r.db.WithContext(ctx).Delete(&model.UserModel{}, "id = ?", id).Error; err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
return nil
}
func (r *UserRepositoryImpl) FindByID(ctx context.Context, userID string) (*entity.User, error) {
id, err := uuid.Parse(userID)
if err != nil {
return nil, fmt.Errorf("invalid user ID: %w", err)
}
var userModel model.UserModel
if err := r.db.WithContext(ctx).First(&userModel, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, fmt.Errorf("failed to find user by ID: %w", err)
}
return mapper.ModelToUser(&userModel), nil
}
func (r *UserRepositoryImpl) FindByUsername(ctx context.Context, username string) (*entity.User, error) {
var userModel model.UserModel
if err := r.db.WithContext(ctx).First(&userModel, "username = ?", username).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, fmt.Errorf("failed to find user by username: %w", err)
}
return mapper.ModelToUser(&userModel), nil
}
func (r *UserRepositoryImpl) FindByEmail(ctx context.Context, email string) (*entity.User, error) {
var userModel model.UserModel
if err := r.db.WithContext(ctx).First(&userModel, "email = ?", email).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, fmt.Errorf("failed to find user by email: %w", err)
}
return mapper.ModelToUser(&userModel), nil
}
func (r *UserRepositoryImpl) FindAll(ctx context.Context, limit, offset int) ([]*entity.User, error) {
var userModels []model.UserModel
if err := r.db.WithContext(ctx).Limit(limit).Offset(offset).Find(&userModels).Error; err != nil {
return nil, fmt.Errorf("failed to find all users: %w", err)
}
users := make([]*entity.User, len(userModels))
for i, userModel := range userModels {
users[i] = mapper.ModelToUser(&userModel)
}
return users, nil
}
func (r *UserRepositoryImpl) FindByStatus(ctx context.Context, status entity.UserStatus, limit, offset int) ([]*entity.User, error) {
var userModels []model.UserModel
if err := r.db.WithContext(ctx).
Where("status = ?", string(status)).
Limit(limit).
Offset(offset).
Find(&userModels).Error; err != nil {
return nil, fmt.Errorf("failed to find users by status: %w", err)
}
users := make([]*entity.User, len(userModels))
for i, userModel := range userModels {
users[i] = mapper.ModelToUser(&userModel)
}
return users, nil
}
func (r *UserRepositoryImpl) FindByIDWithOrders(ctx context.Context, userID string) (*entity.User, error) {
id, err := uuid.Parse(userID)
if err != nil {
return nil, fmt.Errorf("invalid user ID: %w", err)
}
var userModel model.UserModel
if err := r.db.WithContext(ctx).
Preload("Orders").
First(&userModel, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, fmt.Errorf("failed to find user with orders: %w", err)
}
return mapper.ModelToUserWithOrders(&userModel), nil
}
func (r *UserRepositoryImpl) Count(ctx context.Context) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.UserModel{}).Count(&count).Error; err != nil {
return 0, fmt.Errorf("failed to count users: %w", err)
}
return count, nil
}
func (r *UserRepositoryImpl) CountByStatus(ctx context.Context, status entity.UserStatus) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).
Model(&model.UserModel{}).
Where("status = ?", string(status)).
Count(&count).Error; err != nil {
return 0, fmt.Errorf("failed to count users by status: %w", err)
}
return count, nil
}
func (r *UserRepositoryImpl) WithTx(ctx context.Context, fn func(ctx context.Context) error) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txRepo := &UserRepositoryImpl{db: tx}
ctx = context.WithValue(ctx, "tx_repo", txRepo)
return fn(ctx)
})
}
4.3 数据库配置
// internal/infrastructure/config/database.go
package config
import (
"fmt"
"time"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"ecommerce-api/internal/infrastructure/logger"
)
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Driver string `yaml:"driver" validate:"required,oneof=mysql postgres sqlite"`
Host string `yaml:"host" validate:"required"`
Port int `yaml:"port" validate:"required"`
Username string `yaml:"username" validate:"required"`
Password string `yaml:"password" validate:"required"`
Database string `yaml:"database" validate:"required"`
Charset string `yaml:"charset" default:"utf8mb4"`
// 连接池配置
MaxIdleConns int `yaml:"max_idle_conns" default:"10"`
MaxOpenConns int `yaml:"max_open_conns" default:"100"`
ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime" default:"1h"`
ConnMaxIdleTime time.Duration `yaml:"conn_max_idle_time" default:"10m"`
// 日志配置
LogLevel string `yaml:"log_level" default:"info"`
}
// NewDatabaseConnection 创建数据库连接
func NewDatabaseConnection(cfg DatabaseConfig, appLogger logger.Logger) (*gorm.DB, error) {
var dialector gorm.Dialector
switch cfg.Driver {
case "mysql":
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database, cfg.Charset)
dialector = mysql.Open(dsn)
case "postgres":
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Database)
dialector = postgres.Open(dsn)
case "sqlite":
dialector = sqlite.Open(cfg.Database)
default:
return nil, fmt.Errorf("unsupported database driver: %s", cfg.Driver)
}
// 配置GORM日志
logLevel := logger.Silent
switch cfg.LogLevel {
case "error":
logLevel = logger.Error
case "warn":
logLevel = logger.Warn
case "info":
logLevel = logger.Info
}
gormLogger := logger.New(
appLogger,
logger.Config{
SlowThreshold: time.Second,
LogLevel: logLevel,
IgnoreRecordNotFoundError: true,
Colorful: true,
},
)
// GORM配置
config := &gorm.Config{
Logger: gormLogger,
NowFunc: func() time.Time {
return time.Now().UTC()
},
PrepareStmt: true,
}
// 创建连接
db, err := gorm.Open(dialector, config)
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// 获取底层SQL数据库连接
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get database instance: %w", err)
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
sqlDB.SetConnMaxIdleTime(cfg.ConnMaxIdleTime)
// 测试连接
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
appLogger.Info("Database connection established", "driver", cfg.Driver, "host", cfg.Host)
return db, nil
}
// AutoMigrate 自动迁移数据库
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(
&model.UserModel{},
&model.OrderModel{},
// 其他模型...
)
}
5. 接口层实现
5.1 HTTP处理器
// internal/interface/http/handler/user_handler.go
package httphandler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"ecommerce-api/internal/application/dto"
"ecommerce-api/internal/application/usecase"
"ecommerce-api/internal/infrastructure/response"
"ecommerce-api/internal/infrastructure/validator"
)
// UserHandler 用户HTTP处理器
type UserHandler struct {
userUseCase *usecase.UserUseCase
validator *validator.Validator
}
func NewUserHandler(userUseCase *usecase.UserUseCase, validator *validator.Validator) *UserHandler {
return &UserHandler{
userUseCase: userUseCase,
validator: validator,
}
}
// CreateUser 创建用户
// @Summary Create a new user
// @Description Create a new user with the provided information
// @Tags users
// @Accept json
// @Produce json
// @Param user body dto.CreateUserDTO true "User information"
// @Success 201 {object} response.Response{data=dto.UserResponseDTO}
// @Failure 400 {object} response.Response
// @Failure 409 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /users [post]
func (h *UserHandler) CreateUser(c *gin.Context) {
var req dto.CreateUserDTO
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "invalid_request", "Invalid request body", err.Error())
return
}
// 验证请求
if err := h.validator.Validate(req); err != nil {
response.ValidationError(c, err)
return
}
// 调用用例
user, err := h.userUseCase.CreateUser(c.Request.Context(), req)
if err != nil {
switch err.Error() {
case "username already exists":
response.Error(c, http.StatusConflict, "username_exists", "Username already exists", nil)
case "email already exists":
response.Error(c, http.StatusConflict, "email_exists", "Email already exists", nil)
default:
response.Error(c, http.StatusInternalServerError, "internal_error", "Failed to create user", err.Error())
}
return
}
response.Success(c, http.StatusCreated, "User created successfully", user)
}
// GetUser 获取用户信息
// @Summary Get user by ID
// @Description Get user information by user ID
// @Tags users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} response.Response{data=dto.UserResponseDTO}
// @Failure 400 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /users/{id} [get]
func (h *UserHandler) GetUser(c *gin.Context) {
userID := c.Param("id")
// 验证UUID格式
if _, err := uuid.Parse(userID); err != nil {
response.Error(c, http.StatusBadRequest, "invalid_user_id", "Invalid user ID format", nil)
return
}
user, err := h.userUseCase.GetUser(c.Request.Context(), userID)
if err != nil {
if err.Error() == "user not found" {
response.Error(c, http.StatusNotFound, "user_not_found", "User not found", nil)
} else {
response.Error(c, http.StatusInternalServerError, "internal_error", "Failed to get user", err.Error())
}
return
}
response.Success(c, http.StatusOK, "User retrieved successfully", user)
}
// UpdateUser 更新用户信息
// @Summary Update user
// @Description Update user information
// @Tags users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Param user body dto.UpdateUserDTO true "User information"
// @Success 200 {object} response.Response{data=dto.UserResponseDTO}
// @Failure 400 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /users/{id} [put]
func (h *UserHandler) UpdateUser(c *gin.Context) {
userID := c.Param("id")
// 验证UUID格式
if _, err := uuid.Parse(userID); err != nil {
response.Error(c, http.StatusBadRequest, "invalid_user_id", "Invalid user ID format", nil)
return
}
var req dto.UpdateUserDTO
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "invalid_request", "Invalid request body", err.Error())
return
}
// 设置用户ID
req.UserID = userID
// 验证请求
if err := h.validator.Validate(req); err != nil {
response.ValidationError(c, err)
return
}
user, err := h.userUseCase.UpdateUser(c.Request.Context(), req)
if err != nil {
switch err.Error() {
case "user not found":
response.Error(c, http.StatusNotFound, "user_not_found", "User not found", nil)
case "username already exists":
response.Error(c, http.StatusConflict, "username_exists", "Username already exists", nil)
case "email already exists":
response.Error(c, http.StatusConflict, "email_exists", "Email already exists", nil)
default:
response.Error(c, http.StatusInternalServerError, "internal_error", "Failed to update user", err.Error())
}
return
}
response.Success(c, http.StatusOK, "User updated successfully", user)
}
// DeleteUser 删除用户
// @Summary Delete user
// @Description Delete a user by ID
// @Tags users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /users/{id} [delete]
func (h *UserHandler) DeleteUser(c *gin.Context) {
userID := c.Param("id")
// 验证UUID格式
if _, err := uuid.Parse(userID); err != nil {
response.Error(c, http.StatusBadRequest, "invalid_user_id", "Invalid user ID format", nil)
return
}
err := h.userUseCase.DeleteUser(c.Request.Context(), userID)
if err != nil {
switch err.Error() {
case "user not found":
response.Error(c, http.StatusNotFound, "user_not_found", "User not found", nil)
case "cannot delete user with existing orders":
response.Error(c, http.StatusConflict, "user_has_orders", "Cannot delete user with existing orders", nil)
default:
response.Error(c, http.StatusInternalServerError, "internal_error", "Failed to delete user", err.Error())
}
return
}
response.Success(c, http.StatusOK, "User deleted successfully", nil)
}
// ListUsers 获取用户列表
// @Summary List users
// @Description Get a list of users with pagination
// @Tags users
// @Accept json
// @Produce json
// @Param page query int false "Page number" default(1)
// @Param page_size query int false "Page size" default(10)
// @Param status query string false "User status" Enums(active,inactive,banned)
// @Param search query string false "Search keyword"
// @Success 200 {object} response.Response{data=dto.ListUsersResponseDTO}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /users [get]
func (h *UserHandler) ListUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
status := c.Query("status")
search := c.Query("search")
req := dto.ListUsersDTO{
Page: page,
PageSize: pageSize,
Status: status,
Search: search,
}
// 验证请求
if err := h.validator.Validate(req); err != nil {
response.ValidationError(c, err)
return
}
result, err := h.userUseCase.ListUsers(c.Request.Context(), req)
if err != nil {
response.Error(c, http.StatusInternalServerError, "internal_error", "Failed to list users", err.Error())
return
}
response.Success(c, http.StatusOK, "Users retrieved successfully", result)
}
5.2 响应封装
// internal/infrastructure/response/response.go
package response
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// Response 统一响应格式
type Response struct {
Success bool `json:"success"`
Code string `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Timestamp time.Time `json:"timestamp"`
Path string `json:"path,omitempty"`
Method string `json:"method,omitempty"`
}
// Success 成功响应
func Success(c *gin.Context, statusCode int, message string, data interface{}) {
c.JSON(statusCode, Response{
Success: true,
Code: "success",
Message: message,
Data: data,
Timestamp: time.Now(),
Path: c.Request.URL.Path,
Method: c.Request.Method,
})
}
// Error 错误响应
func Error(c *gin.Context, statusCode int, code, message string, details interface{}) {
c.JSON(statusCode, Response{
Success: false,
Code: code,
Message: message,
Data: details,
Timestamp: time.Now(),
Path: c.Request.URL.Path,
Method: c.Request.Method,
})
}
// ValidationError 验证错误响应
func ValidationError(c *gin.Context, errors interface{}) {
Error(c, http.StatusBadRequest, "validation_error", "Validation failed", errors)
}
// NotFound 404响应
func NotFound(c *gin.Context, message string) {
Error(c, http.StatusNotFound, "not_found", message, nil)
}
// Unauthorized 401响应
func Unauthorized(c *gin.Context, message string) {
Error(c, http.StatusUnauthorized, "unauthorized", message, nil)
}
// Forbidden 403响应
func Forbidden(c *gin.Context, message string) {
Error(c, http.StatusForbidden, "forbidden", message, nil)
}
// InternalError 500响应
func InternalError(c *gin.Context, message string, details interface{}) {
Error(c, http.StatusInternalServerError, "internal_error", message, details)
}
// BadRequest 400响应
func BadRequest(c *gin.Context, message string, details interface{}) {
Error(c, http.StatusBadRequest, "bad_request", message, details)
}
6. 测试实现
6.1 单元测试
// internal/application/usecase/user_usecase_test.go
package usecase_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/google/uuid"
"ecommerce-api/internal/application/dto"
"ecommerce-api/internal/application/usecase"
"ecommerce-api/internal/domain/entity"
"ecommerce-api/internal/domain/service"
"ecommerce-api/mocks"
)
// MockUserService 模拟用户服务
type MockUserService struct {
mock.Mock
}
func (m *MockUserService) RegisterUser(ctx context.Context, username, email, password string) (*entity.User, error) {
args := m.Called(ctx, username, email, password)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.User), args.Error(1)
}
func (m *MockUserService) UpdateUserProfile(ctx context.Context, userID string, profile *entity.UserProfile) error {
args := m.Called(ctx, userID, profile)
return args.Error(0)
}
func (m *MockUserService) ValidateCredentials(ctx context.Context, username, password string) (*entity.User, error) {
args := m.Called(ctx, username, password)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.User), args.Error(1)
}
// MockUserRepository 模拟用户仓储
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) Save(ctx context.Context, user *entity.User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func (m *MockUserRepository) Update(ctx context.Context, user *entity.User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func (m *MockUserRepository) FindByID(ctx context.Context, userID string) (*entity.User, error) {
args := m.Called(ctx, userID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entity.User), args.Error(1)
}
func TestUserUseCase_CreateUser(t *testing.T) {
tests := []struct {
name string
input dto.CreateUserDTO
mockSetup func(*MockUserService, *MockUserRepository)
expectedError bool
expectedUser *dto.UserResponseDTO
}{
{
name: "successful user creation",
input: dto.CreateUserDTO{
Username: "testuser",
Email: "test@example.com",
Password: "Test123!@#",
FirstName: "Test",
LastName: "User",
},
mockSetup: func(userService *MockUserService, userRepo *MockUserRepository) {
user := &entity.User{}
userService.On("RegisterUser", mock.Anything, "testuser", "test@example.com", "Test123!@#").
Return(user, nil)
userService.On("UpdateUserProfile", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
},
expectedError: false,
},
{
name: "username already exists",
input: dto.CreateUserDTO{
Username: "existinguser",
Email: "test@example.com",
Password: "Test123!@#",
},
mockSetup: func(userService *MockUserService, userRepo *MockUserRepository) {
userService.On("RegisterUser", mock.Anything, "existinguser", "test@example.com", "Test123!@#").
Return(nil, errors.New("username already exists"))
},
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 设置模拟对象
userService := new(MockUserService)
userRepo := new(MockUserRepository)
logger := mocks.NewMockLogger()
tt.mockSetup(userService, userRepo)
// 创建用例
useCase := usecase.NewUserUseCase(userService, userRepo, logger)
// 执行测试
result, err := useCase.CreateUser(context.Background(), tt.input)
// 验证结果
if tt.expectedError {
assert.Error(t, err)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
// 验证模拟调用
userService.AssertExpectations(t)
userRepo.AssertExpectations(t)
})
}
}
func TestUserUseCase_GetUser(t *testing.T) {
tests := []struct {
name string
userID string
mockSetup func(*MockUserRepository)
expectedError bool
}{
{
name: "user found",
userID: uuid.New().String(),
mockSetup: func(userRepo *MockUserRepository) {
user := &entity.User{}
userRepo.On("FindByID", mock.Anything, mock.Anything).Return(user, nil)
},
expectedError: false,
},
{
name: "user not found",
userID: uuid.New().String(),
mockSetup: func(userRepo *MockUserRepository) {
userRepo.On("FindByID", mock.Anything, mock.Anything).Return(nil, nil)
},
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
userService := new(MockUserService)
userRepo := new(MockUserRepository)
logger := mocks.NewMockLogger()
tt.mockSetup(userRepo)
useCase := usecase.NewUserUseCase(userService, userRepo, logger)
result, err := useCase.GetUser(context.Background(), tt.userID)
if tt.expectedError {
assert.Error(t, err)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
userRepo.AssertExpectations(t)
})
}
}
func TestUserUseCase_ListUsers(t *testing.T) {
// Table-driven test for ListUsers
tests := []struct {
name string
input dto.ListUsersDTO
mockSetup func(*MockUserRepository)
expectedCount int
expectedError bool
}{
{
name: "list active users",
input: dto.ListUsersDTO{
Page: 1,
PageSize: 10,
Status: "active",
},
mockSetup: func(userRepo *MockUserRepository) {
users := []*entity.User{
&entity.User{},
&entity.User{},
}
userRepo.On("FindByStatus", mock.Anything, entity.UserStatusActive, 10, 0).
Return(users, nil)
userRepo.On("CountByStatus", mock.Anything, entity.UserStatusActive).
Return(int64(2), nil)
},
expectedCount: 2,
expectedError: false,
},
{
name: "list all users",
input: dto.ListUsersDTO{
Page: 1,
PageSize: 10,
},
mockSetup: func(userRepo *MockUserRepository) {
users := []*entity.User{
&entity.User{},
&entity.User{},
&entity.User{},
}
userRepo.On("FindAll", mock.Anything, 10, 0).
Return(users, nil)
userRepo.On("Count", mock.Anything).
Return(int64(3), nil)
},
expectedCount: 3,
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
userService := new(MockUserService)
userRepo := new(MockUserRepository)
logger := mocks.NewMockLogger()
tt.mockSetup(userRepo)
useCase := usecase.NewUserUseCase(userService, userRepo, logger)
result, err := useCase.ListUsers(context.Background(), tt.input)
if tt.expectedError {
assert.Error(t, err)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.expectedCount, len(result.Users))
assert.Equal(t, int64(tt.expectedCount), result.Total)
}
userRepo.AssertExpectations(t)
})
}
}
6.2 集成测试
// tests/integration/user_integration_test.go
package integration
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"ecommerce-api/internal/application/dto"
"ecommerce-api/internal/infrastructure/config"
"ecommerce-api/internal/infrastructure/persistence"
"ecommerce-api/internal/interface/http"
)
// UserIntegrationTestSuite 用户集成测试套件
type UserIntegrationTestSuite struct {
suite.Suite
db *gorm.DB
router *http.ServeMux
server *httptest.Server
}
func (suite *UserIntegrationTestSuite) SetupSuite() {
// 创建测试数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
suite.Require().NoError(err)
// 运行迁移
err = db.AutoMigrate(&persistence.UserModel{})
suite.Require().NoError(err)
suite.db = db
// 创建HTTP服务器
cfg := &config.Config{
Server: config.ServerConfig{
Host: "localhost",
Port: "8080",
},
Database: config.DatabaseConfig{
Driver: "sqlite",
},
}
// 初始化依赖
userRepo := persistence.NewUserRepository(db)
// ... 初始化其他依赖
// 创建路由
router := http.NewServeMux()
httphandler.RegisterRoutes(router, cfg, userRepo /* 其他依赖 */)
suite.router = router
suite.server = httptest.NewServer(router)
}
func (suite *UserIntegrationTestSuite) TearDownSuite() {
suite.server.Close()
sqlDB, err := suite.db.DB()
suite.Require().NoError(err)
sqlDB.Close()
}
func (suite *UserIntegrationTestSuite) TestCreateUser() {
// 测试创建用户
createUserReq := dto.CreateUserDTO{
Username: "integration_test_user",
Email: "integration@test.com",
Password: "Test123!@#",
FirstName: "Integration",
LastName: "Test",
}
body, err := json.Marshal(createUserReq)
suite.Require().NoError(err)
resp, err := http.Post(suite.server.URL+"/api/v1/users", "application/json", bytes.NewBuffer(body))
suite.Require().NoError(err)
defer resp.Body.Close()
assert.Equal(suite.T(), http.StatusCreated, resp.StatusCode)
var result map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&result)
suite.Require().NoError(err)
assert.True(suite.T(), result["success"].(bool))
assert.NotNil(suite.T(), result["data"])
}
func (suite *UserIntegrationTestSuite) TestGetUser() {
// 先创建用户
createUserReq := dto.CreateUserDTO{
Username: "get_test_user",
Email: "get@test.com",
Password: "Test123!@#",
}
body, _ := json.Marshal(createUserReq)
createResp, _ := http.Post(suite.server.URL+"/api/v1/users", "application/json", bytes.NewBuffer(body))
createResp.Body.Close()
// 解析创建响应获取用户ID
var createResult map[string]interface{}
json.NewDecoder(createResp.Body).Decode(&createResult)
userData := createResult["data"].(map[string]interface{})
userID := userData["user_id"].(string)
// 测试获取用户
resp, err := http.Get(suite.server.URL + "/api/v1/users/" + userID)
suite.Require().NoError(err)
defer resp.Body.Close()
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var result map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&result)
suite.Require().NoError(err)
assert.True(suite.T(), result["success"].(bool))
assert.NotNil(suite.T(), result["data"])
}
func (suite *UserIntegrationTestSuite) TestListUsers() {
// 创建多个用户
for i := 0; i < 5; i++ {
createUserReq := dto.CreateUserDTO{
Username: fmt.Sprintf("list_test_user_%d", i),
Email: fmt.Sprintf("list%d@test.com", i),
Password: "Test123!@#",
}
body, _ := json.Marshal(createUserReq)
resp, _ := http.Post(suite.server.URL+"/api/v1/users", "application/json", bytes.NewBuffer(body))
resp.Body.Close()
}
// 测试获取用户列表
resp, err := http.Get(suite.server.URL + "/api/v1/users?page=1&page_size=10")
suite.Require().NoError(err)
defer resp.Body.Close()
assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
var result map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&result)
suite.Require().NoError(err)
assert.True(suite.T(), result["success"].(bool))
data := result["data"].(map[string]interface{})
assert.GreaterOrEqual(suite.T(), data["total"], float64(5))
assert.NotNil(suite.T(), data["users"])
}
func TestUserIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(UserIntegrationTestSuite))
}
7. Docker化部署
7.1 Dockerfile
# Dockerfile
# 构建阶段
FROM golang:1.21-alpine AS builder
# 安装构建依赖
RUN apk add --no-cache git ca-certificates tzdata
# 设置工作目录
WORKDIR /app
# 复制go mod文件
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码
COPY . .
# 构建应用
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main cmd/api/main.go
# 运行阶段
FROM alpine:latest
# 安装运行时依赖
RUN apk --no-cache add ca-certificates
# 创建非root用户
RUN addgroup -g 1000 -S appgroup && \
adduser -u 1000 -S appuser -G appgroup
# 设置工作目录
WORKDIR /root/
# 从构建阶段复制二进制文件
COPY --from=builder /app/main .
# 复制配置文件
COPY --from=builder /app/config ./config
# 复制迁移文件
COPY --from=builder /app/migrations ./migrations
# 设置时区
ENV TZ=Asia/Shanghai
# 切换到非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
# 启动命令
CMD ["./main"]
7.2 Docker Compose配置
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: ecommerce-api
ports:
- "8080:8080"
environment:
- APP_ENV=production
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=ecommerce
- DB_PASSWORD=ecommerce123
- DB_NAME=ecommerce_db
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- postgres
- redis
volumes:
- ./config:/root/config
- ./logs:/root/logs
networks:
- ecommerce-network
restart: unless-stopped
postgres:
image: postgres:15-alpine
container_name: ecommerce-postgres
environment:
- POSTGRES_DB=ecommerce_db
- POSTGRES_USER=ecommerce
- POSTGRES_PASSWORD=ecommerce123
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d
networks:
- ecommerce-network
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: ecommerce-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- ecommerce-network
restart: unless-stopped
nginx:
image: nginx:alpine
container_name: ecommerce-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl:/etc/nginx/ssl
- ./logs/nginx:/var/log/nginx
depends_on:
- app
networks:
- ecommerce-network
restart: unless-stopped
volumes:
postgres_data:
redis_data:
networks:
ecommerce-network:
driver: bridge
7.3 生产环境配置
# docker-compose.prod.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: production
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
environment:
- APP_ENV=production
- LOG_LEVEL=info
- DB_MAX_OPEN_CONNS=25
- DB_MAX_IDLE_CONNS=5
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=ecommerce_db
- POSTGRES_USER=ecommerce
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
volumes:
- postgres_data:/var/lib/postgresql/data
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
secrets:
db_password:
external: true
configs:
nginx_config:
external: true
8. Makefile构建脚本
# Makefile
.PHONY: help build run test clean docker-build docker-run migrate lint
# 变量定义
APP_NAME=ecommerce-api
DOCKER_IMAGE=$(APP_NAME):latest
GO_VERSION=1.21
# 帮助信息
help:
@echo "Available targets:"
@echo " help - Show this help message"
@echo " build - Build the application"
@echo " run - Run the application"
@echo " test - Run tests"
@echo " test-coverage - Run tests with coverage"
@echo " lint - Run linter"
@echo " migrate - Run database migrations"
@echo " docker-build - Build Docker image"
@echo " docker-run - Run Docker container"
@echo " clean - Clean build artifacts"
# 构建应用
build:
@echo "Building $(APP_NAME)..."
@go build -o bin/$(APP_NAME) cmd/api/main.go
@echo "Build complete: bin/$(APP_NAME)"
# 运行应用
run:
@echo "Running $(APP_NAME)..."
@go run cmd/api/main.go
# 运行测试
test:
@echo "Running tests..."
@go test -v ./...
# 运行测试并生成覆盖率报告
test-coverage:
@echo "Running tests with coverage..."
@go test -v -coverprofile=coverage.out ./...
@go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
# 运行基准测试
benchmark:
@echo "Running benchmarks..."
@go test -bench=. -benchmem ./...
# 代码格式化
fmt:
@echo "Formatting code..."
@go fmt ./...
# 运行静态代码分析
lint:
@echo "Running linter..."
@golangci-lint run
# 下载依赖
deps:
@echo "Downloading dependencies..."
@go mod download
@go mod tidy
# 运行数据库迁移
migrate:
@echo "Running database migrations..."
@go run cmd/migrate/main.go
# 创建新的迁移文件
migrate-create:
@echo "Creating new migration..."
@read -p "Enter migration name: " name; \
go run cmd/migrate/main.go create $$name
# 构建Docker镜像
docker-build:
@echo "Building Docker image..."
@docker build -t $(DOCKER_IMAGE) .
@echo "Docker image built: $(DOCKER_IMAGE)"
# 运行Docker容器
docker-run:
@echo "Running Docker container..."
@docker-compose up -d
# 停止Docker容器
docker-stop:
@echo "Stopping Docker containers..."
@docker-compose down
# 查看Docker日志
docker-logs:
@docker-compose logs -f
# 清理构建产物
clean:
@echo "Cleaning build artifacts..."
@rm -rf bin/
@rm -f coverage.out coverage.html
@docker system prune -f
@echo "Clean complete"
# 安装开发工具
install-tools:
@echo "Installing development tools..."
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
@go install github.com/swaggo/swag/cmd/swag@latest
@go install github.com/golang/mock/mockgen@latest
@echo "Development tools installed"
# 生成Swagger文档
swagger:
@echo "Generating Swagger documentation..."
@swag init -g cmd/api/main.go
@echo "Swagger documentation generated"
# 生成Mock代码
generate-mocks:
@echo "Generating mock code..."
@go generate ./...
@echo "Mock code generated"
# 运行完整CI流程
ci: fmt lint test build
@echo "CI pipeline completed successfully"
# 部署到生产环境
deploy:
@echo "Deploying to production..."
@docker-compose -f docker-compose.prod.yml up -d
@echo "Deployment complete"
# 回滚部署
rollback:
@echo "Rolling back deployment..."
@docker-compose -f docker-compose.prod.yml down
@docker-compose -f docker-compose.prod.yml up -d
@echo "Rollback complete"
9. 部署和运维
9.1 环境配置
# config/config.yaml
app:
name: ecommerce-api
version: 1.0.0
env: development # development, staging, production
server:
host: 0.0.0.0
port: 8080
read_timeout: 30s
write_timeout: 30s
idle_timeout: 60s
max_header_bytes: 1048576 # 1MB
database:
driver: postgres # mysql, postgres, sqlite
host: localhost
port: 5432
username: ecommerce
password: ecommerce123
database: ecommerce_db
charset: utf8mb4
max_idle_conns: 10
max_open_conns: 100
conn_max_lifetime: 1h
conn_max_idle_time: 10m
log_level: info # error, warn, info, debug
redis:
host: localhost
port: 6379
password: ""
db: 0
pool_size: 10
min_idle_conns: 5
max_retries: 3
dial_timeout: 5s
read_timeout: 3s
write_timeout: 3s
jwt:
secret: your-secret-key-here
access_token_expire: 24h
refresh_token_expire: 168h # 7 days
log:
level: info # debug, info, warn, error
format: json # json, text
output: stdout # stdout, file
file_path: /var/log/ecommerce-api.log
max_size: 100 # MB
max_backups: 5
max_age: 30 # days
compress: true
metrics:
enabled: true
path: /metrics
namespace: ecommerce_api
tracing:
enabled: true
jaeger_endpoint: http://localhost:14268/api/traces
sample_rate: 1.0
security:
cors:
enabled: true
allowed_origins:
- http://localhost:3000
- https://example.com
allowed_methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowed_headers:
- Content-Type
- Authorization
- X-Request-ID
allow_credentials: true
rate_limit:
enabled: true
requests_per_minute: 100
burst: 10
9.2 监控和日志
// internal/infrastructure/monitoring/metrics.go
package monitoring
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// HTTP请求指标
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path", "status"},
)
// 业务指标
userRegistrationsTotal = promauto.NewCounter(
prometheus.CounterOpts{
Name: "user_registrations_total",
Help: "Total number of user registrations",
},
)
activeUsersGauge = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "active_users_count",
Help: "Number of active users",
},
)
// 数据库指标
dbQueriesTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "db_queries_total",
Help: "Total number of database queries",
},
[]string{"operation", "table"},
)
dbQueryDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "db_query_duration_seconds",
Help: "Database query duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"operation", "table"},
)
)
// RecordHTTPRequest 记录HTTP请求
func RecordHTTPRequest(method, path, status string, duration float64) {
httpRequestsTotal.WithLabelValues(method, path, status).Inc()
httpRequestDuration.WithLabelValues(method, path, status).Observe(duration)
}
// RecordUserRegistration 记录用户注册
func RecordUserRegistration() {
userRegistrationsTotal.Inc()
}
// SetActiveUsers 设置活跃用户数量
func SetActiveUsers(count float64) {
activeUsersGauge.Set(count)
}
// RecordDBQuery 记录数据库查询
func RecordDBQuery(operation, table string, duration float64) {
dbQueriesTotal.WithLabelValues(operation, table).Inc()
dbQueryDuration.WithLabelValues(operation, table).Observe(duration)
}
9.3 健康检查和就绪检查
// internal/interface/http/handler/health_handler.go
package httphandler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ecommerce-api/internal/infrastructure/response"
)
// HealthHandler 健康检查处理器
type HealthHandler struct {
db *gorm.DB
}
func NewHealthHandler(db *gorm.DB) *HealthHandler {
return &HealthHandler{db: db}
}
// HealthCheck 健康检查
// @Summary Health check
// @Description Check if the service is healthy
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 503 {object} response.Response
// @Router /health [get]
func (h *HealthHandler) HealthCheck(c *gin.Context) {
health := h.checkHealth()
if health.Status == "healthy" {
response.Success(c, http.StatusOK, "Service is healthy", health)
} else {
response.Error(c, http.StatusServiceUnavailable, "unhealthy", "Service is unhealthy", health)
}
}
// ReadinessCheck 就绪检查
// @Summary Readiness check
// @Description Check if the service is ready to serve requests
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 503 {object} response.Response
// @Router /ready [get]
func (h *HealthHandler) ReadinessCheck(c *gin.Context) {
ready := h.checkReadiness()
if ready.Status == "ready" {
response.Success(c, http.StatusOK, "Service is ready", ready)
} else {
response.Error(c, http.StatusServiceUnavailable, "not_ready", "Service is not ready", ready)
}
}
// LivenessCheck 存活检查
// @Summary Liveness check
// @Description Check if the service is alive
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Router /live [get]
func (h *HealthHandler) LivenessCheck(c *gin.Context) {
response.Success(c, http.StatusOK, "Service is alive", gin.H{
"status": "alive",
"timestamp": time.Now().Unix(),
})
}
type HealthStatus struct {
Status string `json:"status"`
Timestamp int64 `json:"timestamp"`
Checks map[string]Check `json:"checks"`
}
type Check struct {
Status string `json:"status"`
Message string `json:"message,omitempty"`
Latency time.Duration `json:"latency,omitempty"`
}
func (h *HealthHandler) checkHealth() HealthStatus {
checks := make(map[string]Check)
overallStatus := "healthy"
// 检查数据库连接
start := time.Now()
sqlDB, err := h.db.DB()
if err != nil {
checks["database"] = Check{
Status: "unhealthy",
Message: "Failed to get database connection: " + err.Error(),
}
overallStatus = "unhealthy"
} else {
if err := sqlDB.Ping(); err != nil {
checks["database"] = Check{
Status: "unhealthy",
Message: "Database ping failed: " + err.Error(),
}
overallStatus = "unhealthy"
} else {
checks["database"] = Check{
Status: "healthy",
Latency: time.Since(start),
}
}
}
return HealthStatus{
Status: overallStatus,
Timestamp: time.Now().Unix(),
Checks: checks,
}
}
func (h *HealthHandler) checkReadiness() HealthStatus {
checks := make(map[string]Check)
overallStatus := "ready"
// 检查数据库是否就绪
sqlDB, err := h.db.DB()
if err != nil {
checks["database"] = Check{
Status: "not_ready",
Message: "Database not available",
}
overallStatus = "not_ready"
} else {
if err := sqlDB.Ping(); err != nil {
checks["database"] = Check{
Status: "not_ready",
Message: "Database not responding",
}
overallStatus = "not_ready"
} else {
checks["database"] = Check{
Status: "ready",
}
}
}
return HealthStatus{
Status: overallStatus,
Timestamp: time.Now().Unix(),
Checks: checks,
}
}
总结
本教程完整实现了一个生产级的RESTful后端服务,涵盖了:
架构设计
- 分层架构:清晰的领域层、应用层、基础设施层分离
- DDD实现:聚合根、实体、值对象、仓储模式
- 依赖注入:松耦合的组件设计
核心功能
- CRUD操作:完整的用户管理功能
- 认证授权:JWT-based认证机制
- 数据验证:输入验证和业务规则验证
- 错误处理:统一的错误响应格式
测试策略
- 单元测试:使用mock对象隔离测试
- 集成测试:端到端的功能测试
- Table-Driven测试:覆盖多种场景
- 测试覆盖率:全面的测试覆盖
容器化部署
- Docker化:多阶段构建优化镜像大小
- Docker Compose:开发环境一键启动
- 生产配置:性能优化和安全配置
- 健康检查:容器健康状态监控
运维监控
- 日志系统:结构化日志记录
- 性能监控:Prometheus指标收集
- 配置管理:环境分离的配置管理
- 构建自动化:Makefile简化开发流程
最佳实践
- 代码组织:清晰的项目结构
- 错误处理:一致的错误处理机制
- 安全考虑:输入验证和SQL注入防护
- 性能优化:连接池和缓存策略
这个架构提供了:
- 可维护性:清晰的代码结构和文档
- 可测试性:易于测试的设计
- 可扩展性:支持业务增长和功能扩展
- 可部署性:容器化支持各种部署环境
通过掌握这些技术,你可以构建企业级的Go应用程序,满足生产环境的要求。
更多推荐


所有评论(0)