⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

3.6.3 业务逻辑放哪里——服务层设计

一句话破题

服务层是业务规则的家,让 API Route 保持简洁,让逻辑可测试、可复用。

核心价值

当业务逻辑散落在 API Route 各处时,代码很难复用,也很难测试。服务层把"做什么事"和"怎么接收请求"分开,让代码更清晰。

服务层的职责

应该在服务层不应该在服务层
业务规则判断HTTP 请求解析
数据转换加工响应格式构造
多表操作协调认证鉴权检查
第三方服务调用缓存 header 设置

基础服务结构

tsx
// services/postService.ts

import { prisma } from '@/lib/prisma'
import { CreatePostInput, UpdatePostInput } from '@/lib/validations/post'
import { NotFoundError, ConflictError } from '@/lib/errors'

export const postService = {
  async findAll(options: { page: number; limit: number; status?: string }) {
    const { page, limit, status } = options
    const skip = (page - 1) * limit
    
    const [posts, total] = await Promise.all([
      prisma.post.findMany({
        where: status ? { status } : undefined,
        skip,
        take: limit,
        orderBy: { createdAt: 'desc' },
      }),
      prisma.post.count({ where: status ? { status } : undefined }),
    ])
    
    return { posts, total, page, limit }
  },

  async findById(id: string) {
    const post = await prisma.post.findUnique({ where: { id } })
    if (!post) {
      throw new NotFoundError('文章不存在')
    }
    return post
  },

  async create(data: CreatePostInput) {
    const slug = this.generateSlug(data.title)
    
    const existing = await prisma.post.findUnique({ where: { slug } })
    if (existing) {
      throw new ConflictError('该标题已被使用')
    }
    
    return prisma.post.create({
      data: { ...data, slug },
    })
  },

  async update(id: string, data: UpdatePostInput) {
    await this.findById(id)
    
    if (data.title) {
      const slug = this.generateSlug(data.title)
      const existing = await prisma.post.findFirst({
        where: { slug, NOT: { id } },
      })
      if (existing) {
        throw new ConflictError('该标题已被使用')
      }
      data = { ...data, slug }
    }
    
    return prisma.post.update({ where: { id }, data })
  },

  async delete(id: string) {
    await this.findById(id)
    return prisma.post.delete({ where: { id } })
  },

  generateSlug(title: string) {
    return title
      .toLowerCase()
      .trim()
      .replace(/\s+/g, '-')
      .replace(/[^\w\-]+/g, '')
  },
}

在 API Route 中使用

tsx
// app/api/posts/route.ts
import { postService } from '@/services/postService'
import { createPostSchema, queryPostsSchema } from '@/lib/validations/post'
import { handleError } from '@/lib/errorHandler'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const result = queryPostsSchema.safeParse(Object.fromEntries(searchParams))
  
  if (!result.success) {
    return Response.json({ error: result.error.flatten() }, { status: 400 })
  }
  
  try {
    const data = await postService.findAll(result.data)
    return Response.json({ data })
  } catch (error) {
    return handleError(error)
  }
}

export async function POST(request: Request) {
  const body = await request.json()
  const result = createPostSchema.safeParse(body)
  
  if (!result.success) {
    return Response.json({ error: result.error.flatten() }, { status: 400 })
  }
  
  try {
    const post = await postService.create(result.data)
    return Response.json({ data: post }, { status: 201 })
  } catch (error) {
    return handleError(error)
  }
}

复杂业务示例

场景:发布文章时需要:检查作者权限、生成 slug、发送通知

tsx
// services/postService.ts
async publish(postId: string, userId: string) {
  const post = await this.findById(postId)
  
  if (post.authorId !== userId) {
    throw new ForbiddenError('只能发布自己的文章')
  }
  
  if (post.status === 'published') {
    throw new ConflictError('文章已发布')
  }
  
  const updatedPost = await prisma.post.update({
    where: { id: postId },
    data: { 
      status: 'published',
      publishedAt: new Date(),
    },
  })
  
  await notificationService.notifyFollowers(userId, {
    type: 'new_post',
    postId: updatedPost.id,
  })
  
  return updatedPost
}

事务处理

多个数据库操作需要原子性时,使用事务:

tsx
async createOrder(userId: string, items: OrderItem[]) {
  return prisma.$transaction(async (tx) => {
    const order = await tx.order.create({
      data: { userId, status: 'pending' },
    })
    
    for (const item of items) {
      const product = await tx.product.findUnique({
        where: { id: item.productId },
      })
      
      if (!product || product.stock < item.quantity) {
        throw new Error(`商品 ${item.productId} 库存不足`)
      }
      
      await tx.product.update({
        where: { id: item.productId },
        data: { stock: { decrement: item.quantity } },
      })
      
      await tx.orderItem.create({
        data: {
          orderId: order.id,
          productId: item.productId,
          quantity: item.quantity,
          price: product.price,
        },
      })
    }
    
    const total = await tx.orderItem.aggregate({
      where: { orderId: order.id },
      _sum: { price: true },
    })
    
    return tx.order.update({
      where: { id: order.id },
      data: { total: total._sum.price || 0 },
      include: { items: true },
    })
  })
}

目录结构建议

src/
├── app/
│   └── api/
│       └── posts/
│           └── route.ts       # 只处理 HTTP
├── services/
│   ├── postService.ts         # 文章业务逻辑
│   ├── userService.ts         # 用户业务逻辑
│   └── notificationService.ts # 通知服务
├── repositories/              # 可选,进一步分离数据访问
│   └── postRepository.ts
└── lib/
    ├── prisma.ts              # Prisma 客户端
    └── errors.ts              # 自定义错误类

AI 协作指南

核心意图:让 AI 帮你设计业务逻辑清晰的服务层。

需求定义公式

  • 功能描述:实现 [业务场景] 的服务方法
  • 业务规则:[规则1]、[规则2]、[规则3]
  • 技术要求:使用 Prisma,抛出自定义错误

关键术语Service事务$transaction、业务规则、错误处理

示例 Prompt

请实现用户注册服务:
1. 检查邮箱是否已注册
2. 检查用户名是否已被使用
3. 密码加密存储
4. 创建用户后发送欢迎邮件
5. 返回用户信息(不含密码)
使用 Prisma 事务保证一致性

避坑指南

  1. 不要在服务层依赖 Request/Response:保持服务层的纯粹性
  2. 抛出语义化错误:使用自定义错误类,而非通用 Error
  3. 单一职责:一个服务方法只做一件事
  4. 避免循环依赖:服务之间的调用要小心

验收清单

  • [ ] 服务层不依赖 HTTP 相关对象
  • [ ] 业务规则集中在服务层
  • [ ] 使用事务处理多步骤操作
  • [ ] 抛出有意义的错误信息