--- 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) => { 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(res => { res({ fieldEq: [], }); }); }, // 获得ctx对象 pageQueryOp: ctx => { return new Promise(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; @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 { @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; }; } } ```