Service 层设计规范
概述
Service 层是业务逻辑的核心实现层,负责处理业务规则、协调数据访问和管理事务。本规范定义了 Service 层的设计标准和最佳实践。
职责定义
Service 层职责
Service 层负责以下工作:
- 实现业务逻辑 - 核心业务规则和计算
- 数据访问 - 通过 GORM 操作数据库
- 事务管理 - 确保数据一致性
- 跨实体协调 - 协调多个数据模型的操作
- 调用其他 Service - 通过依赖注入复用业务逻辑
- 调用第三方服务 - 通过 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)规范要点:
- 第一个参数必须是
context.Context - 最后一个返回值必须是
error - 方法名使用大驼峰(PascalCase)
- 参数使用小驼峰(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 状态码