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 事务保证一致性避坑指南
- 不要在服务层依赖 Request/Response:保持服务层的纯粹性
- 抛出语义化错误:使用自定义错误类,而非通用 Error
- 单一职责:一个服务方法只做一件事
- 避免循环依赖:服务之间的调用要小心
验收清单
- [ ] 服务层不依赖 HTTP 相关对象
- [ ] 业务规则集中在服务层
- [ ] 使用事务处理多步骤操作
- [ ] 抛出有意义的错误信息
