# 服务器控制器和服务

> Source: https://strapi.nodejs.cn/cms/plugins-development/server-controllers-services

🌐 Server API: Controllers & services

就像 Strapi 核心一样，插件也可以有控制器和服务。插件控制器处理 HTTP 层：它们接收 `ctx`、调用服务并返回响应。插件服务包含可重用的业务逻辑，并通过文档服务 API 与内容类型交互。保持控制器简洁，将字段逻辑放在服务中。

控制器和服务是插件服务器中处理请求和业务逻辑的两个构建模块。它们在职责分离上协同工作：控制器负责 HTTP 层，服务负责字段层：

🌐 Controllers and services are the 2 building blocks that handle request processing and business logic in a plugin server. They work together in a clear separation of concerns: controllers own the HTTP layer, services own the domain layer:

| 目标 | 使用 |
| --- | --- |
| 接收 `ctx`，读取请求，设置响应 | [控制器](#controllers) |
| 查询数据库或应用业务规则 | [服务](#services) |
| 在多个控制器或生命周期钩子中重用逻辑 | [服务](#services) |
| 作为请求的一部分调用外部 API | [服务](#services) |

:::prerequisites

在深入了解本页的概念之前，请确保你已经：

🌐 Before diving deeper into the concepts on this page, please ensure you have:

- [创建了一个 Strapi 插件](/cms/plugins-development/create-a-plugin)，
- 已阅读并理解了[服务器 API](/cms/plugins-development/server-api)的基础知识

:::

## 控制器 {#controllers}

🌐 Controllers

控制器是一个包含动作方法的对象，每个方法对应一个路由处理程序。控制器接收包含请求和响应的 Koa 上下文对象（`ctx`），调用相应的服务，并为响应设置 `ctx.body` 或 `ctx.status`。

🌐 A controller is an object of action methods, each corresponding to a route handler. Controllers receive a Koa context object (`ctx`) containing the request and response, call the appropriate service, and set `ctx.body` or `ctx.status` for the response.

### 声明 {#declaration}

🌐 Declaration

控制器可以作为接收 `{ strapi }` 的工厂函数导出，也可以作为普通对象导出。工厂函数模式是推荐的依赖注入方式，并且与大多数文档示例保持一致。

🌐 Controllers can be exported either as a factory function receiving `{ strapi }` or as a plain object. The factory function pattern is the recommended approach for dependency injection and consistency with most documentation examples.

在运行时，Strapi 支持导出，并通过使用 `{ strapi }` 调用它们来解析函数导出。

🌐 At runtime, Strapi supports both exports and resolves function exports by calling them with `{ strapi }`.

`controllers/index.js|ts` 中使用的导出键必须与路由定义中使用的处理程序名称匹配。

🌐 The export key used in `controllers/index.js|ts` must match the handler name used in route definitions.

```js title="/src/plugins/my-plugin/server/src/controllers/index.js"
'use strict';

const article = require('./article');

module.exports = {
  article,
};
```

```js title="/src/plugins/my-plugin/server/src/controllers/article.js"
'use strict';

module.exports = ({ strapi }) => ({
  async find(ctx) {
    const articles = await strapi
      .plugin('my-plugin')
      .service('article')
      .findAll();

    ctx.body = articles;
  },

  async findOne(ctx) {
    const { documentId } = ctx.params;
    const article = await strapi
      .plugin('my-plugin')
      .service('article')
      .findOne(documentId);

    if (!article) {
      return ctx.notFound('Article not found');
    }

    ctx.body = article;
  },

  async create(ctx) {
    const article = await strapi
      .plugin('my-plugin')
      .service('article')
      .create(ctx.request.body);

    ctx.status = 201;
    ctx.body = article;
  },
});
```
```ts title="/src/plugins/my-plugin/server/src/controllers/index.ts"

  article,
};
```

```ts title="/src/plugins/my-plugin/server/src/controllers/article.ts"

interface ArticleService {
  findAll(): Promise<unknown[]>;
  findOne(id: string): Promise<unknown>;
  create(data: unknown): Promise<unknown>;
}

  async find(ctx: any) {
    // Limitation: in @strapi/types, plugin services are currently typed as unknown.
    const articleService = strapi.plugin('my-plugin').service('article') as ArticleService;

    ctx.body = await articleService.findAll();
  },

  async findOne(ctx: any) {
    const { documentId } = ctx.params;
    const article = await (strapi.plugin('my-plugin').service('article') as ArticleService).findOne(documentId);

    if (!article) {
      return ctx.notFound('Article not found');
    }

    ctx.body = article;
  },

  async create(ctx: any) {
    const articleService = strapi.plugin('my-plugin').service('article') as ArticleService;

    ctx.status = 201;
    ctx.body = await articleService.create(ctx.request.body);
  },
});
```

### 消毒 {#sanitization}

🌐 Sanitization

当你的插件公开内容 API 路由时，在返回查询参数和输出数据之前，请对其进行清理。这可以防止泄露私有字段或绕过访问规则。

🌐 When your plugin exposes Content API routes, sanitize query parameters and output data before returning them. This prevents leaking private fields or bypassing access rules.

插件控制器是普通的工厂函数，并不像 Strapi 核心中那样继承 `createCoreController`（详情请参见 [后端自定义](/cms/backend-customization/controllers)）。这意味着 `this.sanitizeQuery` 和 `this.sanitizeOutput` 简写不可用。请直接使用 `strapi.contentAPI.sanitize`，并显式传入内容类型的架构：

🌐 Plugin controllers are plain factory functions and do not extend `createCoreController` like in the Strapi core (see [backend customization](/cms/backend-customization/controllers) for details). This means the `this.sanitizeQuery` and `this.sanitizeOutput` shorthands are not available. Use `strapi.contentAPI.sanitize` directly instead, passing the content-type schema explicitly:

```js title="/src/plugins/my-plugin/server/src/controllers/article.js"
module.exports = ({ strapi }) => ({
  async find(ctx) {
    // highlight-start
    const schema = strapi.contentType('plugin::my-plugin.article');

    const sanitizedQuery = await strapi.contentAPI.sanitize.query(
      ctx.query, schema, { auth: ctx.state.auth }
    );
    // highlight-end
    const articles = await strapi.plugin('my-plugin').service('article').findAll(sanitizedQuery);
    // highlight-next-line
    ctx.body = await strapi.contentAPI.sanitize.output(articles, schema, { auth: ctx.state.auth });
  },
});
```
```ts title="/src/plugins/my-plugin/server/src/controllers/article.ts"

  async find(ctx: any) {
    // highlight-start
    const schema = strapi.contentType('plugin::my-plugin.article');

    const sanitizedQuery = await strapi.contentAPI.sanitize.query(
      ctx.query, schema, { auth: ctx.state.auth }
    );
    // highlight-end
    const articles = await (strapi.plugin('my-plugin').service('article') as any).findAll(sanitizedQuery);
    // highlight-next-line
    ctx.body = await strapi.contentAPI.sanitize.output(articles, schema, { auth: ctx.state.auth });
  },
});
```

:::strapi Backend customization

有关完整的清理和验证参考，包括 `sanitizeInput`、`validateQuery` 和 `validateInput`，请参见 [Controllers](/cms/backend-customization/controllers#sanitize-validate-custom-controllers)。

🌐 For the full sanitization and validation reference, including `sanitizeInput`, `validateQuery`, and `validateInput`, see [Controllers](/cms/backend-customization/controllers#sanitize-validate-custom-controllers).

:::

## 服务 {#services}

🌐 Services

服务是一个工厂函数，它接收 `{ strapi }` 并返回一个具有命名方法的对象，或一个普通对象；像 [controllers](#declaration) 一样，Strapi 在运行时解析两者。服务包含由控制器、生命周期钩子或其他服务调用的业务逻辑。

🌐 A service is a factory function that receives `{ strapi }` and returns an object of named methods, or a plain object; like [controllers](#declaration), Strapi resolves both at runtime. Services hold business logic called from controllers, lifecycle hooks, or other services.

### 声明 {#declaration-1}

🌐 Declaration

```js title="/src/plugins/my-plugin/server/src/services/index.js"
'use strict';

const article = require('./article');

module.exports = {
  article,
};
```

```js title="/src/plugins/my-plugin/server/src/services/article.js"
'use strict';

module.exports = ({ strapi }) => ({
  async findAll(params = {}) {
    // highlight-next-line
    return strapi.documents('plugin::my-plugin.article').findMany(params);
  },

  async findOne(documentId) {
    return strapi.documents('plugin::my-plugin.article').findOne({
      documentId,
    });
  },

  async create(data) {
    return strapi.documents('plugin::my-plugin.article').create({ data });
  },

  async update(documentId, data) {
    return strapi.documents('plugin::my-plugin.article').update({
      documentId,
      data,
    });
  },

  async delete(documentId) {
    return strapi.documents('plugin::my-plugin.article').delete({
      documentId,
    });
  },
});
```
```ts title="/src/plugins/my-plugin/server/src/services/index.ts"

  article,
};
```

```ts title="/src/plugins/my-plugin/server/src/services/article.ts"

  async findAll(params: Record<string, unknown> = {}) {
    // highlight-next-line
    return strapi.documents('plugin::my-plugin.article').findMany(params);
  },

  async findOne(documentId: string) {
    return strapi.documents('plugin::my-plugin.article').findOne({
      documentId,
    });
  },

  async create(data: Record<string, unknown>) {
    return strapi.documents('plugin::my-plugin.article').create({ data });
  },

  async update(documentId: string, data: Record<string, unknown>) {
    return strapi.documents('plugin::my-plugin.article').update({
      documentId,
      data,
    });
  },

  async delete(documentId: string) {
    return strapi.documents('plugin::my-plugin.article').delete({
      documentId,
    });
  },
});
```

:::caution TypeScript service typing

`services` 在当前的 `ServerObject` TypeScript 接口 (`@strapi/types`) 中被类型化为 `unknown`。这意味着 `strapi.plugin('my-plugin').service('article')` 返回 `unknown`，并且调用带有类型安全的方法时需要进行类型转换。对于完全类型化的服务调用，请显式定义并导出服务类型，并在调用处进行类型转换。

:::

:::strapi Document Service API

服务通过[文档服务 API](/cms/api/document-service)与内容类型进行交互，该 API 记录了可用方法和参数的完整列表。

🌐 Services interact with content-types through the [Document Service API](/cms/api/document-service), which documents the full list of available methods and parameters.

:::

## 端到端示例 {#end-to-end-example}

🌐 End-to-end example

以下示例展示了针对一个简单文章资源，跨路由、控制器和服务的完整请求流程。

🌐 The following example shows the complete request flow across routes, a controller, and a service for a simple article resource.

```js title="/src/plugins/my-plugin/server/src/routes/index.js"
'use strict';

module.exports = {
  'content-api': {
    type: 'content-api',
    routes: [
      {
        method: 'GET',
        path: '/articles',
        // highlight-next-line
        handler: 'article.find', // maps to controllers/article.js → find()
        config: { auth: false },
      },
      {
        method: 'POST',
        path: '/articles',
        // highlight-next-line
        handler: 'article.create', // maps to controllers/article.js → create()
        config: { auth: false },
      },
    ],
  },
};
```

```js title="/src/plugins/my-plugin/server/src/controllers/article.js"
'use strict';

module.exports = ({ strapi }) => ({
  // highlight-next-line
  async find(ctx) {
    ctx.body = await strapi.plugin('my-plugin').service('article').findAll();
    // Note: sanitize query and output in production — see the Sanitization section above
  },

  // highlight-next-line
  async create(ctx) {
    const article = await strapi
      .plugin('my-plugin')
      .service('article')
      .create(ctx.request.body);
    ctx.status = 201;
    ctx.body = article;
  },
});
```

```js title="/src/plugins/my-plugin/server/src/services/article.js"
'use strict';

module.exports = ({ strapi }) => ({
  findAll() {
    return strapi.documents('plugin::my-plugin.article').findMany();
  },

  create(data) {
    return strapi.documents('plugin::my-plugin.article').create({ data });
  },
});
```
```ts title="/src/plugins/my-plugin/server/src/routes/index.ts"

  'content-api': {
    type: 'content-api' as const,
    routes: [
      {
        method: 'GET' as const,
        path: '/articles',
        // highlight-next-line
        handler: 'article.find', // maps to controllers/article.ts → find()
        config: { auth: false },
      },
      {
        method: 'POST' as const,
        path: '/articles',
        // highlight-next-line
        handler: 'article.create', // maps to controllers/article.ts → create()
        config: { auth: false },
      },
    ],
  },
};
```

```ts title="/src/plugins/my-plugin/server/src/controllers/article.ts"

interface ArticleService {
  findAll(): Promise<unknown[]>;
  create(data: unknown): Promise<unknown>;
}

  // highlight-next-line
  async find(ctx: any) {
    ctx.body = await (strapi.plugin('my-plugin').service('article') as ArticleService).findAll();
    // Note: sanitize query and output in production — see the Sanitization section above
  },

  // highlight-next-line
  async create(ctx: any) {
    const article = await (strapi.plugin('my-plugin').service('article') as ArticleService)
      .create(ctx.request.body);
    ctx.status = 201;
    ctx.body = article;
  },
});
```

```ts title="/src/plugins/my-plugin/server/src/services/article.ts"

  findAll() {
    // highlight-next-line
    return strapi.documents('plugin::my-plugin.article').findMany();
  },

  create(data: Record<string, unknown>) {
    return strapi.documents('plugin::my-plugin.article').create({ data });
  },
});
```

## 最佳实践 {#best-practices}

🌐 Best practices

- **保持控制器简洁。** 控制器操作应该做三件事：接收 `ctx`，委托给服务，并设置响应。业务逻辑、数据库调用以及条件分支都应该放在服务中。
- **每个资源一个服务。** 按它们管理的资源组织服务（例如 `article`、`comment`、`settings`），而不是按操作类型组织。这可以让每个文件集中并且易于测试。
- **在服务中使用文档服务 API，而不是在控制器中。** 在控制器中直接调用 `strapi.documents(...)` 会绕过服务层，使逻辑难以复用。将所有文档服务调用放在服务中。
- **清理内容 API 响应。** 在公开内容 API 路由时，返回数据之前使用 `strapi.contentAPI.sanitize.output()`。跳过清理可能会将私有字段泄露给终端用户。管理员路由不受相同内容类型字段可见性规则的限制，但对它们进行清理也是无害的。
- **在 TypeScript 中显式地转换服务类型。** 在 `@strapi/types` 中 `services` 还未强类型化之前，在每个调用点将 `strapi.plugin('my-plugin').service('my-service')` 的返回值转换为服务接口。避免在整个代码库中使用 `any`。
