---
description: 控制器(Controller)
globs: 
---
# 控制器(Controller)

为了实现`快速CRUD`与`自动路由`功能，框架基于[midwayjs controller](mdc:https:/www.midwayjs.org/docs/controller)，进行改造加强

完全继承[midwayjs controller](mdc:https:/www.midwayjs.org/docs/controller)的所有功能

`快速CRUD`与`自动路由`，大大提高编码效率与编码量

## 路由前缀

虽然可以手动设置，但是我们并不推荐，cool-admin 在全局权限校验包含一定的规则，

如果你没有很了解框架原理手动设置可能产生部分功能失效的问题

### 手动

`/api/other`

无通用 CRUD 设置方法

```ts
import { CoolController, BaseController } from "@cool-midway/core";

/**
 * 商品
 */
@CoolController("/api")
export class AppDemoGoodsController extends BaseController {
  /**
   * 其他接口
   */
  @Get("/other")
  async other() {
    return this.ok("hello, cool-admin!!!");
  }
}
```

含通用 CRUD 配置方法

```ts
import { Get } from "@midwayjs/core";
import { CoolController, BaseController } from "@cool-midway/core";
import { DemoGoodsEntity } from "../../entity/goods";

/**
 * 商品
 */
@CoolController({
  prefix: "/api",
  api: ["add", "delete", "update", "info", "list", "page"],
  entity: DemoGoodsEntity,
})
export class AppDemoGoodsController extends BaseController {
  /**
   * 其他接口
   */
  @Get("/other")
  async other() {
    return this.ok("hello, cool-admin!!!");
  }
}
```

### 自动

大多数情况下你无需指定自己的路由前缀，路由前缀将根据规则自动生成。

::: warning 警告
自动路由只影响模块中的 controller，其他位置建议不要使用
:::

`src/modules/demo/controller/app/goods.ts`

路由前缀是根据文件目录文件名按照[规则](mdc:src/guide/core/controller.html#规则)生成的，上述示例生成的路由为

`http://127.0.0.1:8001/app/demo/goods/xxx`

`xxx`代表具体的方法，如： `add`、`page`、`other`

```ts
import { Get } from "@midwayjs/core";
import { CoolController, BaseController } from "@cool-midway/core";
import { DemoGoodsEntity } from "../../entity/goods";

/**
 * 商品
 */
@CoolController({
  api: ["add", "delete", "update", "info", "list", "page"],
  entity: DemoGoodsEntity,
})
export class AppDemoGoodsController extends BaseController {
  /**
   * 其他接口
   */
  @Get("/other")
  async other() {
    return this.ok("hello, cool-admin!!!");
  }
}
```

### 规则

/controller 文件夹下的文件夹名或者文件名/模块文件夹名/方法名

#### 举例

```ts
 // 模块目录
 ├── modules
 │   └── demo(模块名)
 │   │    └── controller(api接口)
 │   │    │     └── app(参数校验)
 │   │    │     │     └── goods.ts(商品的controller)
 │   │    │     └── pay.ts(支付的controller)
 │   │    └── config.ts(必须，模块的配置)
 │   │    └── init.sql(可选，初始化该模块的sql)

```

生成的路由前缀为：
`/pay/demo/xxx(具体的方法)`与`/app/demo/goods/xxx(具体的方法)`

## CRUD

### 参数配置(CurdOption)

通用增删改查配置参数

| 参数               | 类型     | 说明                                                          | 备注 |
| ------------------ | -------- | ------------------------------------------------------------- | ---- |
| prefix             | String   | 手动设置路由前缀                                              |      |
| api                | Array    | 快速 API 接口可选`add` `delete` `update` `info` `list` `page` |      |
| serviceApis        | Array    | 将 service 方法注册为 api，通过 post 请求，直接调用 service 方法 |      |
| pageQueryOp        | QueryOp  | 分页查询设置                                                  |      |
| listQueryOp        | QueryOp  | 列表查询设置                                                  |      |
| insertParam        | Function | 请求插入参数，如新增的时候需要插入当前登录用户的 ID           |      |
| infoIgnoreProperty | Array    | `info`接口忽略返回的参数，如用户信息不想返回密码              |      |

### 查询配置(QueryOp)

分页查询与列表查询配置参数

| 参数              | 类型     | 说明                                                                                | 备注 |
| ----------------- | -------- | ----------------------------------------------------------------------------------- | ---- |
| keyWordLikeFields | Array    | 支持模糊查询的字段，如一个表中的`name`字段需要模糊查询                              |      |
| where             | Function | 其他查询条件                                                                        |      |
| select            | Array    | 选择查询字段                                                                        |      |
| fieldEq           | Array    | 筛选字段，字符串数组或者对象数组{ column: string, requestParam: string }，如 type=1 |      |
| fieldLike         | Array    | 模糊查询字段，字符串数组或者对象数组{ column: string, requestParam: string }，如 title |      |
| addOrderBy        | Object   | 排序                                                                                |      |
| join              | JoinOp[] | 关联表查询                                                                          |      |

### 关联表(JoinOp)

关联表查询配置参数

| 参数      | 类型   | 说明                                                               |
| --------- | ------ | ------------------------------------------------------------------ |
| entity    | Class  | 实体类，注意不能写表名                                                  |
| alias     | String | 别名，如果有关联表默认主表的别名为`a`, 其他表一般按 b、c、d...设置 |
| condition | String | 关联条件                                                           |
| type      | String | 内关联： 'innerJoin', 左关联：'leftJoin'                           |

### 完整示例

```ts
import { Get } from "@midwayjs/core";
import { CoolController, BaseController } from "@cool-midway/core";
import { BaseSysUserEntity } from "../../../base/entity/sys/user";
import { DemoAppGoodsEntity } from "../../entity/goods";

/**
 * 商品
 */
@CoolController({
  // 添加通用CRUD接口
  api: ["add", "delete", "update", "info", "list", "page"],
  // 8.x新增，将service方法注册为api，通过post请求，直接调用service方法
  serviceApis: [
    'use',
    {
      method: 'test1',
      summary: '不使用多租户', // 接口描述
    },
    'test2', // 也可以不设置summary
  ]
  // 设置表实体
  entity: DemoAppGoodsEntity,
  // 向表插入当前登录用户ID
  insertParam: (ctx) => {
    return {
      // 获得当前登录的后台用户ID，需要请求头传Authorization参数
      userId: ctx.admin.userId,
    };
  },
  // 操作crud之前做的事情 @cool-midway/core@3.2.14 新增
  before: (ctx) => {
    // 将前端的数据转JSON格式存数据库
    const { data } = ctx.request.body;
    ctx.request.body.data = JSON.stringify(data);
  },
  // info接口忽略价格字段
  infoIgnoreProperty: ["price"],
  // 分页查询配置
  pageQueryOp: {
    // 让title字段支持模糊查询
    keyWordLikeFields: ["title"],
    // 让type字段支持筛选，请求筛选字段与表字段一致是情况
    fieldEq: ["type"],
    // 多表关联，请求筛选字段与表字段不一致的情况
    fieldEq: [{ column: "a.id", requestParam: "id" }],
     // 让title字段支持模糊查询，请求参数为title
    fieldLike: ['a.title'],
    // 让title字段支持模糊查询，请求筛选字段与表字段不一致的情况
    fieldLike: [{ column: "a.title", requestParam: "title" }],
    // 指定返回字段，注意多表查询这个是必要的，否则会出现重复字段的问题
    select: ["a.*", "b.name", "a.name AS userName"],
    // 4.x置为过时 改用 join 关联表用户表
    leftJoin: [
      {
        entity: BaseSysUserEntity,
        alias: "b",
        condition: "a.userId = b.id",
      },
    ],
    // 4.x新增
    join: [
      {
        entity: BaseSysUserEntity,
        alias: "b",
        condition: "a.userId = b.id",
        type: "innerJoin",
      },
    ],
    // 4.x 新增 追加其他条件
    extend: async (find: SelectQueryBuilder<DemoGoodsEntity>) => {
      find.groupBy("a.id");
    },
    // 增加其他条件
    where: async (ctx) => {
      // 获取body参数
      const { a } = ctx.request.body;
      return [
        // 价格大于90
        ["a.price > :price", { price: 90.0 }],
        // 满足条件才会执行
        ["a.price > :price", { price: 90.0 }, "条件"],
        // 多个条件一起
        [
          "(a.price = :price or a.userId = :userId)",
          { price: 90.0, userId: ctx.admin.userId },
        ],
      ];
    },
    // 添加排序
    addOrderBy: {
      price: "desc",
    },
  },
})
export class DemoAppGoodsController extends BaseController {
  /**
   * 其他接口
   */
  @Get("/other")
  async other() {
    return this.ok("hello, cool-admin!!!");
  }
}
```

::: warning
如果是多表查询，必须设置 select 参数，否则会出现重复字段的错误，因为每个表都继承了 BaseEntity，至少都有 id、createTime、updateTime 三个相同的字段。
:::

通过这一波操作之后，我们的商品接口的功能已经很强大了，除了通用的 CRUD，我们的接口还支持多种方式的数据筛选

### 获得 ctx 对象

```ts
@CoolController(
  {
    api: ['add', 'delete', 'update', 'info', 'list', 'page'],
    entity: DemoAppGoodsEntity,
    // 获得ctx对象
    listQueryOp: ctx => {
      return new Promise<QueryOp>(res => {
        res({
          fieldEq: [],
        });
      });
    },
    // 获得ctx对象
    pageQueryOp: ctx => {
      return new Promise<QueryOp>(res => {
        res({
          fieldEq: [],
        });
      });
    },
  },
  {
    middleware: [],
  }
)
```

### 接口调用

`add` `delete` `update` `info` 等接口可以用法[参照快速开始](mdc:src/guide/quick.html#接口调用)

这里详细说明下`page` `list`两个接口的调用方式，这两个接口调用方式差不多，一个是分页一个是非分页。
以`page`接口为例

#### 分页

POST `/admin/demo/goods/page` 分页数据

**请求**
Url: http://127.0.0.1:8001/admin/demo/goods/page

Method: POST

#### Body

```json
{
  "keyWord": "商品标题", // 模糊搜索，搜索的字段对应keyWordLikeFields
  "type": 1, // 全等于筛选，对应fieldEq
  "page": 2, // 第几页
  "size": 1, // 每页返回个数
  "sort": "desc", // 排序方向
  "order": "id" // 排序字段
}
```

**返回**

```json
{
  "code": 1000,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 4,
        "createTime": "2021-03-12 16:23:46",
        "updateTime": "2021-03-12 16:23:46",
        "title": "这是一个商品2",
        "pic": "https://show.cool-admin.com/uploads/20210311/2e393000-8226-11eb-abcf-fd7ae6caeb70.png",
        "price": "99.00",
        "userId": 1,
        "type": 1,
        "name": "超级管理员"
      }
    ],
    "pagination": {
      "page": 2,
      "size": 1,
      "total": 4
    }
  }
}
```

### 服务注册成 Api

很多情况下，我们在`Controller`层并不想过多地操作，而是想直接调用`Service`层的方法，这个时候我们可以将`Service`层的方法注册成`Api`，那么你的某个`Service`方法就变成了`Api`。

#### 示例：

在 Controller 中

```ts
import { CoolController, BaseController } from "@cool-midway/core";
import { DemoGoodsEntity } from "../../entity/goods";
import { DemoTenantService } from "../../service/tenant";

/**
 * 示例
 */
@CoolController({
  serviceApis: [
    "use",
    {
      method: "test1",
      summary: "不使用多租户", // 接口描述
    },
    "test2", // 也可以不设置summary
  ],
  entity: DemoGoodsEntity,
  service: DemoXxxService,
})
export class AdminDemoTenantController extends BaseController {}
```

在 Service 中

```ts
/**
 * 示例服务
 */
@Provide()
export class DemoXxxService extends BaseService {
  /**
   * 示例方法1
   */
  async test1(params) {
    console.log(params);
    return "test1";
  }

  /**
   * 示例方法2
   */
  async test2() {
    return "test2";
  }
}
```

::: warning 注意
`serviceApis` 注册为`Api`的请求方法是`POST`，所以`Service`层的方法参数需要通过`body`传递
:::

### 重写 CRUD 实现

在实际开发过程中，除了这些通用的接口可以满足大部分的需求，但是也有一些特殊的需求无法满足用户要求，这个时候也可以重写`add` `delete` `update` `info` `list` `page` 的实现

#### 编写 service

在模块新建 service 文件夹(名称非强制性)，再新建一个`service`实现，继承框架的`BaseService`

```ts
import { Inject, Provide } from "@midwayjs/core";
import { BaseService } from "@cool-midway/core";
import { InjectEntityModel } from "@midwayjs/orm";
import { Repository } from "typeorm";
import { BaseSysMenuEntity } from "../../entity/sys/menu";
import * as _ from "lodash";
import { BaseSysPermsService } from "./perms";

/**
 * 菜单
 */
@Provide()
export class BaseSysMenuService extends BaseService {
  @Inject()
  ctx;

  @InjectEntityModel(BaseSysMenuEntity)
  baseSysMenuEntity: Repository<BaseSysMenuEntity>;

  @Inject()
  baseSysPermsService: BaseSysPermsService;

  /**
   * 重写list实现
   */
  async list() {
    const menus = await this.getMenus(
      this.ctx.admin.roleIds,
      this.ctx.admin.username === "admin"
    );
    if (!_.isEmpty(menus)) {
      menus.forEach((e) => {
        const parentMenu = menus.filter((m) => {
          e.parentId = parseInt(e.parentId);
          if (e.parentId == m.id) {
            return m.name;
          }
        });
        if (!_.isEmpty(parentMenu)) {
          e.parentName = parentMenu[0].name;
        }
      });
    }
    return menus;
  }
}
```

#### 设置服务实现

`CoolController`设置自己的服务实现

```ts
import { Inject } from "@midwayjs/core";
import { CoolController, BaseController } from "@cool-midway/core";
import { BaseSysMenuEntity } from "../../../entity/sys/menu";
import { BaseSysMenuService } from "../../../service/sys/menu";

/**
 * 菜单
 */
@CoolController({
  api: ["add", "delete", "update", "info", "list", "page"],
  entity: BaseSysMenuEntity,
  service: BaseSysMenuService,
})
export class BaseSysMenuController extends BaseController {
  @Inject()
  baseSysMenuService: BaseSysMenuService;
}
```

## 路由标签

我们经常有这样的需求：给某个请求地址打上标记，如忽略 token，忽略签名等。

```ts
import { Get, Inject } from "@midwayjs/core";
import {
  CoolController,
  BaseController,
  CoolUrlTag,
  TagTypes,
  CoolUrlTagData,
} from "@cool-midway/core";

/**
 * 测试给URL打标签
 */
@CoolController({
  api: [],
  entity: "",
  pageQueryOp: () => {},
})
// add 接口忽略token
@CoolUrlTag({
  key: TagTypes.IGNORE_TOKEN,
  value: ["add"],
})
export class DemoAppTagController extends BaseController {
  @Inject()
  tag: CoolUrlTagData;

  /**
   * 获得标签数据， 如可以标记忽略token的url，然后在中间件判断
   * @returns
   */
  // 这是6.x支持的，可以直接标记这个接口忽略token，更加灵活优雅，但是记得配合@CoolUrlTag()一起使用，也就是Controller上要有这个注解，@CoolTag才会生效
  @CoolTag(TagTypes.IGNORE_TOKEN)
  @Get("/data")
  async data() {
    return this.ok(this.tag.byKey(TagTypes.IGNORE_TOKEN));
  }
}
```

#### 中间件

```ts
import { CoolUrlTagData, TagTypes } from "@cool-midway/core";
import { IMiddleware } from "@midwayjs/core";
import { Inject, Middleware } from "@midwayjs/core";
import { NextFunction, Context } from "@midwayjs/koa";

@Middleware()
export class DemoMiddleware implements IMiddleware<Context, NextFunction> {
  @Inject()
  tag: CoolUrlTagData;

  resolve() {
    return async (ctx: Context, next: NextFunction) => {
      const urls = this.tag.byKey(TagTypes.IGNORE_TOKEN);
      console.log("忽略token的URL数组", urls);
      // 这里可以拿到下一个中间件或者控制器的返回值
      const result = await next();
      // 控制器之后执行的逻辑
      // 返回给上一个中间件的结果
      return result;
    };
  }
}
```

