Skip to content

控制器(Controller)

为了实现快速CRUD自动路由功能,框架基于midwayjs controller,进行改造加强

完全继承midwayjs 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!!!");
  }
}

自动

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

警告

自动路由只影响模块中的 controller,其他位置建议不要使用

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

路由前缀是根据文件目录文件名按照规则生成的,上述示例生成的路由为

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

xxx代表具体的方法,如: addpageother

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)

通用增删改查配置参数

参数类型说明备注
prefixString手动设置路由前缀
apiArray快速 API 接口可选add delete update info list page
serviceApisArray将 service 方法注册为 api,通过 post 请求,直接调用 service 方法
pageQueryOpQueryOp分页查询设置
listQueryOpQueryOp列表查询设置
insertParamFunction请求插入参数,如新增的时候需要插入当前登录用户的 ID
infoIgnorePropertyArrayinfo接口忽略返回的参数,如用户信息不想返回密码

查询配置(QueryOp)

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

参数类型说明备注
keyWordLikeFieldsArray支持模糊查询的字段,如一个表中的name字段需要模糊查询
whereFunction其他查询条件
selectArray选择查询字段
fieldEqArray筛选字段,字符串数组或者对象数组{ column: string, requestParam: string },如 type=1
fieldLikeArray模糊查询字段,字符串数组或者对象数组{ column: string, requestParam: string },如 title
addOrderByObject排序
joinJoinOp[]关联表查询

关联表(JoinOp)

关联表查询配置参数

参数类型说明
entityClass实体类
aliasString别名,如果有关联表默认主表的别名为a, 其他表一般按 b、c、d...设置
conditionString关联条件
typeString内关联: '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字段支持模糊查询,请求参数为keyWord
    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 等接口可以用法参照快速开始

这里详细说明下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";
  }
}

注意

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/typeorm";
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;
    };
  }
}