Services层设计

要点

  • Services 层的核心职责是承载业务逻辑——它接收来自 Routes 层的调用,按业务规则编排数据,再把结果交还给 Routes 层或传给 Repositories 层
  • 判断一段代码是否属于 Service,可以问一个问题:「如果把 HTTP 换掉,这段逻辑还有意义吗?」有意义就属于 Service,没意义就留在 Routes 层
  • Service 的组织方式有两条路——函数式和类式。小项目用函数式就够了,需要依赖注入或共享状态时再切类式
  • Service 之间的依赖通过参数传入,不要在 Service 内部直接 import 另一个 Service 的实例
  • 错误处理在 Service 层需要区分「业务错误」和「系统错误」,前者是预期内的规则冲突,后者是预期外的基础设施故障
  • 事务边界由 Service 层控制,Repository 只负责单条 SQL 的原子性,跨 Repository 的一致性由 Service 协调
  • 测试 Service 时 mock 掉 Repository 层,让测试只验证业务逻辑本身

1. Service 层解决什么问题

02 把项目按职责拆成了 routes/middleware/db/lib/ 几个目录。路由文件里直接写 db.select().from(usersTable) 确实能跑。但当业务规则逐渐变多——注册前要检查邮箱是否重复、密码要加盐哈希、注册后要发验证邮件、不同角色有不同的创建权限——这些逻辑继续留在路由处理函数里,会让单个函数膨胀到上百行。

Services 层的作用是把「做什么」和「怎么做」分开。Routes 层只关心「接收请求、返回响应」,Services 层负责「按业务规则把事做完」。

// src/routes/users.ts
// Routes 层:只处理 HTTP 协议相关的事
import { userService } from '../services/user'
 
export const usersApp.get('/', authMiddleware, async (c) => {
  const body = await c.req.json()
 
  // 路由层不关心注册逻辑的细节,只负责参数传递和响应格式化
  const result = await userService.register(body.email, body.password)
 
  return c.json(result, 201)
})
// src/services/user.ts
// Service 层:承载业务规则
import { userRepository } from '../repositories/user'
import { hashPassword } from '../lib/password'
 
async function register(email: string, password: string) {
  // 业务规则 1:检查邮箱是否已注册
  const existing = await userRepository.findByEmail(email)
  if (existing) {
    throw new BusinessError('EMAIL_ALREADY_EXISTS', '该邮箱已被注册')
  }
 
  // 业务规则 2:密码加盐哈希
  const hashedPassword = await hashPassword(password)
 
  // 业务规则 3:创建用户
  const user = await userRepository.create({
    email,
    password: hashedPassword,
  })
 
  return { id: user.id, email: user.email }
}

这个分工带来一个直接的好处: Routes 层的处理函数保持简短,业务规则集中在 Service 层,修改业务逻辑时不需要在路由文件里翻找。

2. Service 层的边界

判断一段代码是否属于 Service,有一个比较实用的标准:如果把 HTTP 协议换掉(比如改成 gRPC、消息队列消费、定时任务),这段逻辑是否仍然需要? 需要的话就属于 Service,不需要的话就留在 Routes 层。

属于 Service 层的职责

  1. 业务规则校验:邮箱是否重复、订单金额是否超过限额、用户是否有权限执行某操作
  2. 数据编排:一个操作需要跨多个 Repository 读写数据时,由 Service 协调调用顺序
  3. 数据转换:把数据库的行记录转换成业务对象或 DTO,把外部 API 返回的格式转换成内部模型
  4. 外部服务调用:发邮件、调第三方支付、推送通知——这些动作的业务编排属于 Service
  5. 领域事件触发:注册完成后发验证邮件、订单支付完成后更新库存

不属于 Service 层的职责

  1. HTTP 协议细节:状态码选择、请求头解析、响应体格式化——这些是 Routes 层的事
  2. 数据库查询语句SELECTINSERT 这类具体 SQL 由 Repository 层负责,Service 只调用 Repository 暴露的方法
  3. 请求参数校验:参数格式校验(类型、必填、长度)在 Routes 层通过 validator 中间件完成,Service 接收的参数已经是合法值
  4. 认证与授权的前置判断:JWT 解析、Token 验证由中间件完成,Service 接收的是已确认身份的用户信息
// src/services/user.ts
// ✅ Service 做业务校验
async function updateRole(userId: number, newRole: string) {
  const user = await userRepository.findById(userId)
  if (!user) {
    throw new BusinessError('USER_NOT_FOUND', '用户不存在')
  }
 
  // 业务规则:只有 admin 才能被设置为 moderator
  if (newRole === 'moderator' && user.role !== 'admin') {
    throw new BusinessError('INSUFFICIENT_ROLE', '权限不足')
  }
 
  return userRepository.updateRole(userId, newRole)
}
// src/routes/users.ts
// ✅ Route 做 HTTP 格式校验
import { z } from 'zod'
import { validator } from 'hono/validator'
 
const updateRoleSchema = z.object({
  userId: z.number().int().positive(),
  newRole: z.enum(['admin', 'moderator', 'user']),
})
 
app.post(
  '/users/role',
  validator('json', (value, c) => {
    const result = updateRoleSchema.safeParse(value)
    if (!result.success) {
      return c.json({ error: 'invalid input' }, 400)
    }
    return result.data
  }),
  async (c) => {
    const { userId, newRole } = c.req.valid('json')
    // 参数格式校验已在 validator 完成,Service 只做业务规则校验
    const result = await userService.updateRole(userId, newRole)
    return c.json(result)
  }
)

3. 函数式与类式 Service

Service 的组织方式有两种常见选择。两种没有绝对优劣,按项目规模和团队偏好来定。

函数式 Service

每个 Service 是一个文件,导出若干函数。函数接收需要的依赖作为参数,或通过闭包访问共享的 Repository 实例。

// src/services/user.ts
import { userRepository } from '../repositories/user'
import { emailService } from './email'
 
// 直接 import 其他模块的实例
export async function register(email: string, password: string) {
  const existing = await userRepository.findByEmail(email)
  if (existing) {
    throw new BusinessError('EMAIL_ALREADY_EXISTS', '该邮箱已被注册')
  }
 
  const hashedPassword = await hashPassword(password)
  const user = await userRepository.create({ email, password: hashedPassword })
 
  // 调用其他 Service
  await emailService.sendVerification(user.email)
 
  return { id: user.id, email: user.email }
}
 
export async function getById(id: number) {
  return userRepository.findById(id)
}

函数式的优点是简单直接——文件即模块,不需要实例化过程,调用时 import { register } from '../services/user' 就行。适合 Service 的依赖关系比较固定、不需要在测试和运行时切换实现的场景。

类式 Service

用 class 封装 Service,依赖通过构造函数注入。

// src/services/user.ts
import type { UserRepository } from '../repositories/user'
import type { EmailService } from './email'
 
export class UserService {
  // 依赖通过构造函数注入,不在内部直接 import
  constructor(
    private userRepository: UserRepository,
    private emailService: EmailService
  ) {}
 
  async register(email: string, password: string) {
    const existing = await this.userRepository.findByEmail(email)
    if (existing) {
      throw new BusinessError('EMAIL_ALREADY_EXISTS', '该邮箱已被注册')
    }
 
    const hashedPassword = await hashPassword(password)
    const user = await this.userRepository.create({
      email,
      password: hashedPassword,
    })
 
    await this.emailService.sendVerification(user.email)
    return { id: user.id, email: user.email }
  }
 
  async getById(id: number) {
    return this.userRepository.findById(id)
  }
}
// src/services/container.ts
// 统一的 Service 实例工厂
import { UserService } from './user'
import { EmailService } from './email'
import { userRepository } from '../repositories/user'
 
// 在应用启动时组装依赖图
const emailService = new EmailService({ apiKey: process.env.EMAIL_API_KEY! })
export const userService = new UserService(userRepository, emailService)

类式的优点是依赖关系显式声明,测试时可以轻松替换 Repository 为 mock 实现。代价是需要一个组装依赖的地方(通常叫 containerdi)。

如何选择

项目里 Service 不超过五个、依赖关系简单时,函数式足够。当出现以下情况时,切到类式会更顺畅:

  1. 同一个 Service 在测试和生产环境需要不同的 Repository 实现
  2. 多个 Service 共享一组依赖,需要统一管理
  3. 团队习惯面向接口编程,需要明确的类型约束

两种风格也可以共存。核心 Service(如用户、订单)用类式,辅助 Service(如格式化、计算)用函数式。

4. 依赖注入

Service 层需要访问 Repository、外部 API 客户端、配置等外部依赖。依赖的获取方式直接影响代码的可测试性和可维护性。

构造函数注入

类式 Service 最标准的做法,前面已经展示过。构造函数声明所有依赖,实例化时一次性传入:

// src/services/order.ts
import type { OrderRepository } from '../repositories/order'
import type { UserRepository } from '../repositories/user'
import type { PaymentClient } from '../clients/payment'
 
export class OrderService {
  constructor(
    private orderRepo: OrderRepository,
    private userRepo: UserRepository,
    private paymentClient: PaymentClient
  ) {}
 
  async createOrder(userId: number, items: OrderItem[]) {
    const user = await this.userRepo.findById(userId)
    if (!user) throw new BusinessError('USER_NOT_FOUND', '用户不存在')
 
    const total = calculateTotal(items)
    const payment = await this.paymentClient.charge(user.paymentMethodId, total)
 
    return this.orderRepo.create({
      userId,
      items,
      total,
      paymentId: payment.id,
    })
  }
}

工厂函数注入

函数式 Service 也可以用类似思路——用一个工厂函数接收依赖,返回绑定好依赖的函数集合:

// src/services/user.ts
import type { UserRepository } from '../repositories/user'
import type { EmailService } from './email'
 
export function createUserService(deps: {
  userRepo: UserRepository
  emailService: EmailService
}) {
  const { userRepo, emailService } = deps
 
  async function register(email: string, password: string) {
    const existing = await userRepo.findByEmail(email)
    if (existing) {
      throw new BusinessError('EMAIL_ALREADY_EXISTS', '该邮箱已被注册')
    }
    const hashedPassword = await hashPassword(password)
    const user = await userRepo.create({ email, password: hashedPassword })
    await emailService.sendVerification(user.email)
    return { id: user.id, email: user.email }
  }
 
  async function getById(id: number) {
    return userRepo.findById(id)
  }
 
  return { register, getById }
}
// src/services/container.ts
export const userService = createUserService({
  userRepo: userRepository,
  emailService,
})

工厂函数的思路和构造函数注入一致——依赖从外部传入,不在内部直接获取——只是用闭包替代了 class 的私有字段。

不要这样做

// src/services/user.ts
// ❌ 在 Service 内部直接创建 Repository 实例
import { drizzle } from 'drizzle-orm/d1'
 
export async function register(email: string) {
  // Service 直接创建了数据库连接,测试时无法替换
  const db = drizzle(c.env.DB)
  const existing = await db.select().from(usersTable).where(...)
}

这样做的问题是:Service 和具体的数据库实现绑死了,测试时没法把数据库替换成 mock,部署到不同环境时也没法切换数据源。

5. Service 之间的组合

一个复杂操作通常需要多个 Service 协作。比如「用户下单」涉及 OrderService、UserService、InventoryService、PaymentService、NotificationService。

直接注入

最常见的做法是让需要协调的 Service 持有其他 Service 的引用:

// src/services/order.ts
export class OrderService {
  constructor(
    private orderRepo: OrderRepository,
    private userService: UserService,
    private inventoryService: InventoryService,
    private paymentService: PaymentService,
    private notificationService: NotificationService
  ) {}
 
  async placeOrder(userId: number, items: OrderItem[]) {
    // 1. 验证用户
    const user = await this.userService.getById(userId)
    if (!user) throw new BusinessError('USER_NOT_FOUND', '用户不存在')
 
    // 2. 检查库存
    await this.inventoryService.reserve(items)
 
    // 3. 扣款
    const payment = await this.paymentService.charge(user.paymentMethodId, calculateTotal(items))
 
    // 4. 创建订单记录
    const order = await this.orderRepo.create({ userId, items, paymentId: payment.id })
 
    // 5. 发送通知
    await this.notificationService.sendOrderConfirmation(user.email, order)
 
    return order
  }
}

编排 vs 委托

上面的 placeOrder 是一个「编排者」——它知道完整的业务流程,按顺序调用各个 Service。每个被调用的 Service 只负责自己领域的逻辑。

需要注意「编排」和「委托」的区别。编排是 Service A 知道要调用 B、C、D,按顺序组织调用。委托是 Service A 把一整块业务交给 Service B,不关心 B 内部怎么做。

// ✅ 编排:OrderService 知道下单流程的步骤
async placeOrder(userId: number, items: OrderItem[]) {
  await this.userService.validate(userId)
  await this.inventoryService.reserve(items)
  const payment = await this.paymentService.charge(...)
  const order = await this.orderRepo.create(...)
  await this.notificationService.sendConfirmation(...)
  return order
}
 
// ✅ 委托:OrderService 只关心订单创建,库存和支付由各自的 Service 全权处理
async createOrder(userId: number, items: OrderItem[], paymentMethodId: string) {
  const order = await this.orderRepo.create({ userId, items })
  return order
}

编排放在哪里,取决于业务变化的频率。如果下单流程经常调整(加步骤、改顺序),放在一个专门的编排 Service 里比较合适。如果各步骤独立变化,编排逻辑放在 Routes 层也可以接受。

6. Service 层的错误处理

Service 层的错误需要区分两类:业务错误(预期内)和系统错误(预期外)。

业务错误

业务错误是指业务规则被触发时的正常失败——邮箱已注册、余额不足、库存不够。这类错误是业务流程的一部分,Routes 层收到后应该返回合适的 HTTP 状态码(4xx)给客户端。

// src/errors.ts
// 业务错误:有明确的错误码和消息,Routes 层可以据此构造响应
export class BusinessError extends Error {
  constructor(
    public code: string,
    message: string
  ) {
    super(message)
    this.name = 'BusinessError'
  }
}

系统错误

系统错误是指预期外的故障——数据库连接断开、第三方 API 超时、内存不足。这类错误通常意味着基础设施出了问题,Routes 层收到后应该返回 500 并触发告警。

// src/services/payment.ts
export class PaymentService {
  constructor(private client: PaymentClient) {}
 
  async charge(methodId: string, amount: number): Promise<Payment> {
    try {
      return await this.client.charge(methodId, amount)
    } catch (error) {
      // 区分错误类型
      if (error instanceof PaymentDeclinedError) {
        // 业务错误:支付方式被拒绝(余额不足、卡过期等)
        throw new BusinessError('PAYMENT_DECLINED', '支付被拒绝,请更换支付方式')
      }
 
      // 系统错误:网络超时、服务不可用
      // 不在 Service 层吞掉,让上层统一处理
      throw error
    }
  }
}
// src/middleware/error-handler.ts
// Routes 层的统一错误处理
export async function errorHandler(c: Context, next: Next) {
  try {
    await next()
  } catch (error) {
    if (error instanceof BusinessError) {
      // 业务错误:返回 4xx,不需要记录堆栈
      return c.json({ code: error.code, message: error.message }, 400)
    }
 
    // 系统错误:记录日志、触发告警、返回 500
    console.error('[Unexpected Error]', error)
    return c.json({ message: 'Internal Server Error' }, 500)
  }
}

这种分层处理让 Service 层专注于「这个错误属于什么性质」,Routes 层专注于「用什么 HTTP 状态码回应」。

7. 事务管理

当一个业务操作需要修改多张表的数据时,需要保证这些修改要么全部成功,要么全部回滚。事务的边界由 Service 层控制。

// src/services/transfer.ts
export class TransferService {
  constructor(
    private accountRepo: AccountRepository,
    private transactionLogRepo: TransactionLogRepository
  ) {}
 
  async transfer(fromId: number, toId: number, amount: number) {
    // 事务边界由 Service 控制
    // 使用 tx 参数让多个 Repository 操作共享同一个事务
    return this.accountRepo.withTransaction(async (tx) => {
      // 在同一个事务内完成扣款和入账
      await this.accountRepo.deduct(fromId, amount, tx)
      await this.accountRepo.add(toId, amount, tx)
      await this.transactionLogRepo.record(
        { fromId, toId, amount, timestamp: new Date() },
        tx
      )
    })
  }
}

Repository 层提供 withTransaction 方法,接收一个回调函数,把事务对象 tx 传进去。Service 层在回调内组织多次 Repository 调用,保证它们在同一事务内执行。

如果 Service 调用了另一个 Service,而那个 Service 也需要事务,有两种处理方式:

// 方式一:把事务对象显式传递
async placeOrder(userId: number, items: OrderItem[]) {
  return this.orderRepo.withTransaction(async (tx) => {
    await this.inventoryService.reserve(items, tx)
    const order = await this.orderRepo.create({ userId, items }, tx)
    return order
  })
}
 
// 方式二:Service 内部各管各的事务(不推荐用于需要跨 Service 一致性的场景)
async placeOrder(userId: number, items: OrderItem[]) {
  await this.inventoryService.reserve(items) // 独立事务
  const order = await this.orderRepo.create({ userId, items }) // 独立事务
  return order
}

方式一能保证跨 Service 操作的原子性,但会让 Service 接口多出 tx 参数。方式二更简洁,但如果 reserve 成功而 create 失败,库存已经扣了却没有订单记录。选择哪种方式取决于业务对一致性的要求。

8. 测试 Service

Service 层的测试重点是业务逻辑本身——规则是否正确、边界条件是否处理、调用顺序是否符合预期。Repository 和外部服务的实际行为不在测试范围内,用 mock 替换掉。

函数式 Service 的测试

// tests/services/user.test.ts
import { describe, it, expect, vi } from 'vitest'
import { createUserService } from '../../src/services/user'
 
describe('UserService.register', () => {
  it('邮箱已存在时抛出 EMAIL_ALREADY_EXISTS', async () => {
    const mockUserRepo = {
      findByEmail: vi.fn().mockResolvedValue({ id: 1, email: '[email protected]' }),
      create: vi.fn(),
    }
    const mockEmailService = {
      sendVerification: vi.fn(),
    }
 
    const userService = createUserService({
      userRepo: mockUserRepo,
      emailService: mockEmailService,
    })
 
    await expect(userService.register('[email protected]', 'password'))
      .rejects.toThrow('EMAIL_ALREADY_EXISTS')
 
    // 确认 create 没有被调用
    expect(mockUserRepo.create).not.toHaveBeenCalled()
  })
 
  it('注册成功后发送验证邮件', async () => {
    const mockUserRepo = {
      findByEmail: vi.fn().mockResolvedValue(null),
      create: vi.fn().mockResolvedValue({ id: 1, email: '[email protected]' }),
    }
    const mockEmailService = {
      sendVerification: vi.fn(),
    }
 
    const userService = createUserService({
      userRepo: mockUserRepo,
      emailService: mockEmailService,
    })
 
    await userService.register('[email protected]', 'password')
 
    expect(mockEmailService.sendVerification).toHaveBeenCalledWith('[email protected]')
  })
})

类式 Service 的测试

类式 Service 的测试思路一样,只是实例化方式不同:

// tests/services/order.test.ts
import { describe, it, expect, vi } from 'vitest'
import { OrderService } from '../../src/services/order'
 
describe('OrderService.placeOrder', () => {
  it('库存不足时抛出 OUT_OF_STOCK', async () => {
    const mockInventoryService = {
      reserve: vi.fn().mockRejectedValue(
        new BusinessError('OUT_OF_STOCK', '库存不足')
      ),
    }
 
    const orderService = new OrderService(
      mockOrderRepo,
      mockUserService,
      mockInventoryService,
      mockPaymentService,
      mockNotificationService
    )
 
    await expect(orderService.placeOrder(1, [{ sku: 'ABC', qty: 100 }]))
      .rejects.toThrow('OUT_OF_STOCK')
  })
})

测试 Service 时,mock 的粒度是 Repository 接口,不是数据库。这样做的好处是:

  1. 测试不依赖数据库运行环境,速度快
  2. 测试只验证 Service 的逻辑——它是否正确地调用了 Repository、处理了返回值、抛出了预期的错误
  3. Repository 自身的正确性由 Repository 层的测试负责

9. 常见反模式

贫血 Service

Service 只是透传 Repository 的调用,没有任何业务逻辑:

// ❌ 贫血 Service——只是 Repository 的搬运工
export class UserService {
  constructor(private userRepo: UserRepository) {}
 
  async getById(id: number) {
    return this.userRepo.findById(id)
  }
 
  async create(data: CreateUserInput) {
    return this.userRepo.create(data)
  }
 
  async update(id: number, data: UpdateUserInput) {
    return this.userRepo.update(id, data)
  }
}

如果 Service 里没有任何业务规则、数据转换或跨 Repository 编排,那这个 Service 可能没有必要存在。Routes 层直接调用 Repository 反而更直接。

判断标准:Service 里的每个方法都是单行透传吗?如果是,考虑去掉这层,或者把散落在 Routes 层的业务逻辑下沉到 Service。

上帝 Service

一个 Service 承担了太多职责,文件膨胀到上千行:

// ❌ 上帝 Service——什么都做
export class AppService {
  // 用户相关
  async registerUser(...) { ... }
  async loginUser(...) { ... }
  async resetPassword(...) { ... }
 
  // 订单相关
  async createOrder(...) { ... }
  async cancelOrder(...) { ... }
  async refundOrder(...) { ... }
 
  // 商品相关
  async createProduct(...) { ... }
  async updatePrice(...) { ... }
 
  // 通知相关
  async sendEmail(...) { ... }
  async sendSMS(...) { ... }
  async pushNotification(...) { ... }
}

上帝 Service 的特征是方法数超过 15-20 个,或涉及多个不相关的业务领域。按业务领域拆分——UserService、OrderService、ProductService、NotificationService——每个 Service 只负责自己领域的逻辑。

循环依赖

Service A 依赖 Service B,Service B 又依赖 Service A:

// ❌ UserService 依赖 OrderService
export class UserService {
  constructor(private orderService: OrderService) {}
 
  async getUserOrderCount(userId: number) {
    return this.orderService.countByUserId(userId)
  }
}
 
// ❌ OrderService 依赖 UserService
export class OrderService {
  constructor(private userService: UserService) {}
 
  async createOrder(userId: number, items: OrderItem[]) {
    const user = await this.userService.getById(userId)
    // ...
  }
}

循环依赖通常是 Service 职责划分不当的信号。解决方式:

  1. 提取共享逻辑到第三个 Service:如果 getUserOrderCount 需要同时访问用户和订单信息,创建一个 UserOrderQueryService
  2. 把查询下沉到 RepositorygetUserOrderCount 本质是一个查询,可以直接在 Repository 层用一条 SQL 完成
  3. 事件驱动:OrderService 创建订单后发布一个事件,UserService 订阅这个事件来更新统计,而不是被直接调用

在 Service 层处理 HTTP 细节

// ❌ Service 里不应该出现 c.json()、c.status() 这类 HTTP 操作
export class UserService {
  async register(email: string, password: string, c: Context) {
    if (!email) {
      return c.json({ error: 'email required' }, 400)
    }
    // ...
    return c.json({ id: user.id }, 201)
  }
}

Service 层的返回值应该是业务数据(对象、数组),不是 HTTP Response。把 Context 传进 Service 会让 Service 和 Hono 框架耦合,无法在非 HTTP 场景下复用。

10. 何时拆分 Service

一个 Service 在以下情况下应该考虑拆分:

  1. 方法数超过 10-15 个:单个文件超过 300 行时,阅读和维护成本明显上升
  2. 涉及多个业务领域:UserService 里出现了订单逻辑,说明它可能该叫 UserOrderService,或者订单逻辑应该挪到 OrderService
  3. 依赖注入过多:构造函数超过 5 个参数,说明这个 Service 承担了太多协调工作
  4. 不同部分的变更频率差异大:用户注册逻辑每周改,用户权限逻辑半年改一次——它们不应该在同一个文件里

拆分时不需要一步到位。先按业务领域拆成独立的 Service 文件,再逐步调整依赖关系。

// 拆分前:一个 UserService 处理所有用户相关逻辑
// src/services/user.ts
 
// 拆分后:按职责细分
// src/services/user-command.ts — 写操作(注册、修改、删除)
// src/services/user-query.ts   — 读操作(查询、统计、列表)
// src/services/user-auth.ts    — 认证逻辑(登录、登出、Token 管理)

读操作和写操作的变化原因通常不同。查询条件经常因为前端需求调整(加筛选、改排序),而写操作的变化来自业务规则的演进(加校验、改流程)。把它们分开后,修改查询逻辑不会碰到写操作的代码,反之亦然。

延伸阅读

总结

Services 层在项目结构中扮演的角色是「业务逻辑的家」。它从 Routes 层接收已经校验过的参数,按业务规则编排 Repository 和外部服务的调用,再把结果返回给 Routes 层格式化成 HTTP 响应。

几个关键判断:

  1. 归属判断:把 HTTP 换掉后仍然有意义的逻辑,属于 Service 层
  2. 依赖方向:Service 依赖 Repository,不依赖 Routes 层;依赖从外部注入,不在内部直接获取
  3. 错误分类:业务错误用自定义异常类表达,系统错误向上传播由统一中间件处理
  4. 事务边界:跨 Repository 的一致性由 Service 层控制,Repository 只保证单条操作的原子性
  5. 拆分信号:方法数超过 15 个、涉及多个业务领域、依赖注入过多——这些都是拆分的信号

下一篇进入 Repositories 层设计——Repository 如何封装数据库访问、如何定义接口让 Service 层不直接依赖 SQL,以及 Drizzle ORM 在 Repository 层的集成方式。