diff --git a/.cursor/rules/controller.mdc b/.cursor/rules/controller.mdc new file mode 100644 index 0000000..3e5dcc0 --- /dev/null +++ b/.cursor/rules/controller.mdc @@ -0,0 +1,584 @@ +--- +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; + }; + } +} +``` +