2025-02-09 23:50:09 +08:00

585 lines
16 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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;
};
}
}
```