From 5d9708015a1dce2c35f9009a802f1a32a9cedb9c Mon Sep 17 00:00:00 2001 From: xiaopeng Date: Sun, 9 Feb 2025 21:02:47 +0800 Subject: [PATCH] add cursor rules --- .cursor/rules/authority.mdc | 242 +++++++++++++++ .cursor/rules/cache.mdc | 174 +++++++++++ .cursor/rules/controller.mdc | 578 +++++++++++++++++++++++++++++++++++ .cursor/rules/db.mdc | 460 ++++++++++++++++++++++++++++ .cursor/rules/event.mdc | 102 +++++++ .cursor/rules/exception.mdc | 22 ++ .cursor/rules/module.mdc | 185 +++++++++++ .cursor/rules/service.mdc | 320 +++++++++++++++++++ .cursor/rules/socket.mdc | 127 ++++++++ .cursor/rules/task.mdc | 382 +++++++++++++++++++++++ .cursor/rules/tenant.mdc | 178 +++++++++++ .cursorrules | 8 +- 12 files changed, 2776 insertions(+), 2 deletions(-) create mode 100644 .cursor/rules/authority.mdc create mode 100644 .cursor/rules/cache.mdc create mode 100644 .cursor/rules/controller.mdc create mode 100644 .cursor/rules/db.mdc create mode 100644 .cursor/rules/event.mdc create mode 100644 .cursor/rules/exception.mdc create mode 100644 .cursor/rules/module.mdc create mode 100644 .cursor/rules/service.mdc create mode 100644 .cursor/rules/socket.mdc create mode 100644 .cursor/rules/task.mdc create mode 100644 .cursor/rules/tenant.mdc diff --git a/.cursor/rules/authority.mdc b/.cursor/rules/authority.mdc new file mode 100644 index 0000000..11782fd --- /dev/null +++ b/.cursor/rules/authority.mdc @@ -0,0 +1,242 @@ +--- +description: 权限管理(Authority) +globs: +--- +# 权限管理(Authority) + +cool-admin 采用是是一种无状态的权限校验方式。[jwt](mdc:https:/jwt.io/introduction), 通俗地讲他就是把用户的一些信息经过处理生成一段加密的字符串,后端解密到信息进行校验。而且这个信息是带有时效的。 + +cool-admin 默认约定每个模块下的 `controller/admin`为后台编写接口,`controller/app`编写对外如 app、小程序的接口。 + +- 框架会对路由前缀 `/admin/**` 开头的接口进行权限校验,校验逻辑写在`base`模块下的`middleware/authority.ts`中间件 +- 框架会对路由前缀 `/app/**` 开头的接口进行权限校验,校验逻辑写在`user`模块下的`middleware/app.ts`中间件 + +::: tip +也就是说模块`controller/admin`与`controller/app`是需要进行 token 校验的,如果你不想 token 校验有两种方式: + +- 使用路由标签的形式,忽略 token 校验,详细查看[路由标签](mdc:src/guide/core/controller.html#路由标签); + +- 新建其他的文件夹比如:`controller/open`; + +这样就不会提示登录失效~ +::: + +## 登录 + +查询校验用户信息,然后将用户信息用 jwt 的方式加密保存返回给客户端。 + +`src/app/modules/base/service/sys/login.ts` + +```ts +/** + * 登录 + * @param login + */ + async login(login: LoginDTO) { + const { username, captchaId, verifyCode, password } = login; + // 校验验证码 + const checkV = await this.captchaCheck(captchaId, verifyCode); + if (checkV) { + const user = await this.baseSysUserEntity.findOne({ username }); + // 校验用户 + if (user) { + // 校验用户状态及密码 + if (user.status === 0 || user.password !== md5(password)) { + throw new CoolCommException('账户或密码不正确~'); + } + } else { + throw new CoolCommException('账户或密码不正确~'); + } + // 校验角色 + const roleIds = await this.baseSysRoleService.getByUser(user.id); + if (_.isEmpty(roleIds)) { + throw new CoolCommException('该用户未设置任何角色,无法登录~'); + } + + // 生成token + const { expire, refreshExpire } = this.coolConfig.jwt.token; + const result = { + expire, + token: await this.generateToken(user, roleIds, expire), + refreshExpire, + refreshToken: await this.generateToken( + user, + roleIds, + refreshExpire, + true + ), + }; + + // 将用户相关信息保存到缓存 + const perms = await this.baseSysMenuService.getPerms(roleIds); + const departments = await this.baseSysDepartmentService.getByRoleIds( + roleIds, + user.username === 'admin' + ); + await this.coolCache.set( + `admin:department:${user.id}`, + JSON.stringify(departments) + ); + await this.coolCache.set(`admin:perms:${user.id}`, JSON.stringify(perms)); + await this.coolCache.set(`admin:token:${user.id}`, result.token); + await this.coolCache.set(`admin:token:refresh:${user.id}`, result.token); + + return result; + } else { + throw new CoolCommException('验证码不正确'); + } + } +``` + +## 权限配置 + +admin 用户拥有所有的权限,无需配置,但是对于其他只拥有部分权限的用户,我们得选择他们的权限,在这之前我们得先录入我们的系统有哪些权限是可以配置的 + +可以登录后台管理系统,`系统管理/权限管理/菜单列表` + +![authority](mdc:admin/node/authority.png) + +## 选择权限 + +新建一个角色,就可以为这个角色配置对应的权限,用户管理可以选择对应的角色,那么该用户就有对应的权限,一个用户可以选择多个角色 + +![authority](mdc:admin/node/authority-role.png) + +## 全局校验 + +通过一个全局的中间件,我们在全局统一处理,这样就无需在每个 controller 处理,显得有点多余。 + +`src/app/modules/base/middleware/authority.ts` + +```ts +import { App, Config, Middleware } from "@midwayjs/core"; +import * as _ from "lodash"; +import { RESCODE } from "@cool-midway/core"; +import * as jwt from "jsonwebtoken"; +import { NextFunction, Context } from "@midwayjs/koa"; +import { IMiddleware, IMidwayApplication } from "@midwayjs/core"; + +/** + * 权限校验 + */ +@Middleware() +export class BaseAuthorityMiddleware + implements IMiddleware +{ + @Config("koa.globalPrefix") + prefix; + + @Config("module.base") + jwtConfig; + + coolCache; + + @App() + app: IMidwayApplication; + + resolve() { + return async (ctx: Context, next: NextFunction) => { + let statusCode = 200; + let { url } = ctx; + url = url.replace(this.prefix, ""); + const token = ctx.get("Authorization"); + const adminUrl = "/admin/"; + // 路由地址为 admin前缀的 需要权限校验 + if (_.startsWith(url, adminUrl)) { + try { + ctx.admin = jwt.verify(token, this.jwtConfig.jwt.secret); + } catch (err) {} + // 不需要登录 无需权限校验 + if (new RegExp(`^${adminUrl}?.*/open/`).test(url)) { + await next(); + return; + } + if (ctx.admin) { + // 超管拥有所有权限 + if (ctx.admin.username == "admin" && !ctx.admin.isRefresh) { + await next(); + return; + } + // 要登录每个人都有权限的接口 + if (new RegExp(`^${adminUrl}?.*/comm/`).test(url)) { + await next(); + return; + } + // 如果传的token是refreshToken则校验失败 + if (ctx.admin.isRefresh) { + ctx.status = 401; + ctx.body = { + code: RESCODE.COMMFAIL, + message: "登录失效~", + }; + return; + } + // 需要动态获得缓存 + this.coolCache = await ctx.requestContext.getAsync("cool:cache"); + // 判断密码版本是否正确 + const passwordV = await this.coolCache.get( + `admin:passwordVersion:${ctx.admin.userId}` + ); + if (passwordV != ctx.admin.passwordVersion) { + ctx.status = 401; + ctx.body = { + code: RESCODE.COMMFAIL, + message: "登录失效~", + }; + return; + } + const rToken = await this.coolCache.get( + `admin:token:${ctx.admin.userId}` + ); + if (!rToken) { + ctx.status = 401; + ctx.body = { + code: RESCODE.COMMFAIL, + message: "登录失效或无权限访问~", + }; + return; + } + if (rToken !== token && this.jwtConfig.sso) { + statusCode = 401; + } else { + let perms = await this.coolCache.get( + `admin:perms:${ctx.admin.userId}` + ); + if (!_.isEmpty(perms)) { + perms = JSON.parse(perms).map((e) => { + return e.replace(/:/g, "/"); + }); + if (!perms.includes(url.split("?")[0].replace("/admin/", ""))) { + statusCode = 403; + } + } else { + statusCode = 403; + } + } + } else { + statusCode = 401; + } + if (statusCode > 200) { + ctx.status = statusCode; + ctx.body = { + code: RESCODE.COMMFAIL, + message: "登录失效或无权限访问~", + }; + return; + } + } + await next(); + }; + } +} +``` + +## 令牌续期 + +jwt 加密完的字符串是有时效的,系统默认时效时间为 2 个小时。这期间就需要续期令牌才可以继续操作。 + +框架登录设置了一个 refreshToken,默认过期时间为 30 天。可以使用这个去换取新的 token,这时候又可以延长 2 个小时。 + +## 其他权限 + +你可以单独编写一个中间间来控制其他权限,如 app、小程序及其他对外接口,但是可以参考后台管理系统权限过滤、token 生成校验的实现方式 diff --git a/.cursor/rules/cache.mdc b/.cursor/rules/cache.mdc new file mode 100644 index 0000000..9922293 --- /dev/null +++ b/.cursor/rules/cache.mdc @@ -0,0 +1,174 @@ +--- +description: 缓存(Cache) +globs: +--- +# 缓存 + +为了方便开发者进行缓存操作的组件,它有利于改善项目的性能。它为我们提供了一个数据中心以便进行高效的数据访问。 + +::: + +## 使用 + +```ts +import { InjectClient, Provide } from '@midwayjs/core'; +import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager'; + +@Provide() +export class UserService { + + @InjectClient(CachingFactory, 'default') + cache: MidwayCache; + + async invoke(name: string, value: string) { + // 设置缓存 + await this.cache.set(name, value); + // 获取缓存 + const data = await this.cache.get(name); + // ... + } +} +``` + +## 换成 Redis (v7.1 版本) + +安装依赖,具体可以查看@midwayjs cache + +```bash +pnpm i cache-manager-ioredis-yet --save +``` + +`src/config/config.default.ts` + +```ts +import { CoolFileConfig, MODETYPE } from "@cool-midway/file"; +import { MidwayConfig } from "@midwayjs/core"; +// redis缓存 +import { redisStore } from "cache-manager-ioredis-yet"; + +export default { + // Redis缓存 + cacheManager: { + clients: { + default: { + store: redisStore, + options: { + port: 6379, + host: "127.0.0.1", + password: "", + ttl: 0, + db: 0, + }, + }, + }, + }, +} as unknown as MidwayConfig; +``` + +## 换成 Redis (以往版本) + +```bash +pnpm i cache-manager-ioredis --save +``` + +`src/config/config.default.ts` + +```ts +import { CoolFileConfig, MODETYPE } from "@cool-midway/file"; +import { MidwayConfig } from "@midwayjs/core"; +// redis缓存 +import * as redisStore from "cache-manager-ioredis"; + +export default { + // Redis缓存 + cache: { + store: redisStore, + options: { + port: 6379, + host: "127.0.0.1", + password: "", + db: 0, + keyPrefix: "cool:", + ttl: null, + }, + }, +} as unknown as MidwayConfig; +``` + +## 使用 + +`src/modules/demo/controller/open/cache.ts` + +```ts +import { DemoCacheService } from "../../service/cache"; +import { Inject, Post, Provide, Get, InjectClient } from "@midwayjs/core"; +import { CoolController, BaseController } from "@cool-midway/core"; +import { CachingFactory, MidwayCache } from "@midwayjs/cache-manager"; + +/** + * 缓存 + */ +@Provide() +@CoolController() +export class AppDemoCacheController extends BaseController { + @InjectClient(CachingFactory, "default") + midwayCache: MidwayCache; + + @Inject() + demoCacheService: DemoCacheService; + + /** + * 设置缓存 + * @returns + */ + @Post("/set") + async set() { + await this.midwayCache.set("a", 1); + // 缓存10秒 + await this.midwayCache.set("a", 1, 10 * 1000); + return this.ok(await this.midwayCache.get("a")); + } + + /** + * 获得缓存 + * @returns + */ + @Get("/get") + async get() { + return this.ok(await this.demoCacheService.get()); + } +} +``` + +## 方法缓存 + +有些业务场景,我们并不希望每次请求接口都需要操作数据库,如:今日推荐、上个月排行榜等,数据存储在 redis + +框架提供了 `@CoolCache` 方法装饰器,方法设置缓存,让代码更优雅 + +`src/modules/demo/service/cache.ts` + +```ts +import { Provide } from "@midwayjs/core"; +import { CoolCache } from "@cool-midway/core"; + +/** + * 缓存 + */ +@Provide() +export class DemoCacheService { + // 数据缓存5秒 + @CoolCache(5000) + async get() { + console.log("执行方法"); + return { + a: 1, + b: 2, + }; + } +} +``` + +::: warning +service 主要是处理业务逻辑,`@CoolCache`应该要在 service 中使用,不要在 controller 等其他位置使用 +::: diff --git a/.cursor/rules/controller.mdc b/.cursor/rules/controller.mdc new file mode 100644 index 0000000..9df4dfc --- /dev/null +++ b/.cursor/rules/controller.mdc @@ -0,0 +1,578 @@ +--- +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` | | +| 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 | | +| 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" }], + // 指定返回字段,注意多表查询这个是必要的,否则会出现重复字段的问题 + 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; + }; + } +} +``` + diff --git a/.cursor/rules/db.mdc b/.cursor/rules/db.mdc new file mode 100644 index 0000000..4a8923d --- /dev/null +++ b/.cursor/rules/db.mdc @@ -0,0 +1,460 @@ +--- +description: 数据库(db) +globs: +--- +# 数据库(db) + +数据库使用的是`typeorm`库 + +中文文档:](httpsom) + +官方文档:[https://typeorm.io](https:/据库文档:[https://www.midwayjs.org/docs/extensions/orm](https://www.midwayjs.org/docs/extensions/orm) + +## 数据库配置 + +支持`Mysql`、`PostgreSQL`、`Sqlite`三种数据库 + +#### Mysql + +`src/config/config.local.ts` + +```ts +import { CoolConfig } from "@cool-midway/core"; +import { MidwayConfig } from "@midwayjs/core"; + +export default { + typeorm: { + dataSource: { + default: { + type: "mysql", + host: "127.0.0.1", + port: 3306, + username: "root", + password: "123456", + database: "cool", + // 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失 + synchronize: true, + // 打印日志 + logging: false, + // 字符集 + charset: "utf8mb4", + // 是否开启缓存 + cache: true, + // 实体路径 + entities: ["**/modules/*/entity"], + }, + }, + }, +} as MidwayConfig; +``` + +#### PostgreSQL + +需要先安装驱动 + +```shell +npm install pg --save +``` + +`src/config/config.local.ts` + +```ts +import { CoolConfig } from "@cool-midway/core"; +import { MidwayConfig } from "@midwayjs/core"; + +export default { + typeorm: { + dataSource: { + default: { + type: "postgres", + host: "127.0.0.1", + port: 5432, + username: "postgres", + password: "123456", + database: "cool", + // 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失 + synchronize: true, + // 打印日志 + logging: false, + // 字符集 + charset: "utf8mb4", + // 是否开启缓存 + cache: true, + // 实体路径 + entities: ["**/modules/*/entity"], + }, + }, + }, +} as MidwayConfig; +``` + +#### Sqlite + +需要先安装驱动 + +```shell +npm install sqlite3 --save +``` + +`src/config/config.local.ts` + +```ts +import { CoolConfig } from "@cool-midway/core"; +import { MidwayConfig } from "@midwayjs/core"; +import * as path from "path"; + +export default { + typeorm: { + dataSource: { + default: { + type: "sqlite", + // 数据库文件地址 + database: path.join(__dirname, "../../cool.sqlite"), + // 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失 + synchronize: true, + // 打印日志 + logging: false, + // 实体路径 + entities: ["**/modules/*/entity"], + }, + }, + }, +} as MidwayConfig; +``` + +## 事务示例 + +`cool-admin`封装了自己事务,让代码更简洁 + +#### 示例 + +```ts +import { Inject, Provide } from "@midwayjs/core"; +import { BaseService, CoolTransaction } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/orm"; +import { Repository, QueryRunner } from "typeorm"; +import { DemoAppGoodsEntity } from "../entity/goods"; + +/** + * 商品 + */ +@Provide() +export class DemoGoodsService extends BaseService { + @InjectEntityModel(DemoAppGoodsEntity) + demoAppGoodsEntity: Repository; + + /** + * 事务 + * @param params + * @param queryRunner 无需调用者传参, 自动注入,最后一个参数 + */ + @CoolTransaction({ isolation: "SERIALIZABLE" }) + async testTransaction(params: any, queryRunner?: QueryRunner) { + await queryRunner.manager.insert(DemoAppGoodsEntity, { + title: "这是个商品", + pic: "商品图", + price: 99.0, + type: 1, + }); + } +} +``` + +::: tip +`CoolTransaction`中已经做了异常捕获,所以方法内部无需捕获异常,必须使用`queryRunner`做数据库操作, +而且不能是异步的,否则事务无效, +`queryRunner`会注入到被注解的方法最后一个参数中, 无需调用者传参 +::: + +## 字段 + +BaseEntity 是实体基类,所有实体类都需要继承它。 + +- v8.x 之前位于`@cool-midway/core`包中 +- v8.x 之后位于`src/modules/base/entity/base.ts` + +```typescript +import { Index, PrimaryGeneratedColumn, Column } from "typeorm"; +import * as moment from "moment"; +import { CoolBaseEntity } from "@cool-midway/core"; + +const transformer = { + to(value) { + return value + ? moment(value).format("YYYY-MM-DD HH:mm:ss") + : moment().format("YYYY-MM-DD HH:mm:ss"); + }, + from(value) { + return value; + }, +}; + +/** + * 实体基类 + */ +export abstract class BaseEntity extends CoolBaseEntity { + // 默认自增 + @PrimaryGeneratedColumn("increment", { + comment: "ID", + }) + id: number; + + @Index() + @Column({ + comment: "创建时间", + type: "varchar", + transformer, + }) + createTime: Date; + + @Index() + @Column({ + comment: "更新时间", + type: "varchar", + transformer, + }) + updateTime: Date; + + @Index() + @Column({ comment: "租户ID", nullable: true }) + tenantId: number; +} +``` + +```typescript +// v8.x 之前 +import { BaseEntity } from "@cool-midway/core"; +// v8.x 之后 +import { BaseEntity } from "../../base/entity/base"; +import { Column, Entity, Index } from "typeorm"; + +/** + * demo模块-用户信息 + */ +// 表名必须包含模块固定格式:模块_, +@Entity("demo_user_info") +// DemoUserInfoEntity是模块+表名+Entity +export class DemoUserInfoEntity extends BaseEntity { + @Index() + @Column({ comment: "手机号", length: 11 }) + phone: string; + + @Index({ unique: true }) + @Column({ comment: "身份证", length: 50 }) + idCard: string; + + // 生日只需要精确到哪一天,所以type:'date',如果需要精确到时分秒,应为'datetime' + @Column({ comment: "生日", type: "date" }) + birthday: Date; + + @Column({ comment: "状态 0-禁用 1-启用", default: 1 }) + status: number; + + @Column({ + comment: "分类 0-普通 1-会员 2-超级会员", + default: 0, + type: "tinyint", + }) + type: number; + + // 由于labels的类型是一个数组,所以Column中的type类型必须得是'json' + @Column({ comment: "标签", nullable: true, type: "json" }) + labels: string[]; + + @Column({ + comment: "余额", + type: "decimal", + precision: 5, + scale: 2, + }) + balance: number; + + @Column({ comment: "备注", nullable: true }) + remark: string; + + @Column({ comment: "简介", type: "text", nullable: true }) + summary: string; +} +``` + +## 虚拟字段 + +虚拟字段是指数据库中没有实际存储的字段,而是通过其他字段计算得到的字段,这种字段在查询时可以直接使用,但是不能进行更新操作 + +```ts +import { BaseEntity } from "@cool-midway/core"; +import { Column, Entity, Index } from "typeorm"; + +/** + * 数据实体 + */ +@Entity("xxx_xxx") +export class XxxEntity extends BaseEntity { + @Index() + @Column({ + type: "varchar", + length: 7, + asExpression: "DATE_FORMAT(createTime, '%Y-%m')", + generatedType: "VIRTUAL", + comment: "月份", + }) + month: string; + + @Index() + @Column({ + type: "varchar", + length: 4, + asExpression: "DATE_FORMAT(createTime, '%Y')", + generatedType: "VIRTUAL", + comment: "年份", + }) + year: string; + + @Index() + @Column({ + type: "varchar", + length: 10, + asExpression: "DATE_FORMAT(createTime, '%Y-%m-%d')", + generatedType: "VIRTUAL", + comment: "日期", + }) + date: string; + + @Column({ comment: "退款", type: "json", nullable: true }) + refund: { + // 退款单号 + orderNum: string; + // 金额 + amount: number; + // 实际退款金额 + realAmount: number; + // 状态 0-申请中 1-已退款 2-拒绝 + status: number; + // 申请时间 + applyTime: Date; + // 退款时间 + time: Date; + // 退款原因 + reason: string; + // 拒绝原因 + refuseReason: string; + }; + + // 将退款状态提取出来,方便查询 + @Index() + @Column({ + asExpression: "JSON_EXTRACT(refund, '$.status')", + generatedType: "VIRTUAL", + comment: "退款状态", + nullable: true, + }) + refundStatus: number; +} +``` + +## 不使用外键 + +typeorm 有很多 OneToMany, ManyToOne, ManyToMany 等关联关系,这种都会生成外键,但是在实际生产开发中,不推荐使用外键: + +- 性能影响:外键会在插入、更新或删除操作时增加额外的开销。数据库需要检查外键约束是否满足,这可能会降低数据库的性能,特别是在大规模数据操作时更为明显。 + +- 复杂性增加:随着系统的发展,数据库结构可能会变得越来越复杂。外键约束增加了数据库结构的复杂性,使得数据库的维护和理解变得更加困难。 + +- 可扩展性问题:在分布式数据库系统中,数据可能分布在不同的服务器上。外键约束会影响数据的分片和分布,限制了数据库的可扩展性。 + +- 迁移和备份困难:带有外键约束的数据库迁移或备份可能会变得更加复杂。迁移时需要保证数据的完整性和约束的一致性,这可能会增加迁移的难度和时间。 + +- 业务逻辑耦合:过多依赖数据库的外键约束可能会导致业务逻辑过度耦合于数据库层。这可能会限制应用程序的灵活性和后期的业务逻辑调整。 + +- 并发操作问题:在高并发的场景下,外键约束可能会导致锁的竞争,增加死锁的风险,影响系统的稳定性和响应速度。 + +尽管外键提供了数据完整性保障,但在某些场景下,特别是在高性能和高可扩展性要求的系统中,可能会选择在应用层实现相应的完整性检查和约束逻辑,以避免上述问题。这需要在设计系统时根据实际需求和环境来权衡利弊,做出合适的决策。 + +## 多表关联查询 + +cool-admin 有三种方式的联表查询: + +1、controller 上配置 + +特别注意要配置 select, 不然会报重复字段错误 + +```ts +@CoolController({ + // 添加通用CRUD接口 + api: ['add', 'delete', 'update', 'info', 'list', 'page'], + // 设置表实体 + entity: DemoAppGoodsEntity, + // 分页查询配置 + pageQueryOp: { + // 指定返回字段,注意多表查询这个是必要的,否则会出现重复字段的问题 + select: ['a.*', 'b.name', 'a.name AS userName'], + // 联表查询 + join: [ + { + entity: BaseSysUserEntity, + alias: 'b', + condition: 'a.userId = b.id' + }, + ] +}) +``` + +2、service 中 + +通过`this.nativeQuery`或者`this.sqlRenderPage`两种方法执行自定义 sql + +- nativeQuery:执行原生 sql,返回数组 +- sqlRenderPage:执行原生 sql,返回分页对象 + +模板 sql 示例,方便动态传入参数,千万不要直接拼接 sql,有 sql 注入风险,以下方法 cool-admin 内部已经做了防注入处理 + +- setSql:第一个参数是条件,第二个参数是 sql,第三个参数是参数数组 + +```ts +this.nativeQuery( + `SELECT + a.*, + b.nickName + FROM + demo_goods a + LEFT JOIN user_info b ON a.userId = b.id + ${this.setSql(true, 'and b.userId = ?', [userId])}` +``` + +3、通过 typeorm 原生的写法 + +示例 + +```ts +const find = this.demoGoodsEntity + .createQueryBuilder("a") + .select(["a.*", "b.nickName as userName"]) + .leftJoin(UserInfoEntity, "b", "a.id = b.id") + .getRawMany(); +``` + +## 配置字典和可选项(8.x 新增) + +为了让前端可能自动识别某个字段的可选项或者属于哪个字典,我们可以在@Column 注解上配置`options`和`dict`属性, + +旧的写法 + +```ts +// 无法指定字典 + +// 可选项只能按照一定规则编写,否则前端无法识别 +@Column({ comment: '状态 0-禁用 1-启用', default: 1 }) +status: number; +``` + +新的写法 + +```ts +// 指定字典为goodsType,这样前端生成的时候就会默认指定这个字典 +@Column({ comment: '分类', dict: 'goodsType' }) +type: number; + +// 状态的可选项有禁用和启用,默认是启用,值是数组的下标,0-禁用,1-启用 +@Column({ comment: '状态', dict: ['禁用', '启用'], default: 1 }) +status: number; +``` diff --git a/.cursor/rules/event.mdc b/.cursor/rules/event.mdc new file mode 100644 index 0000000..5d03a04 --- /dev/null +++ b/.cursor/rules/event.mdc @@ -0,0 +1,102 @@ +--- +description: 事件(Event) +globs: +--- +# 事件(Event) + +事件是开发过程中经常使用到的功能,我们经常利用它来做一些解耦的操作。如:更新了用户信息,其他需要更新相关信息的操作自行监听更新等 + +## 新建监听 + +```ts +import { Provide, Scope, ScopeEnum } from "@midwayjs/core"; +import { CoolEvent, Event } from "@cool-midway/core"; + +/** + * 接收事件 + */ +@CoolEvent() +export class DemoEvent { + /** + * 根据事件名接收事件 + * @param msg + * @param a + */ + @Event("updateUser") + async updateUser(msg, a) { + console.log("ImEvent", "updateUser", msg, a); + } +} +``` + +## 发送事件 + +```ts +import { Get, Inject, Provide } from "@midwayjs/core"; +import { + CoolController, + BaseController, + CoolEventManager, +} from "@cool-midway/core"; + +/** + * 事件 + */ +@CoolController() +export class DemoEventController extends BaseController { + @Inject() + coolEventManager: CoolEventManager; + + /** + * 发送事件 + */ + @Get("/send") + public async send() { + this.coolEventManager.emit("updateUser", { a: 1 }, 12); + } +} +``` + +## 多进程通信 + +当你的项目利用如`pm2`等工具部署为 cluster 模式的时候,你的项目会有多个进程,这时候你的事件监听和发送只会在当前进程内有效,如果你需要触发到所有或者随机一个进程,需要使用多进程通信,这里我们提供了一个简单的方式来实现多进程通信。 + +需要根据你的业务需求来使用该功能!!! + +```ts +import { Get, Inject, Provide } from "@midwayjs/core"; +import { + CoolController, + BaseController, + CoolEventManager, +} from "@cool-midway/core"; + +/** + * 事件 + */ +@Provide() +@CoolController() +export class DemoEventController extends BaseController { + @Inject() + coolEventManager: CoolEventManager; + + @Post("/global", { summary: "全局事件,多进程都有效" }) + async global() { + await this.coolEventManager.globalEmit("demo", false, { a: 2 }, 1); + return this.ok(); + } +} +``` + +**globalEmit** + +```ts +/** + * 发送全局事件 + * @param event 事件 + * @param random 是否随机一个 + * @param args 参数 + * @returns + */ +globalEmit(event: string, random?: boolean, ...args: any[]) +``` diff --git a/.cursor/rules/exception.mdc b/.cursor/rules/exception.mdc new file mode 100644 index 0000000..93b27a4 --- /dev/null +++ b/.cursor/rules/exception.mdc @@ -0,0 +1,22 @@ +--- +description: 异常处理(Exception) +globs: +--- +# 异常处理 + +框架自带有: `CoolCommException` + +## 通用异常 + +CoolCommException + +返回码: 1001 + +返回消息:comm fail + +用法: + +```ts +// 可以自定义返回消息 +throw new CoolCommException('用户不存在~'); +``` \ No newline at end of file diff --git a/.cursor/rules/module.mdc b/.cursor/rules/module.mdc new file mode 100644 index 0000000..8c01f72 --- /dev/null +++ b/.cursor/rules/module.mdc @@ -0,0 +1,185 @@ +--- +description: 模块开发(module) +globs: +--- +# 模块开发(module) + +对于一个应用开发,我们应该更加有规划,`cool-admin`提供了模块开发的概念。 + +建议模块目录`src/modules/模块名` + +```ts + ├── modules + │ └── base(基础的权限管理系统) + │ │ └── controller(api接口, 用法参考 [controller.mdc](mdc:.cursor/rules/controller.mdc) ) + │ │ │ └── admin(后台管理接口) + │ │ │ └── app(应用接口,如小程序APP等) + │ │ └── dto(可选,参数校验) + │ │ └── entity(实体类, 用法参考 [db.mdc](mdc:.cursor/rules/db.mdc) ) + │ │ └── middleware(可选,中间件, 参考 [middleware.code-snippets](mdc:.vscode/middleware.code-snippets) [authority.ts](mdc:src/modules/base/middleware/authority.ts) ) + │ │ └── schedule(可选,定时任务 参考 [task.mdc](mdc:.cursor/rules/task.mdc) ) + │ │ └── service(服务,写业务逻辑,参考 [service.mdc](mdc:.cursor/rules/service.mdc) ) + │ │ └── config.ts(必须,模块的配置) + │ │ └── db.json(可选,初始化该模块的数据,参考 [db.json](mdc:src/modules/base/db.json) ) + │ │ └── menu.json(可选(7.x新增,配合模块市场使用),初始化该模块的菜单,参考 [menu.json](mdc:src/modules/base/menu.json) ) + +``` + +## 模块配置 + +#### config.ts + +```ts +import { ModuleConfig } from '@cool-midway/core'; + +/** + * 模块配置 + */ +export default () => { + return { + // 必须,模块名称 + name: '聊天模块', + // 必须,模块描述 + description: '基于socket.io提供即时通讯聊天功能', + // 可选,中间件,只对本模块有效 + middlewares: [], + // 可选,全局中间件 + globalMiddlewares: [], + // 可选,模块加载顺序,默认为0,值越大越优先加载 + order: 1; + // 其他配置,jwt配置 + jwt: 'IUFHOFNIWI', + } as ModuleConfig; +}; + +``` + +::: warning +config.ts 的配置文件是必须的,有几个必填项描述着模块的功能,当然除此之外,你还可以设置模块的一些特有配置 +::: + +#### 引入配置 + +```ts + + @Config('module.模块名,模块文件夹名称,如demo') + config; + +``` + +## 数据导入 + +在模块中预设要导入的数据,位于`模块/db.json` + +1、向`dict_type`表导入数据 + +```json +{ + "dict_type": [ + { + "name": "升级类型", + "key": "upgradeType" + } + ] +} +``` + +2、导入有层级的数据,比如`dict_info`表需要先插入`dict_type`拿到`id`,再插入`dict_info` + +```json +{ + "dict_type": [ + { + "name": "升级类型", + "key": "upgradeType", + "@childDatas": { + "dict_info": [ + { + "typeId": "@id", + "name": "安卓", + "orderNum": 1, + "remark": null, + "parentId": null, + "value": "0" + }, + { + "typeId": "@id", + "name": "IOS", + "orderNum": 1, + "remark": null, + "parentId": null, + "value": "1" + } + ] + } + } + ] +} +``` + +`@childDatas`是一个特殊的字段,表示该字段下的数据需要先插入父级表,再插入子级表,`@id`表示父级表的`id`,`@id`是一个特殊的字段,表示插入父级表后,会返回`id`,然后插入子级表 + +## 菜单导入 + +在模块中预设要导入的菜单,位于`模块/menu.json`,菜单数据可以通过后台管理系统的菜单管理导出,不需要手动编写 + +详细参考 [menu.json](mdc:src/modules/base/menu.json) + +```json +[ + { + "name": "应用管理", + "router": null, + "perms": null, + "type": 0, + "icon": "icon-app", + "orderNum": 2, + "viewPath": null, + "keepAlive": true, + "isShow": true, + "childMenus": [ + { + "name": "套餐管理", + "router": "/app/goods", + "perms": null, + "type": 1, + "icon": "icon-goods", + "orderNum": 0, + "viewPath": "modules/app/views/goods.vue", + "keepAlive": true, + "isShow": true + } + ] + } +] +``` + +#### 关闭自动导入 + +通过该配置开启自动初始化模块数据库脚本 + +```ts +cool: { + // 是否自动导入数据库 + initDB: false, + } as CoolConfig, +``` + +::: warning +我们不建议在生产环境使用该功能,生产环境是数据库请通过本地导入与同步数据库结构 +::: + +#### 重新初始化 + +首次启动会初始化模块数据库,初始化完成会在项目根目录生成`.lock`文件,下次启动就不会重复导入,如果需要重新导入,删除该文件夹即可 + +```ts + ├── lock + │ ├── db + │ └── base.db.lock(base模块) + │ └── task.db.lock(task模块) + │ ├── menu + │ └── base.menu.lock(base模块) + │ └── task.menu.lock(task模块) + │──package.json +``` diff --git a/.cursor/rules/service.mdc b/.cursor/rules/service.mdc new file mode 100644 index 0000000..d6bfacf --- /dev/null +++ b/.cursor/rules/service.mdc @@ -0,0 +1,320 @@ +--- +description: 服务(Service) +globs: +--- +# 服务(Service) + +我们一般将业务逻辑写在`Service`层,`Controller`层只做参数校验、数据转换等操作,`Service`层做具体的业务逻辑处理。 + +`cool-admin`对基本的`Service`进行封装; + +## 重写 CRUD + +`Controller`的六个快速方法,`add`、`update`、`delete`、`info`、`list`、`page`,是通过调用一个通用的`BaseService`的方法实现,所以我们可以重写`Service`的方法来实现自己的业务逻辑。 + +**示例** + +重写 add 方法 + +```ts +import { DemoGoodsEntity } from "./../entity/goods"; +import { Provide } from "@midwayjs/core"; +import { BaseService } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { Repository } from "typeorm"; + +/** + * 商品示例 + */ +@Provide() +export class DemoGoodsService extends BaseService { + @InjectEntityModel(DemoGoodsEntity) + demoGoodsEntity: Repository; + + /** + * 新增 + * @param param + * @returns + */ + async add(param: any) { + // 调用原本的add,如果不需要可以不用这样写,完全按照自己的新增逻辑写 + const result = await super.add(param); + // 你自己的业务逻辑 + return result; + } +} +``` + +记得在`Controller`上配置对应的`Service`才会使其生效 + +```ts +import { DemoGoodsService } from "../../service/goods"; +import { DemoGoodsEntity } from "../../entity/goods"; +import { Body, Inject, Post, Provide } from "@midwayjs/core"; +import { CoolController, BaseController } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { Repository } from "typeorm"; + +/** + * 测试 + */ +@Provide() +@CoolController({ + api: ["add", "delete", "update", "info", "list", "page"], + entity: DemoGoodsEntity, + service: DemoGoodsService +}) +export class AppDemoGoodsController extends BaseController {} +``` + +## 普通查询(TypeOrm) + +普通查询基于[TypeOrm](mdc:https:/typeorm.io),点击查看官方详细文档 + +**示例** + +```ts +import { DemoGoodsEntity } from "./../entity/goods"; +import { Provide } from "@midwayjs/core"; +import { BaseService } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { In, Repository } from "typeorm"; + +/** + * 商品示例 + */ +@Provide() +export class DemoGoodsService extends BaseService { + @InjectEntityModel(DemoGoodsEntity) + demoGoodsEntity: Repository; + + async typeorm() { + // 新增单个,传入的参数字段在数据库中一定要存在 + await this.demoGoodsEntity.insert({ title: "xxx" }); + // 新增单个,传入的参数字段在数据库中可以不存在 + await this.demoGoodsEntity.save({ title: "xxx" }); + // 新增多个 + await this.demoGoodsEntity.save([{ title: "xxx" }]); + // 查找单个 + await this.demoGoodsEntity.findOneBy({ id: 1 }); + // 查找多个 + await this.demoGoodsEntity.findBy({ id: In([1, 2]) }); + // 删除单个 + await this.demoGoodsEntity.delete(1); + // 删除多个 + await this.demoGoodsEntity.delete([1]); + // 根据ID更新 + await this.demoGoodsEntity.update(1, { title: "xxx" }); + // 根据条件更新 + await this.demoGoodsEntity.update({ price: 20 }, { title: "xxx" }); + // 多条件操作 + await this.demoGoodsEntity + .createQueryBuilder() + .where("id = :id", { id: 1 }) + .andWhere("price = :price", { price: 20 }) + .getOne(); + } +} +``` + +## 高级查询(SQL) + +**1、普通 SQL 查询** + +```ts +import { DemoGoodsEntity } from "./../entity/goods"; +import { Provide } from "@midwayjs/core"; +import { BaseService } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { Repository } from "typeorm"; + +/** + * 商品示例 + */ +@Provide() +export class DemoGoodsService extends BaseService { + @InjectEntityModel(DemoGoodsEntity) + demoGoodsEntity: Repository; + + /** + * 执行sql + */ + async sql(query) { + return this.nativeQuery("select * from demo_goods a where a.id = ?", [query.id]); + } +} +``` + +**2、分页 SQL 查询** + +```ts +import { DemoGoodsEntity } from "./../entity/goods"; +import { Provide } from "@midwayjs/core"; +import { BaseService } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { Repository } from "typeorm"; + +/** + * 商品示例 + */ +@Provide() +export class DemoGoodsService extends BaseService { + @InjectEntityModel(DemoGoodsEntity) + demoGoodsEntity: Repository; + + /** + * 执行分页sql + */ + async sqlPage(query) { + return this.sqlRenderPage("select * from demo_goods ORDER BY id ASC", query, false); + } +} +``` + +**3、非 SQL 的分页查询** + +```ts +import { DemoGoodsEntity } from "./../entity/goods"; +import { Provide } from "@midwayjs/core"; +import { BaseService } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { In, Repository } from "typeorm"; + +/** + * 商品示例 + */ +@Provide() +export class DemoGoodsService extends BaseService { + @InjectEntityModel(DemoGoodsEntity) + demoGoodsEntity: Repository; + + /** + * 执行entity分页 + */ + async entityPage(query) { + const find = this.demoGoodsEntity.createQueryBuilder(); + find.where("id = :id", { id: 1 }); + return this.entityRenderPage(find, query); + } +} +``` + +**4、SQL 动态条件** + +分页查询和普通的 SQL 查询都支持动态条件,通过`this.setSql(条件,sql语句,参数)`来配置 + +```ts +import { DemoGoodsEntity } from "./../entity/goods"; +import { Provide } from "@midwayjs/core"; +import { BaseService } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { Repository } from "typeorm"; + +/** + * 商品示例 + */ +@Provide() +export class DemoGoodsService extends BaseService { + @InjectEntityModel(DemoGoodsEntity) + demoGoodsEntity: Repository; + + /** + * 执行sql + */ + async sql(query) { + return this.nativeQuery(` + select * from demo_goods a + WHERE 1=1 + ${this.setSql(query.id, "and a.id = ?", [query.id])} + ORDER BY id ASC + `); + } +} +``` + +## 修改之前(modifyBefore) + +有时候我们需要在数据进行修改动作之前,对它进行一些处理,比如:修改密码时,需要对密码进行加密,这时候我们可以使用`modifyBefore`方法来实现 + +```ts +import { DemoGoodsEntity } from "./../entity/goods"; +import { Provide } from "@midwayjs/core"; +import { BaseService } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { Repository } from "typeorm"; +import * as md5 from "md5"; + +/** + * 商品示例 + */ +@Provide() +export class DemoGoodsService extends BaseService { + @InjectEntityModel(DemoGoodsEntity) + demoGoodsEntity: Repository; + + /** + * 修改之前 + * @param data + * @param type + */ + async modifyBefore(data: any, type: "delete" | "update" | "add") { + if (type == "update") { + data.password = md5(data.password); + } + } +} +``` + +## 修改之后(modifyAfter) + +有时候我们需要在数据进行修改动作之后,对它进行一些处理,比如:修改完数据之后将它放入队列或者 ElasticSearch + +```ts +import { DemoGoodsEntity } from "./../entity/goods"; +import { Provide } from "@midwayjs/core"; +import { BaseService } from "@cool-midway/core"; +import { InjectEntityModel } from "@midwayjs/typeorm"; +import { Repository } from "typeorm"; +import * as md5 from "md5"; + +/** + * 商品示例 + */ +@Provide() +export class DemoGoodsService extends BaseService { + @InjectEntityModel(DemoGoodsEntity) + demoGoodsEntity: Repository; + + /** + * 修改之后 + * @param data + * @param type + */ + async modifyAfter(data: any, type: "delete" | "update" | "add") { + // 你想做的其他事情 + } +} +``` + +## 设置实体 + +`Service`与`Service`之间相互调用`BaseService`里的方法,有可能出现“未设置操作实体”的问题可以通过以下方式设置实体 + +::: warning 建议 +但是一般不建议这样做,因为这样会导致`Service`与`Service`耦合,不利于代码的维护,如果要操作对应的表直接在当前的`Service`注入对应的表操作即可 +::: + +```ts +@Provide() +export class XxxService extends BaseService { + @InjectEntityModel(XxxEntity) + xxxEntity: Repository; + + @Init() + async init() { + await super.init(); + // 设置实体 + this.setEntity(this.xxxEntity); + } +} +``` diff --git a/.cursor/rules/socket.mdc b/.cursor/rules/socket.mdc new file mode 100644 index 0000000..0e35fc7 --- /dev/null +++ b/.cursor/rules/socket.mdc @@ -0,0 +1,127 @@ +--- +description: 即时通讯(Socket) +globs: +--- +# 即时通讯(Socket) + +`cool-admin`即时通讯功能基于[Socket.io(v4)](https://socket.io/docs/v4)开发,[midwayjs 官方 Socket.io 文档](http://midwayjs.org/docs/extensions/socketio) + +## 配置 + +`configuration.ts` + +```ts +import * as socketio from "@midwayjs/socketio"; + +@Configuration({ + imports: [ + // socketio http://www.midwayjs.org/docs/extensions/socketio + socketio, + ], + importConfigs: [join(__dirname, "./config")], +}) +export class ContainerLifeCycle { + @App() + app: koa.Application; + + async onReady() {} +} +``` + +## 配置`config/config.default.ts` + +需要配置 redis 适配器,让进程之间能够进行通讯 + +```ts +import { CoolConfig, MODETYPE } from "@cool-midway/core"; +import { MidwayConfig } from "@midwayjs/core"; +import * as fsStore from "@cool-midway/cache-manager-fs-hash"; +import { createAdapter } from "@socket.io/redis-adapter"; +// @ts-ignore +import Redis from "ioredis"; + +const redis = { + host: "127.0.0.1", + port: 6379, + password: "", + db: 0, +}; + +const pubClient = new Redis(redis); +const subClient = pubClient.duplicate(); + +export default { + // ... + // socketio + socketIO: { + upgrades: ["websocket"], // 可升级的协议 + adapter: createAdapter(pubClient, subClient), + }, +} as MidwayConfig; +``` + +## 服务端 + +```ts +import { + WSController, + OnWSConnection, + Inject, + OnWSMessage, +} from "@midwayjs/core"; +import { Context } from "@midwayjs/socketio"; +/** + * 测试 + */ +@WSController("/") +export class HelloController { + @Inject() + ctx: Context; + + // 客户端连接 + @OnWSConnection() + async onConnectionMethod() { + console.log("on client connect", this.ctx.id); + console.log("参数", this.ctx.handshake.query); + this.ctx.emit("data", "连接成功"); + } + + // 消息事件 + @OnWSMessage("myEvent") + async gotMessage(data) { + console.log("on data got", this.ctx.id, data); + } +} +``` + +## 客户端 + +```ts +const io = require("socket.io-client"); + +const socket = io("http://127.0.0.1:8001", { + auth: { + token: "xxx", + }, +}); + +socket.on("data", (msg) => { + console.log("服务端消息", msg); +}); +``` + +## 注意事项 + +如果部署为多线程的,为了让进程之间能够进行通讯,需要配置 redis 适配器,[配置方式](http://midwayjs.org/docs/extensions/socketio#%E9%85%8D%E7%BD%AE-redis-%E9%80%82%E9%85%8D%E5%99%A8) + +```ts +// src/config/config.default +import { createRedisAdapter } from "@midwayjs/socketio"; + +export default { + // ... + socketIO: { + adapter: createRedisAdapter({ host: "127.0.0.1", port: 6379 }), + }, +}; +``` diff --git a/.cursor/rules/task.mdc b/.cursor/rules/task.mdc new file mode 100644 index 0000000..b2a17ac --- /dev/null +++ b/.cursor/rules/task.mdc @@ -0,0 +1,382 @@ +--- +description: 任务与队列(Task) +globs: +--- +# 任务与队列(Task) + +## 内置任务(代码中配置) + +内置定时任务能力来自于[midwayjs](https://www.midwayjs.org/docs/extensions/cron) + +### 引入组件 + +```ts +import { Configuration } from "@midwayjs/core"; +import * as cron from "@midwayjs/cron"; // 导入模块 +import { join } from "path"; + +@Configuration({ + imports: [cron], + importConfigs: [join(__dirname, "config")], +}) +export class AutoConfiguration {} +``` + +### 使用 + +```ts +import { Job, IJob } from "@midwayjs/cron"; +import { FORMAT } from "@midwayjs/core"; + +@Job({ + cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE, + start: true, +}) +export class DataSyncCheckerJob implements IJob { + async onTick() { + // ... + } +} +``` + +```ts +@Job("syncJob", { + cronTime: "*/2 * * * * *", // 每隔 2s 执行 +}) +export class DataSyncCheckerJob implements IJob { + async onTick() { + // ... + } +} +``` + +### 规则 cron + +```ts +* * * * * * +┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ | +│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) +│ │ │ │ └───── month (1 - 12) +│ │ │ └────────── day of month (1 - 31) +│ │ └─────────────── hour (0 - 23) +│ └──────────────────── minute (0 - 59) +└───────────────────────── second (0 - 59, optional) + +``` + +::: warning 警告 + +注意:该方式在多实例部署的情况下无法做到任务之前的协同,任务存在重复执行的可能 + +::: + +## 本地任务(管理后台配置,v8.0 新增) + +可以到登录后台`/系统管理/任务管理/任务列表`,配置任务。默认是不需要任何依赖的, 旧版需要依赖`redis`才能使用该功能。 + +### 配置任务 + +配置完任务可以调用你配置的 service 方法,如:taskDemoService.test() + +### 规则 cron + +规则 cron + +```ts +* * * * * * +┬ ┬ ┬ ┬ ┬ ┬ +│ │ │ │ │ | +│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) +│ │ │ │ └───── month (1 - 12) +│ │ │ └────────── day of month (1 - 31) +│ │ └─────────────── hour (0 - 23) +│ └──────────────────── minute (0 - 59) +└───────────────────────── second (0 - 59, optional) + +``` + +规则示例: + +- 每 5 秒执行一次: `*/5 * * * * *` +- 每 5 分钟执行一次: `*/5 * * * *` +- 每小时执行一次: `0 * * * *` +- 每天执行一次: `0 0 * * *` +- 每天 1 点执行: `0 1 * * *` +- 每周执行一次: `0 0 * * 0` +- 每月执行一次: `0 0 1 * *` + +![](/admin/node/task.png) + +## 分布式任务(管理后台配置) + +当需要分布式部署时,需要开启分布式任务,通过 redis 作为协同整个集群的任务,防止任务重复执行等异常情况。 + +#### 引入插件 + +`src/configuration.ts` + +```ts +import { Configuration, App } from "@midwayjs/core"; +import { join } from "path"; +import * as task from "@cool-midway/task"; + +@Configuration({ + imports: [task], + importConfigs: [join(__dirname, "./config")], +}) +export class ContainerLifeCycle { + @App() + app: koa.Application; + + async onReady() {} +} +``` + +#### 配置 + +[redis>=5.x](https://redis.io/),推荐[redis>=7.x](https://redis.io/) + +`src/config/config.default.ts` + +::: warning 注意 +很多人忽略了这个配置,导致项目包 redis 连接错误!!! +::: + +```ts +import { CoolFileConfig, MODETYPE } from "@cool-midway/file"; +import { MidwayConfig } from "@midwayjs/core"; +import * as fsStore from "cache-manager-fs-hash"; + +export default { + // 修改成你自己独有的key + keys: "cool-admin for node", + koa: { + port: 8001, + }, + // cool配置 + cool: { + redis: { + host: "127.0.0.1", + port: 6379, + password: "", + db: 0, + }, + }, +} as unknown as MidwayConfig; +``` + +redis cluster 方式 + +```ts +[ + { + host: "192.168.0.103", + port: 7000, + }, + { + host: "192.168.0.103", + port: 7001, + }, + { + host: "192.168.0.103", + port: 7002, + }, + { + host: "192.168.0.103", + port: 7003, + }, + { + host: "192.168.0.103", + port: 7004, + }, + { + host: "192.168.0.103", + port: 7005, + }, +]; +``` + +### 创建执行任务的 service + +```ts +import { Provide } from "@midwayjs/core"; +import { BaseService } from "@cool-midway/core"; +/** + * 任务执行的demo示例 + */ +@Provide() +export class DemoTaskService extends BaseService { + /** + * 测试任务执行 + * @param params 接收的参数 数组 [] 可不传 + */ + async test(params?: []) { + // 需要登录后台任务管理配置任务 + console.log("任务执行了", params); + } +} +``` + +### 配置定时任务 + +登录后台 任务管理/任务列表 + +![](/admin/node/task.png) + +::: warning +截图中的 demoTaskService 为上一步执行任务的 service 的实例 ID,midwayjs 默认为类名首字母小写!!! + +任务调度基于 redis,所有的任务都需要通过代码去维护任务的创建,启动,暂停。 所以直接改变数据库的任务状态是无效的,redis 中的信息还未清空, 任务将继续执行。 +::: + +## 队列 + +之前的分布式任务调度,其实是利用了[bullmq](https://docs.bullmq.io/)的重复队列机制。 + +在项目开发过程中特别是较大型、数据量较大、业务较复杂的场景下往往需要用到队列。 如:抢购、批量发送消息、分布式事务、订单 2 小时后失效等。 + +得益于[bullmq](https://docs.bullmq.io/),cool 的队列也支持`延迟`、`重复`、`优先级`等高级特性。 + +### 创建队列 + +一般放在名称为 queue 文件夹下 + +#### 普通队列 + +普通队列数据由消费者自动消费,必须重写 data 方法用于被动消费数据。 + +`src/modules/demo/queue/comm.ts` + +```ts +import { BaseCoolQueue, CoolQueue } from "@cool-midway/task"; +import { IMidwayApplication } from "@midwayjs/core"; +import { App } from "@midwayjs/core"; + +/** + * 普通队列 + */ +@CoolQueue() +export class DemoCommQueue extends BaseCoolQueue { + @App() + app: IMidwayApplication; + + async data(job: any, done: any): Promise { + // 这边可以执行定时任务具体的业务或队列的业务 + console.log("数据", job.data); + // 抛出错误 可以让队列重试,默认重试5次 + //throw new Error('错误'); + done(); + } +} +``` + +#### 主动队列 + +主动队列数据由消费者主动消费 + +`src/modules/demo/queue/getter.ts` + +```ts +import { BaseCoolQueue, CoolQueue } from "@cool-midway/task"; + +/** + * 主动消费队列 + */ +@CoolQueue({ type: "getter" }) +export class DemoGetterQueue extends BaseCoolQueue {} +``` + +主动消费数据 + +```ts + // 主动消费队列 + @Inject() + demoGetterQueue: DemoGetterQueue; + + const job = await this.demoGetterQueue.getters.getJobs(['wait'], 0, 0, true); + // 获得完将数据从队列移除 + await job[0].remove(); +``` + +### 发送数据 + +```ts +import { Get, Inject, Post, Provide } from "@midwayjs/core"; +import { CoolController, BaseController } from "@cool-midway/core"; +import { DemoCommQueue } from "../../queue/comm"; +import { DemoGetterQueue } from "../../queue/getter"; + +/** + * 队列 + */ +@Provide() +@CoolController() +export class DemoQueueController extends BaseController { + // 普通队列 + @Inject() + demoCommQueue: DemoCommQueue; + + // 主动消费队列 + @Inject() + demoGetterQueue: DemoGetterQueue; + + /** + * 发送数据到队列 + */ + @Post("/add", { summary: "发送队列数据" }) + async queue() { + this.demoCommQueue.add({ a: 2 }); + return this.ok(); + } + + /** + * 获得队列中的数据,只有当队列类型为getter时有效 + */ + @Get("/getter") + async getter() { + const job = await this.demoCommQueue.getters.getJobs(["wait"], 0, 0, true); + // 获得完将数据从队列移除 + await job[0].remove(); + return this.ok(job[0].data); + } +} +``` + +队列配置 + +```ts +interface JobOpts { + priority: number; // Optional priority value. ranges from 1 (highest priority) to MAX_INT (lowest priority). Note that + // using priorities has a slight impact on performance, so do not use it if not required. + + delay: number; // An amount of milliseconds to wait until this job can be processed. Note that for accurate delays, both + // server and clients should have their clocks synchronized. [optional]. + + attempts: number; // The total number of attempts to try the job until it completes. + + repeat: RepeatOpts; // Repeat job according to a cron specification. + + backoff: number | BackoffOpts; // Backoff setting for automatic retries if the job fails, default strategy: `fixed` + + lifo: boolean; // if true, adds the job to the right of the queue instead of the left (default false) + timeout: number; // The number of milliseconds after which the job should be fail with a timeout error [optional] + + jobId: number | string; // Override the job ID - by default, the job ID is a unique + // integer, but you can use this setting to override it. + // If you use this option, it is up to you to ensure the + // jobId is unique. If you attempt to add a job with an id that + // already exists, it will not be added. + + removeOnComplete: boolean | number; // If true, removes the job when it successfully + // completes. A number specified the amount of jobs to keep. Default behavior is to keep the job in the completed set. + + removeOnFail: boolean | number; // If true, removes the job when it fails after all attempts. A number specified the amount of jobs to keep + // Default behavior is to keep the job in the failed set. + stackTraceLimit: number; // Limits the amount of stack trace lines that will be recorded in the stacktrace. +} +``` + +::: tip +this.demoQueue.queue 获得的就是 bull 实例,更多 bull 的高级用户可以查看[bull 文档](https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md) +::: diff --git a/.cursor/rules/tenant.mdc b/.cursor/rules/tenant.mdc new file mode 100644 index 0000000..15e595a --- /dev/null +++ b/.cursor/rules/tenant.mdc @@ -0,0 +1,178 @@ +--- +description: 多租户(Tenant) +globs: +--- +# 多租户(v8.0新增) + +多租户(Multi-tenancy)是一种软件架构模式,允许单个应用实例服务多个租户(客户组织)。每个租户的数据是相互隔离的,但共享同一个应用程序代码和基础设施。 + + +## 主要特点 + +- **数据隔离**: 确保不同租户之间的数据严格分离,互不可见 +- **资源共享**: 多个租户共享同一套应用程序代码和基础设施 +- **独立配置**: 每个租户可以有自己的个性化配置和定制化需求 +- **成本优化**: 通过资源共享降低运营和维护成本 + +## 实现 + +### 1、数据隔离 + +多租户的数据隔离有许多种方案,但最为常见的是以列进行隔离的方式。Cool Admin 通过在`BaseEntity`中加入指定的列(租户ID `tenantId`)对数据进行隔离。 + +::: tip 小贴士 + +v8.0之后,`BaseEntity`已经从`cool-midway/core`中移动至`src/modules/base/entity/base.ts`,方便开发者扩展定制 + +::: + + +`src/modules/base/entity/base.ts` +```ts +import { + Index, + UpdateDateColumn, + CreateDateColumn, + PrimaryGeneratedColumn, + Column, +} from 'typeorm'; +import { CoolBaseEntity } from '@cool-midway/core'; + +/** + * 实体基类 + */ +export abstract class BaseEntity extends CoolBaseEntity { + // 默认自增 + @PrimaryGeneratedColumn('increment', { + comment: 'ID', + }) + id: number; + + @Index() + @CreateDateColumn({ comment: '创建时间' }) + createTime: Date; + + @Index() + @UpdateDateColumn({ comment: '更新时间' }) + updateTime: Date; + + @Index() + @Column({ comment: '租户ID', nullable: true }) + tenantId: number; +} + +``` + +### 2、条件注入 + +Cool 改造了 `typeorm`的 `Subscriber`,新增了以下四种监听: + +```ts +/** + * 当进行select的QueryBuilder构建之后触发 + */ +afterSelectQueryBuilder?(queryBuilder: SelectQueryBuilder): void; + +/** + * 当进行insert的QueryBuilder构建之后触发 + */ +afterInsertQueryBuilder?(queryBuilder: InsertQueryBuilder): void; + +/** + * 当进行update的QueryBuilder构建之后触发 + */ +afterUpdateQueryBuilder?(queryBuilder: UpdateQueryBuilder): void; + +/** + * 当进行delete的QueryBuilder构建之后触发 + */ +afterDeleteQueryBuilder?(queryBuilder: DeleteQueryBuilder): void; +``` + +在`src/modules/base/db/tenant.ts`中,通过`tenantId`进行条件注入,从而实现数据隔离。 + +## 使用 + +### 1、开启多租户 + +框架默认关闭多租户,需要手动开启,在`src/config/config.default.ts`中开启多租户 + +```ts +cool: { + // 是否开启多租户 + tenant: { + // 是否开启多租户 + enable: true, + // 需要过滤多租户的url, 支持通配符,如/admin/**/* 表示admin模块下的所有接口都进行多租户过滤 + urls: [], + }, + } +``` +tenant +### 2、代码中使用 + +只要开启了多租户,并配置了`urls`,那么框架会自动注入`tenantId`,开发者原本的代码不需要做任何修改,框架会自动进行数据隔离。 + +#### Controller + +@CoolController的`add`、`delete`、`update`、`info`、`list`、`page`方法都支持过滤多租户。 + + +#### Service + +`Service`中使用多租户,以下是一个完整的示例,包含有效和无效的情况,开发者需要结合实际业务进行选择。 + +```ts +import { Inject, Provide } from '@midwayjs/core'; +import { BaseService } from '@cool-midway/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { DemoGoodsEntity } from '../entity/goods'; +import { UserInfoEntity } from '../../user/entity/info'; +import { noTenant } from '../../base/db/tenant'; + +/** + * 商品服务 + */ +@Provide() +export class DemoTenantService extends BaseService { + @InjectEntityModel(DemoGoodsEntity) + demoGoodsEntity: Repository; + + @Inject() + ctx; + + /** + * 使用多租户 + */ + async use() { + await this.demoGoodsEntity.createQueryBuilder().getMany(); + await this.demoGoodsEntity.find(); + } + + /** + * 不使用多租户(局部不使用) + */ + async noUse() { + // 过滤多租户 + await this.demoGoodsEntity.createQueryBuilder().getMany(); + // 被noTenant包裹,不会过滤多租户 + await noTenant(this.ctx, async () => { + return await this.demoGoodsEntity.createQueryBuilder().getMany(); + }); + // 过滤多租户 + await this.demoGoodsEntity.find(); + } + + /** + * 无效多租户 + */ + async invalid() { + // 自定义sql,不进行多租户过滤 + await this.nativeQuery('select * from demo_goods'); + // 自定义分页sql,进行多租户过滤 + await this.sqlRenderPage('select * from demo_goods', {}); + } +} + +``` \ No newline at end of file diff --git a/.cursorrules b/.cursorrules index 0d922de..5f0e4e5 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,5 +1,5 @@ # 项目背景 -- 数据库:MySQL、Sqlite、Postgres、Typeorm +- 数据库:MySQL、Sqlite、Postgres、Typeorm(0.3.20) - 语言:TypeScript、JavaScript、CommonJS - 框架:Koa.js、midway.js、cool-admin-midway - 项目版本:8.x @@ -33,4 +33,8 @@ │ │ └── service(服务,写业务逻辑) │ │ └── config.ts(必须,模块的配置) │ │ └── db.json(可选,初始化该模块的数据) - │ │ └── menu.json(可选,初始化该模块的菜单) \ No newline at end of file + │ │ └── menu.json(可选,初始化该模块的菜单) + + # 其它 + + - 始终使用中文回复,包括代码注释等 \ No newline at end of file