Skip to content

Service 层设计规范

概述

Service 层是业务逻辑的核心实现层,负责处理业务规则、协调数据访问和管理事务。本规范定义了 Service 层的设计标准和最佳实践。

职责定义

Service 层职责

Service 层负责以下工作:

  1. 实现业务逻辑 - 核心业务规则和计算
  2. 数据访问 - 通过 GORM 操作数据库
  3. 事务管理 - 确保数据一致性
  4. 跨实体协调 - 协调多个数据模型的操作
  5. 调用其他 Service - 通过依赖注入复用业务逻辑
  6. 调用第三方服务 - 通过 Client 层集成外部API

Service 层禁止事项

Service 层禁止执行以下操作:

  • 禁止直接处理 HTTP 请求(不接受 echo.Context
  • 禁止返回 HTTP 状态码
  • 禁止处理 JSON 序列化/反序列化
  • 禁止直接访问 Session 或 Cookie

结构体设计

标准结构体定义

Service 结构体包含所有依赖

go
package services

import (
    "your-project/config"
    "your-project/clients"
    "gorm.io/gorm"
)

// UserService 用户业务Service
type UserService struct {
    // 基础设施依赖
    db     *gorm.DB
    config *config.Config

    // 其他 Service 依赖
    emailService   *EmailService
    storageService *StorageService

    // Client 依赖(第三方服务)
    smsClient   *clients.SMSClient
    cacheClient *clients.CacheClient
}

// NewUserService 构造函数
func NewUserService(
    db *gorm.DB,
    config *config.Config,
    emailService *EmailService,
    storageService *StorageService,
) *UserService {
    return &UserService{
        db:             db,
        config:         config,
        emailService:   emailService,
        storageService: storageService,
    }
}

关键要点

  • 私有字段:所有字段使用小写开头(私有)
  • 明确依赖:在结构体中声明所有依赖
  • 构造函数注入:通过 NewXXXService 函数注入依赖

依赖类型

Service 可以依赖以下类型

依赖类型说明示例
*gorm.DB数据库连接所有需要数据库操作的 Service
*config.Config配置对象需要读取配置的 Service
其他 Service业务 Service*EmailService, *ProductService
Client第三方服务客户端*SMSClient, *StorageClient

方法签名规范

标准方法签名

Service 方法必须遵循以下签名格式

go
func (s *XxxService) MethodName(ctx context.Context, params...) (result, error)

规范要点

  1. 第一个参数必须是 context.Context
  2. 最后一个返回值必须是 error
  3. 方法名使用大驼峰(PascalCase)
  4. 参数使用小驼峰(camelCase)

完整示例

go
// Create 创建用户
func (s *UserService) Create(ctx context.Context, name, email, password string) (*models.User, error) {
    // 实现...
}

// GetByID 根据 ID 查询用户
func (s *UserService) GetByID(ctx context.Context, id uint) (*models.User, error) {
    // 实现...
}

// List 分页查询用户列表
func (s *UserService) List(ctx context.Context, page, pageSize int) ([]*models.User, int64, error) {
    // 实现...
}

// Update 更新用户信息
func (s *UserService) Update(ctx context.Context, id uint, updates map[string]interface{}) (*models.User, error) {
    // 实现...
}

// Delete 删除用户
func (s *UserService) Delete(ctx context.Context, id uint) error {
    // 实现...
}

context.Context 的使用

必须将 context.Context 传递给所有下游调用

go
func (s *OrderService) Create(ctx context.Context, userID uint, productID uint) (*models.Order, error) {
    // ✅ 传递 ctx 给 GORM
    var user models.User
    if err := s.db.WithContext(ctx).First(&user, userID).Error; err != nil {
        return nil, err
    }

    // ✅ 传递 ctx 给其他 Service
    product, err := s.productService.GetByID(ctx, productID)
    if err != nil {
        return nil, err
    }

    // ✅ 传递 ctx 给 Client
    paymentID, err := s.paymentClient.CreatePayment(ctx, amount)
    if err != nil {
        return nil, err
    }

    // ...
}

context 用途

  • 传递请求级别的值(如用户 ID、请求 ID)
  • 控制超时和取消
  • 链路追踪(OpenTelemetry)

请求结构体设计

Service 请求结构体

定义专门的请求结构体传递参数

go
// CreateUserRequest Service 创建用户请求
type CreateUserRequest struct {
    Name     string
    Email    string
    Password string
}

// UpdateUserRequest Service 更新用户请求(使用指针区分未提供字段)
type UpdateUserRequest struct {
    Name  *string
    Email *string
    Phone *string
}

方法使用请求结构体

go
func (s *UserService) Create(ctx context.Context, req *CreateUserRequest) (*models.User, error) {
    // 参数校验
    if req.Name == "" || req.Email == "" {
        return nil, errors.New("参数不完整")
    }

    // 业务逻辑...
}

func (s *UserService) Update(ctx context.Context, id uint, req *UpdateUserRequest) (*models.User, error) {
    // 只更新提供的字段
    updates := make(map[string]interface{})
    if req.Name != nil {
        updates["name"] = *req.Name
    }
    if req.Email != nil {
        updates["email"] = *req.Email
    }

    // 执行更新...
}

优势

  • ✅ 参数清晰易维护
  • ✅ 便于参数校验
  • ✅ 区分未提供字段和零值

业务逻辑实现

CRUD 操作

Create(创建)

go
func (s *ProductService) Create(ctx context.Context, req *CreateProductRequest) (*models.Product, error) {
    // 1. 参数校验
    if err := s.validateCreate(req); err != nil {
        return nil, err
    }

    // 2. 业务规则检查
    if req.Price < 0 {
        return nil, errors.New("价格不能为负数")
    }

    // 3. 构造数据模型
    product := &models.Product{
        Name:        req.Name,
        Description: req.Description,
        Price:       req.Price,
        Stock:       req.Stock,
        Status:      "active",  // 默认状态
    }

    // 4. 数据库操作
    if err := s.db.WithContext(ctx).Create(product).Error; err != nil {
        return nil, fmt.Errorf("创建商品失败: %w", err)
    }

    return product, nil
}

Read(查询)

go
// GetByID 根据 ID 查询
func (s *ProductService) GetByID(ctx context.Context, id uint) (*models.Product, error) {
    var product models.Product
    if err := s.db.WithContext(ctx).First(&product, id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("商品不存在")
        }
        return nil, fmt.Errorf("查询商品失败: %w", err)
    }
    return &product, nil
}

// List 分页查询列表
func (s *ProductService) List(ctx context.Context, page, pageSize int, filters *ProductFilters) ([]*models.Product, int64, error) {
    var products []*models.Product
    var total int64

    query := s.db.WithContext(ctx).Model(&models.Product{})

    // 应用过滤条件
    if filters.Category != "" {
        query = query.Where("category = ?", filters.Category)
    }
    if filters.MinPrice > 0 {
        query = query.Where("price >= ?", filters.MinPrice)
    }

    // 计算总数
    if err := query.Count(&total).Error; err != nil {
        return nil, 0, err
    }

    // 分页查询
    offset := (page - 1) * pageSize
    if err := query.Offset(offset).Limit(pageSize).Find(&products).Error; err != nil {
        return nil, 0, err
    }

    return products, total, nil
}

Update(更新)

go
func (s *ProductService) Update(ctx context.Context, id uint, req *UpdateProductRequest) (*models.Product, error) {
    // 1. 查询记录是否存在
    var product models.Product
    if err := s.db.WithContext(ctx).First(&product, id).Error; err != nil {
        return nil, errors.New("商品不存在")
    }

    // 2. 构造更新字段
    updates := make(map[string]interface{})
    if req.Name != nil {
        updates["name"] = *req.Name
    }
    if req.Price != nil {
        if *req.Price < 0 {
            return nil, errors.New("价格不能为负数")
        }
        updates["price"] = *req.Price
    }

    // 3. 执行更新
    if len(updates) > 0 {
        if err := s.db.WithContext(ctx).Model(&product).Updates(updates).Error; err != nil {
            return nil, fmt.Errorf("更新商品失败: %w", err)
        }
    }

    return &product, nil
}

Delete(删除)

go
// Delete 软删除
func (s *ProductService) Delete(ctx context.Context, id uint) error {
    result := s.db.WithContext(ctx).Delete(&models.Product{}, id)
    if result.Error != nil {
        return fmt.Errorf("删除商品失败: %w", result.Error)
    }
    if result.RowsAffected == 0 {
        return errors.New("商品不存在")
    }
    return nil
}

// HardDelete 物理删除
func (s *ProductService) HardDelete(ctx context.Context, id uint) error {
    result := s.db.WithContext(ctx).Unscoped().Delete(&models.Product{}, id)
    if result.Error != nil {
        return fmt.Errorf("删除商品失败: %w", result.Error)
    }
    if result.RowsAffected == 0 {
        return errors.New("商品不存在")
    }
    return nil
}

复杂业务逻辑

跨 Service 调用

go
func (s *OrderService) CreateOrder(ctx context.Context, userID, productID uint, quantity int) (*models.Order, error) {
    // 1. 调用 UserService 验证用户
    user, err := s.userService.GetByID(ctx, userID)
    if err != nil {
        return nil, fmt.Errorf("用户验证失败: %w", err)
    }
    if !user.IsActive {
        return nil, errors.New("用户已被禁用")
    }

    // 2. 调用 ProductService 检查库存
    product, err := s.productService.GetByID(ctx, productID)
    if err != nil {
        return nil, fmt.Errorf("商品查询失败: %w", err)
    }
    if product.Stock < quantity {
        return nil, errors.New("库存不足")
    }

    // 3. 计算金额
    amount := product.Price * float64(quantity)

    // 4. 创建订单
    order := &models.Order{
        UserID:    userID,
        ProductID: productID,
        Quantity:  quantity,
        Amount:    amount,
        Status:    "pending",
    }
    if err := s.db.WithContext(ctx).Create(order).Error; err != nil {
        return nil, err
    }

    // 5. 扣减库存(调用 ProductService)
    if err := s.productService.DeductStock(ctx, productID, quantity); err != nil {
        return nil, err
    }

    // 6. 发送通知(调用 NotifyService)
    s.notifyService.SendOrderCreated(ctx, order.ID, userID)

    return order, nil
}

调用第三方服务

go
func (s *UserService) UploadAvatar(ctx context.Context, userID uint, imageData []byte) (*models.User, error) {
    // 1. 查询用户
    var user models.User
    if err := s.db.WithContext(ctx).First(&user, userID).Error; err != nil {
        return nil, errors.New("用户不存在")
    }

    // 2. 调用 StorageClient 上传图片
    imageURL, err := s.storageClient.Upload(ctx, imageData, "avatars")
    if err != nil {
        return nil, fmt.Errorf("图片上传失败: %w", err)
    }

    // 3. 更新用户头像URL
    user.AvatarURL = imageURL
    if err := s.db.WithContext(ctx).Save(&user).Error; err != nil {
        return nil, err
    }

    return &user, nil
}

事务管理

标准事务模式

使用 GORM 的 Transaction 方法管理事务

go
func (s *OrderService) CreateOrderWithPayment(ctx context.Context, req *CreateOrderRequest) (*models.Order, error) {
    var order *models.Order

    // 开启事务
    err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // 1. 创建订单
        order = &models.Order{
            UserID:    req.UserID,
            ProductID: req.ProductID,
            Quantity:  req.Quantity,
            Amount:    req.Amount,
            Status:    "pending",
        }
        if err := tx.Create(order).Error; err != nil {
            return err  // 返回错误会自动回滚
        }

        // 2. 扣减库存
        if err := tx.Model(&models.Product{}).
            Where("id = ? AND stock >= ?", req.ProductID, req.Quantity).
            Update("stock", gorm.Expr("stock - ?", req.Quantity)).
            Error; err != nil {
            return err
        }

        // 3. 创建支付记录
        payment := &models.Payment{
            OrderID: order.ID,
            Amount:  req.Amount,
            Status:  "pending",
        }
        if err := tx.Create(payment).Error; err != nil {
            return err
        }

        return nil  // 返回 nil 会自动提交事务
    })

    if err != nil {
        return nil, fmt.Errorf("创建订单失败: %w", err)
    }

    return order, nil
}

关键点

  • 自动回滚:返回错误时自动回滚
  • 自动提交:返回 nil 时自动提交
  • 使用 tx:在事务闭包内使用 tx 而非 s.db

复杂事务处理

包含外部服务调用的事务

go
func (s *OrderService) CompleteOrder(ctx context.Context, orderID uint) error {
    return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // 1. 查询订单
        var order models.Order
        if err := tx.First(&order, orderID).Error; err != nil {
            return err
        }

        if order.Status != "pending" {
            return errors.New("订单状态不正确")
        }

        // 2. 调用支付服务(外部API)
        paymentID, err := s.paymentClient.CreatePayment(ctx, order.Amount)
        if err != nil {
            return fmt.Errorf("创建支付失败: %w", err)
        }

        // 3. 更新订单状态
        order.Status = "paid"
        order.PaymentID = paymentID
        if err := tx.Save(&order).Error; err != nil {
            // ⚠️ 注意:此时支付已创建,需要手动补偿
            return err
        }

        // 4. 创建发货记录
        shipment := &models.Shipment{
            OrderID: orderID,
            Status:  "pending",
        }
        if err := tx.Create(shipment).Error; err != nil {
            return err
        }

        return nil
    })
}

注意事项

  • ⚠️ 外部调用的幂等性:确保外部 API 调用可以重试
  • ⚠️ 补偿机制:外部调用成功但事务回滚时需要补偿

错误处理

错误返回规范

返回明确的业务错误

go
func (s *UserService) Login(ctx context.Context, email, password string) (*models.User, error) {
    // 1. 查询用户
    var user models.User
    if err := s.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("用户名或密码错误")  // ✅ 明确的业务错误
        }
        return nil, fmt.Errorf("查询用户失败: %w", err)
    }

    // 2. 校验密码
    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
        return nil, errors.New("用户名或密码错误")  // ✅ 同样的错误消息,避免信息泄漏
    }

    // 3. 检查用户状态
    if !user.IsActive {
        return nil, errors.New("用户已被禁用")  // ✅ 状态检查错误
    }

    return &user, nil
}

错误包装

使用 fmt.Errorf%w 包装错误

go
func (s *OrderService) GetOrderDetails(ctx context.Context, orderID uint) (*OrderDetails, error) {
    var order models.Order
    if err := s.db.WithContext(ctx).First(&order, orderID).Error; err != nil {
        // ✅ 包装底层错误,保留调用栈
        return nil, fmt.Errorf("查询订单失败: %w", err)
    }

    // 调用其他 Service
    product, err := s.productService.GetByID(ctx, order.ProductID)
    if err != nil {
        // ✅ 包装 Service 错误
        return nil, fmt.Errorf("查询商品信息失败: %w", err)
    }

    // ...
}

检查清单

编写 Service 时,确保以下要点:

  • [ ] Service 结构体包含所有依赖字段
  • [ ] 构造函数明确声明所有依赖参数
  • [ ] 方法签名第一个参数context.Context
  • [ ] 方法签名最后一个返回值error
  • [ ] 传递 context.Context 给所有下游调用
  • [ ] 事务操作使用 s.db.Transaction
  • [ ] 错误信息明确且对用户友好
  • [ ] 不接受 echo.Context 参数
  • [ ] 不返回 HTTP 状态码

相关规范