Browse Source

Initial commit: cool-admin-midway project

master
[yxf] 1 week ago
commit
e207c76516
  1. 242
      .cursor/rules/authority.mdc
  2. 174
      .cursor/rules/cache.mdc
  3. 584
      .cursor/rules/controller.mdc
  4. 460
      .cursor/rules/db.mdc
  5. 102
      .cursor/rules/event.mdc
  6. 22
      .cursor/rules/exception.mdc
  7. 202
      .cursor/rules/module.mdc
  8. 320
      .cursor/rules/service.mdc
  9. 127
      .cursor/rules/socket.mdc
  10. 382
      .cursor/rules/task.mdc
  11. 178
      .cursor/rules/tenant.mdc
  12. 51
      .cursorrules
  13. 11
      .editorconfig
  14. 30
      .eslintrc.json
  15. 4
      .gitattributes
  16. 21
      .gitignore
  17. 3
      .prettierrc.js
  18. 28
      .vscode/config.code-snippets
  19. 19
      .vscode/controller.code-snippets
  20. 20
      .vscode/entity.code-snippets
  21. 21
      .vscode/event.code-snippets
  22. 29
      .vscode/middleware.code-snippets
  23. 21
      .vscode/queue.code-snippets
  24. 33
      .vscode/service.code-snippets
  25. 33
      Dockerfile
  26. 33
      LICENSE
  27. 196
      README.md
  28. 10
      bootstrap.js
  29. 40
      docker-compose.yml
  30. 6
      jest.config.js
  31. 93
      package.json
  32. BIN
      public/Thumbs.db
  33. 92
      public/css/welcome.css
  34. BIN
      public/favicon.ico
  35. 30
      public/index.html
  36. 14
      public/js/welcome.js
  37. 202
      public/swagger/LICENSE
  38. 2
      public/swagger/NOTICE
  39. 22
      public/swagger/README.md
  40. 14
      public/swagger/absolute-path.js
  41. BIN
      public/swagger/favicon-16x16.png
  42. BIN
      public/swagger/favicon-32x32.png
  43. 16
      public/swagger/index.css
  44. 19
      public/swagger/index.html
  45. 17
      public/swagger/index.js
  46. 79
      public/swagger/oauth2-redirect.html
  47. 18
      public/swagger/package.json
  48. 20
      public/swagger/swagger-initializer.js
  49. 3
      public/swagger/swagger-ui-bundle.js
  50. 1
      public/swagger/swagger-ui-bundle.js.map
  51. 3
      public/swagger/swagger-ui-es-bundle-core.js
  52. 1
      public/swagger/swagger-ui-es-bundle-core.js.map
  53. 3
      public/swagger/swagger-ui-es-bundle.js
  54. 1
      public/swagger/swagger-ui-es-bundle.js.map
  55. 3
      public/swagger/swagger-ui-standalone-preset.js
  56. 1
      public/swagger/swagger-ui-standalone-preset.js.map
  57. 3
      public/swagger/swagger-ui.css
  58. 1
      public/swagger/swagger-ui.css.map
  59. 2
      public/swagger/swagger-ui.js
  60. 1
      public/swagger/swagger-ui.js.map
  61. 66
      src/comm/path.ts
  62. 51
      src/comm/port.ts
  63. 271
      src/comm/utils.ts
  64. 110
      src/config/config.default.ts
  65. 43
      src/config/config.local.ts
  66. 44
      src/config/config.prod.ts
  67. 73
      src/configuration.ts
  68. 2
      src/entities.ts
  69. 6
      src/interface.ts
  70. 40
      src/modules/base/config.ts
  71. 29
      src/modules/base/controller/admin/coding.ts
  72. 99
      src/modules/base/controller/admin/comm.ts
  73. 98
      src/modules/base/controller/admin/open.ts
  74. 32
      src/modules/base/controller/admin/sys/department.ts
  75. 64
      src/modules/base/controller/admin/sys/log.ts
  76. 46
      src/modules/base/controller/admin/sys/menu.ts
  77. 34
      src/modules/base/controller/admin/sys/param.ts
  78. 38
      src/modules/base/controller/admin/sys/role.ts
  79. 35
      src/modules/base/controller/admin/sys/user.ts
  80. 1
      src/modules/base/controller/app/README.md
  81. 72
      src/modules/base/controller/app/comm.ts
  82. 102
      src/modules/base/db.json
  83. 213
      src/modules/base/db/tenant.ts
  84. 21
      src/modules/base/dto/login.ts
  85. 65
      src/modules/base/entity/base.ts
  86. 15
      src/modules/base/entity/sys/conf.ts
  87. 23
      src/modules/base/entity/sys/department.ts
  88. 28
      src/modules/base/entity/sys/log.ts
  89. 47
      src/modules/base/entity/sys/menu.ts
  90. 27
      src/modules/base/entity/sys/param.ts
  91. 31
      src/modules/base/entity/sys/role.ts
  92. 14
      src/modules/base/entity/sys/role_department.ts
  93. 14
      src/modules/base/entity/sys/role_menu.ts
  94. 58
      src/modules/base/entity/sys/user.ts
  95. 14
      src/modules/base/entity/sys/user_role.ts
  96. 45
      src/modules/base/event/app.ts
  97. 50
      src/modules/base/event/menu.ts
  98. 25
      src/modules/base/job/log.ts
  99. 943
      src/modules/base/menu.json
  100. 153
      src/modules/base/middleware/authority.ts

242
.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<Context, NextFunction>
{
@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 生成校验的实现方式

174
.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 等其他位置使用
:::

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

460
.cursor/rules/db.mdc

@ -0,0 +1,460 @@
---
description: 数据库(db)
globs:
---
# 数据库(db)
数据库使用的是`typeorm`库
中文文档:](httpsom)
官方文档:[https://typeorm.io](mdc: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<DemoAppGoodsEntity>;
/**
* 事务
* @param params
* @param queryRunner 无需调用者传参, 自动注入,最后一个参数
*/
@CoolTransaction({ isolation: "SERIALIZABLE" })
async testTransaction(params: any, queryRunner?: QueryRunner) {
await queryRunner.manager.insert<DemoAppGoodsEntity>(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;
```

102
.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[])
```

22
.cursor/rules/exception.mdc

@ -0,0 +1,22 @@
---
description: 异常处理(Exception)
globs:
---
# 异常处理
框架自带有: `CoolCommException`
## 通用异常
CoolCommException
返回码: 1001
返回消息:comm fail
用法:
```ts
// 可以自定义返回消息
throw new CoolCommException('用户不存在~');
```

202
.cursor/rules/module.mdc

@ -0,0 +1,202 @@
---
description: 模块开发(module)
globs:
---
# 模块开发(module)
对于一个应用开发,我们应该更加有规划,`cool-admin`提供了模块开发的概念。
建议模块目录`src/modules/模块名`
```ts
├── modules
│ └── base(基础的权限管理系统)
│ │ └── controller(api接口, 用法参考 [controller.mdc](mdc:.cursor/rules/controller.mdc) ,必要时需要创建关联查询, 配置pageQueryOp)
│ │ │ └── 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) )
```
创建模块一般需要创建`controller`、`entity`、`service`,
如果entity文件夹没有子文件夹,那么引用BaseEntity是
- 引用BaseEntity固定为
```ts
import { BaseEntity } from '../../modules/base/entity/base';
```
错误示例
```ts
import { BaseEntity } from '../../../modules/base/entity/base';
```
多了一个层级
## 模块配置
#### 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
```

320
.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<DemoGoodsEntity>;
/**
* 新增
* @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<DemoGoodsEntity>;
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<DemoGoodsEntity>;
/**
* 执行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<DemoGoodsEntity>;
/**
* 执行分页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<DemoGoodsEntity>;
/**
* 执行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<DemoGoodsEntity>;
/**
* 执行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<DemoGoodsEntity>;
/**
* 修改之前
* @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<DemoGoodsEntity>;
/**
* 修改之后
* @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<XxxEntity>;
@Init()
async init() {
await super.init();
// 设置实体
this.setEntity(this.xxxEntity);
}
}
```

127
.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 }),
},
};
```

382
.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<void> {
// 这边可以执行定时任务具体的业务或队列的业务
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)
:::

178
.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<any>): void;
/**
* 当进行insert的QueryBuilder构建之后触发
*/
afterInsertQueryBuilder?(queryBuilder: InsertQueryBuilder<any>): void;
/**
* 当进行update的QueryBuilder构建之后触发
*/
afterUpdateQueryBuilder?(queryBuilder: UpdateQueryBuilder<any>): void;
/**
* 当进行delete的QueryBuilder构建之后触发
*/
afterDeleteQueryBuilder?(queryBuilder: DeleteQueryBuilder<any>): 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<DemoGoodsEntity>;
@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', {});
}
}
```

51
.cursorrules

@ -0,0 +1,51 @@
# 项目背景
- 数据库:MySQL、Sqlite、Postgres、Typeorm(0.3.20版本, 不使用外键方式,如@ManyToOne、@OneToMany等)
- 语言:TypeScript、JavaScript、CommonJS
- 框架:Koa.js、midway.js、cool-admin-midway
- 项目版本:8.x
# 目录
项目目录:
├── .vscode(代码片段,根据关键字可以快速地生成代码)
├── public(静态资源文件,如js、css或者上传的文件)
├── src
│ └── comm(通用库)
│ └── modules(项目模块)
│ └── config
│ │ └── config.default.ts(默认配置,不区分环境,都生效)
│ │ └── config.local.ts(本地开发配置,对应npm run dev)
│ │ └── config.prod.ts(生产环境配置,对应npm run start)
│ └── configuration.ts(midway的配置文件)
│ └── welcome.ts(环境的controller)
│ └── interface.ts(类型声明)
├── package.json(依赖管理,项目信息)
├── bootstrap.js(生产环境启动入口文件,可借助pm2等工具多进程启动)
└── ...
模块目录
├── modules
│ └── base(基础的权限管理系统)
│ │ └── controller(api接口)
│ │ └── dto(参数校验)
│ │ └── entity(实体类)
│ │ └── middleware(中间件)
│ │ └── schedule(定时任务)
│ │ └── service(服务,写业务逻辑)
│ │ └── config.ts(必须,模块的配置)
│ │ └── db.json(可选,初始化该模块的数据)
│ │ └── menu.json(可选,初始化该模块的菜单)
# 其它
- 始终使用中文回复,包括代码注释等
- `@midwayjs/decorator`,已弃用,使用`@midwayjs/core`
- 不要使用自定义sql来操作数据库,而是使用typeorm的api,统计相关的可以考虑使用原生sql
- Controller中不允许重写`add`、`delete`、`update`、`info`、`list`、`page`方法
- Controller不需要加@Provide()注解
- page接口关联表查询一般写在Controller的pageQueryOp中,尽量不要使用自定义sql
- Entity字段使用驼峰命名,如:studentNo
- Entity不允许使用@ManyToOne、@OneToMany等外键关系
- Entity的BaseEntity引用固定为:`import { BaseEntity } from '../../base/entity/base';`,禁止修改层级
- 创建api接口时,不要多层级如:`/student/detail`,改为`/studentDetail`,用驼峰法;
- 本项目是版本8.x,所有代码都需要按照新的写法进行编写,如Entity字典的配置
- 文件的命名不要使用驼峰法,而是使用下划线法,如:student_info.entity.ts,另外禁止太啰嗦,比如:student模块下的学生信息,不要写成:student_info, 而是写成info.ts,班级信息:class.ts,不要写成student_class.ts
- 创建模块代码需要读取.cursor/rules的module.mdc、controller.mdc、service.mdc、db.mdc,其它的rules根据需要进行参考

11
.editorconfig

@ -0,0 +1,11 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

30
.eslintrc.json

@ -0,0 +1,30 @@
{
"extends": "./node_modules/mwts/",
"ignorePatterns": [
"node_modules",
"dist",
"test",
"jest.config.js",
"typings",
"public/**/**",
"view/**/**",
"packages"
],
"env": {
"jest": true
},
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/ban-ts-comment": "off",
"node/no-extraneous-import": "off",
"no-empty": "off",
"node/no-extraneous-require": "off",
"node/no-unpublished-import": "off",
"eqeqeq": "off",
"node/no-unsupported-features/node-builtins": "off",
"@typescript-eslint/ban-types": "off",
"no-control-regex": "off",
"prefer-const": "off"
}
}

4
.gitattributes

@ -0,0 +1,4 @@
*.js text eol=lf
*.json text eol=lf
*.ts text eol=lf
*.code-snippets text eol=lf

21
.gitignore

@ -0,0 +1,21 @@
logs/
cache/
npm-debug.log
yarn-error.log
node_modules/
package-lock.json
yarn.lock
coverage/
dist/
.idea/
run/
build/
.DS_Store
launch.json
*.sw*
*.un~
.tsbuildinfo
.tsbuildinfo.*
data/*
pnpm-lock.yaml
public/uploads/*

3
.prettierrc.js

@ -0,0 +1,3 @@
module.exports = {
...require('mwts/.prettierrc.json')
}

28
.vscode/config.code-snippets

@ -0,0 +1,28 @@
{
"config": {
"prefix": "config",
"body": [
"import { ModuleConfig } from '@cool-midway/core';",
"",
"/**",
" * 模块配置",
" */",
"export default () => {",
" return {",
" // 模块名称",
" name: 'xxx',",
" // 模块描述",
" description: 'xxx',",
" // 中间件,只对本模块有效",
" middlewares: [],",
" // 中间件,全局有效",
" globalMiddlewares: [],",
" // 模块加载顺序,默认为0,值越大越优先加载",
" order: 0,",
" } as ModuleConfig;",
"};",
""
],
"description": "cool-admin config代码片段"
}
}

19
.vscode/controller.code-snippets

@ -0,0 +1,19 @@
{
"controller": {
"prefix": "controller",
"body": [
"import { CoolController, BaseController } from '@cool-midway/core';",
"",
"/**",
" * 描述",
" */",
"@CoolController({",
" api: ['add', 'delete', 'update', 'info', 'list', 'page'],",
" entity: 实体,",
"})",
"export class XxxController extends BaseController {}",
""
],
"description": "cool-admin controller代码片段"
}
}

20
.vscode/entity.code-snippets

@ -0,0 +1,20 @@
{
"entity": {
"prefix": "entity",
"body": [
"import { BaseEntity } from '../../base/entity/base';",
"import { Column, Entity } from 'typeorm';",
"",
"/**",
" * 描述",
" */",
"@Entity('xxx_xxx_xxx')",
"export class XxxEntity extends BaseEntity {",
" @Column({ comment: '描述' })",
" xxx: string;",
"}",
""
],
"description": "cool-admin entity代码片段"
}
}

21
.vscode/event.code-snippets

@ -0,0 +1,21 @@
{
"event": {
"prefix": "event",
"body": [
"import { CoolEvent, Event } from '@cool-midway/core';",
"",
"/**",
" * 接收事件",
" */",
"@CoolEvent()",
"export class xxxEvent {",
" @Event('updateUser')",
" async updateUser(msg, a) {",
" console.log('ImEvent', 'updateUser', msg, a);",
" }",
"}",
""
],
"description": "cool-admin event代码片段"
}
}

29
.vscode/middleware.code-snippets

@ -0,0 +1,29 @@
{
"middleware": {
"prefix": "middleware",
"body": [
"import { Middleware } from '@midwayjs/core';",
"import { NextFunction, Context } from '@midwayjs/koa';",
"import { IMiddleware } from '@midwayjs/core';",
"",
"/**",
" * 描述",
" */",
"@Middleware()",
"export class XxxMiddleware implements IMiddleware<Context, NextFunction> {",
" resolve() {",
" return async (ctx: Context, next: NextFunction) => {",
" // 控制器前执行的逻辑",
" const startTime = Date.now();",
" // 执行下一个 Web 中间件,最后执行到控制器",
" await next();",
" // 控制器之后执行的逻辑",
" console.log(Date.now() - startTime);",
" };",
" }",
"}",
""
],
"description": "cool-admin middleware代码片段"
}
}

21
.vscode/queue.code-snippets

@ -0,0 +1,21 @@
{
"queue": {
"prefix": "queue",
"body": [
"import { BaseCoolQueue, CoolQueue } from '@cool-midway/task';",
"",
"/**",
" * 队列",
" */",
"@CoolQueue()",
"export abstract class xxxQueue extends BaseCoolQueue {",
" async data(job: any, done: any) {",
" console.log('收到的数据', job.data);",
" done();",
" }",
"}",
""
],
"description": "cool-admin service代码片段"
}
}

33
.vscode/service.code-snippets

@ -0,0 +1,33 @@
{
"service": {
"prefix": "service",
"body": [
"import { Init, Provide } from '@midwayjs/core';",
"import { BaseService } from '@cool-midway/core';",
"import { InjectEntityModel } from '@midwayjs/typeorm';",
"import { Repository } from 'typeorm';",
"",
"/**",
" * 描述",
" */",
"@Provide()",
"export class XxxService extends BaseService {",
" @InjectEntityModel(实体)",
" xxxEntity: Repository<实体>;",
""
" @Init()"
" async init() {",
" await super.init();",
" this.setEntity(this.xxxEntity);",
" }",
"",
" /**",
" * 描述",
" */",
" async xxx() {}",
"}",
""
],
"description": "cool-admin service代码片段"
}
}

33
Dockerfile

@ -0,0 +1,33 @@
FROM node:lts-alpine
WORKDIR /app
# 配置alpine国内镜像加速
RUN sed -i "s@http://dl-cdn.alpinelinux.org/@https://repo.huaweicloud.com/@g" /etc/apk/repositories
# 安装tzdata,默认的alpine基础镜像不包含时区组件,安装后可通过TZ环境变量配置时区
RUN apk add --no-cache tzdata
# 设置时区为中国东八区,这里的配置可以被docker-compose.yml或docker run时指定的时区覆盖
ENV TZ="Asia/Shanghai"
# 如果各公司有自己的私有源,可以替换registry地址,如使用官方源注释下一行
RUN npm config set registry https://registry.npmmirror.com
# 复制package.json
COPY package.json ./package.json
# 安装依赖
RUN npm install
# 构建项目
COPY . .
RUN npm run build
# 删除开发期依赖
RUN rm -rf node_modules && rm package-lock.json
# 安装生产环境依赖
RUN npm install
# 如果端口更换,这边可以更新一下
EXPOSE 8001
CMD ["npm", "run", "start"]

33
LICENSE

@ -0,0 +1,33 @@
MIT License
Copyright (c) [2025] [厦门闪酷科技开发有限公司]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
MIT 许可证
版权所有 (c) [2025] [厦门闪酷科技开发有限公司]
特此免费授予获得本软件及相关文档文件(“软件”)副本的任何人无限制地处理本软件的权限,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件的副本,并允许软件提供给其的人员这样做,但须符合以下条件:
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何明示或暗示的担保,包括但不限于对适销性、特定用途适用性和非侵权的担保。在任何情况下,作者或版权持有人均不对因软件或软件使用或其他交易而产生的任何索赔、损害或其他责任负责,无论是在合同诉讼、侵权诉讼或其他诉讼中。

196
README.md

@ -0,0 +1,196 @@
<p align="center">
<a href="https://midwayjs.org/" target="blank"><img src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/logo.png" width="200" alt="Midway Logo" /></a>
</p>
<p align="center">cool-admin(nodejs版)一个很酷的后台权限管理系统,开源免费,Ai编码、流程编排、模块化、插件化、极速开发CRUD,方便快速构建迭代后台管理系统,支持原生、docker、普通服务器等多种方式部署
<a href="https://cool-js.com" target="_blank">官网</a> 进一步了解。
<p align="center">
<a href="https://github.com/cool-team-official/cool-admin-midway/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="GitHub license" />
<a href=""><img src="https://img.shields.io/github/package-json/v/cool-team-official/cool-admin-midway?style=flat-square" alt="GitHub tag"></a>
<img src="https://img.shields.io/github/last-commit/cool-team-official/cool-admin-midway?style=flat-square" alt="GitHub tag"></a>
</p>
## 特性
Ai 时代,很多老旧的框架已经无法满足现代化的开发需求,Cool-Admin 开发了一系列的功能,让开发变得更简单、更快速、更高效。
- **Ai 编码**:通过微调大模型学习框架特有写法,实现简单功能从 Api 接口到前端页面的一键生成[详情](https://node.cool-admin.com/src/guide/ai.html)
- **流程编排**:通过拖拽编排方式,即可实现类似像智能客服这样的功能[详情](https://node.cool-admin.com/src/guide/flow.html)
- **多租户**:支持多租户,采用全局动态注入查询条件[详情](https://node.cool-admin.com/src/guide/core/tenant.html)
- **多语言**:基于大模型自动翻译,无需更改原有代码[详情](https://node.cool-admin.com/src/guide/core/i18n.html)
- **原生打包**:打包成 exe 等安装包,打包完可以直接运行在 windows、mac、linux 等操作系统上[详情](https://node.cool-admin.com/src/guide/core/pkg.html)
- **模块化**:代码是模块化的,清晰明了,方便维护
- **插件化**:插件化的设计,可以通过安装插件的方式扩展如:支付、短信、邮件等功能
- ......
![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/flow.png)
## 技术栈
- 后端:**`node.js` `typescript`**
- 前端:**`vue.js` `element-plus` `jsx` `pinia` `vue-router`**
- 数据库:**`mysql` `postgresql` `sqlite`**
如果你是前端,后端的这些技术选型对你是特别友好的,前端开发者可以较快速地上手。
如果你是后端,Typescript 的语法又跟 java、php 等特别类似,一切看起来也是那么得熟悉。
如果你想使用 java 版本后端,请移步[cool-admin-java](https://cool-js.com/admin/java/introduce.html)
#### 官网
[https://cool-js.com](https://cool-js.com)
## 视频教程
[官方 B 站视频教程](https://www.bilibili.com/video/BV1j1421R7aB)
<!-- 在此次添加使用文档 -->
## 演示
[AI 极速编码](https://node.cool-admin.com/src/guide/ai.html)
[https://show.cool-admin.com](https://show.cool-admin.com)
- 账户:admin
- 密码:123456
<img src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/home-mini.png" alt="Admin Home"></a>
#### 项目前端
[https://github.com/cool-team-official/cool-admin-vue](https://github.com/cool-team-official/cool-admin-vue)
[https://gitee.com/cool-team-official/cool-admin-vue](https://gitee.com/cool-team-official/cool-admin-vue)
[https://gitcode.com/cool_team/cool-admin-vue](https://gitcode.com/cool_team/cool-admin-vue)
## 微信群
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg?v=1" alt="Admin Wechat"></a>
## 运行
#### 修改数据库配置,配置文件位于`src/config/config.local.ts`
以 Mysql 为例,其他数据库请参考[数据库配置文档](https://cool-js.com/admin/node/quick.html#%E6%95%B0%E6%8D%AE%E5%BA%93%E9%85%8D%E7%BD%AE)
Mysql(`>=5.7版本`),建议 8.0,node 版本(`>=18.x`),首次启动会自动初始化并导入数据
```ts
// mysql,驱动已经内置,无需安装
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'],
},
},
},
```
#### 安装依赖并运行
```bash
$ npm i
$ npm run dev
```
启动完成访问:[http://localhost:8001/](http://localhost:8001)
注: `npm i`如果安装失败可以尝试使用切换您的镜像源,推荐使用[pnpm](https://pnpm.io/)安装
## CURD(快速增删改查)
大部分的后台管理系统,或者 API 服务都是对数据进行管理,所以可以看到大量的 CRUD 场景(增删改查),cool-admin 对此进行了大量地封装,让这块的编码量变得极其地少。
#### 新建一个数据表
`src/modules/demo/entity/goods.ts`,项目启动数据库会自动创建该表,无需手动创建
```ts
import { BaseEntity } from '../../base/entity/base';
import { Column, Entity, Index } from 'typeorm';
/**
* 商品
*/
@Entity('demo_app_goods')
export class DemoAppGoodsEntity extends BaseEntity {
@Column({ comment: '标题' })
title: string;
@Column({ comment: '图片' })
pic: string;
@Column({ comment: '价格', type: 'decimal', precision: 5, scale: 2 })
price: number;
}
```
#### 编写 api 接口
`src/modules/demo/controller/app/goods.ts`,快速编写 6 个 api 接口
```ts
import { CoolController, BaseController } from '@cool-midway/core';
import { DemoAppGoodsEntity } from '../../entity/goods';
/**
* 商品
*/
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: DemoAppGoodsEntity,
})
export class DemoAppGoodsController extends BaseController {
/**
* 其他接口
*/
@Get('/other')
async other() {
return this.ok('hello, cool-admin!!!');
}
}
```
这样我们就完成了 6 个接口的编写,对应的接口如下:
- `POST /app/demo/goods/add` 新增
- `POST /app/demo/goods/delete` 删除
- `POST /app/demo/goods/update` 更新
- `GET /app/demo/goods/info` 单个信息
- `POST /app/demo/goods/list` 列表信息
- `POST /app/demo/goods/page` 分页查询(包含模糊查询、字段全匹配等)
### 部署
[部署教程](https://node.cool-admin.com/src/guide/deploy.html)
### 内置指令
- 使用 `npm run lint` 来做代码风格检查。
[midway]: https://midwayjs.org
### 低价服务器
[阿里云、腾讯云、华为云低价云服务器,不限新老](https://cool-js.com/service/cloud)

10
bootstrap.js

@ -0,0 +1,10 @@
const { Bootstrap } = require('@midwayjs/bootstrap');
// 显式以组件方式引入用户代码
Bootstrap.configure({
// 这里引用的是编译后的入口,本地开发不走这个文件
// eslint-disable-next-line node/no-unpublished-require
imports: require('./dist/index'),
// 禁用依赖注入的目录扫描
moduleDetector: false,
}).run();

40
docker-compose.yml

@ -0,0 +1,40 @@
# 本地数据库环境
# 数据存放在当前目录下的 data里
# 推荐使用安装了docker扩展的vscode打开目录 在本文件上右键可以快速启动,停止
# 如不需要相关容器开机自启动,可注释掉 restart: always
# 如遇端口冲突 可调整ports下 :前面的端口号
version: "3.1"
services:
coolDB:
image: mysql
command:
--default-authentication-plugin=mysql_native_password
--sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
--group_concat_max_len=102400
restart: always
volumes:
- ./data/mysql/:/var/lib/mysql/
environment:
TZ: Asia/Shanghai # 指定时区
MYSQL_ROOT_PASSWORD: "123456" # 配置root用户密码
MYSQL_DATABASE: "cool" # 业务库名
MYSQL_USER: "root" # 业务库用户名
MYSQL_PASSWORD: "123456" # 业务库密码
networks:
- cool
ports:
- 3306:3306
coolRedis:
image: redis
#command: --requirepass "12345678" # redis库密码,不需要密码注释本行
restart: always
environment:
TZ: Asia/Shanghai # 指定时区
volumes:
- ./data/redis/:/data/
networks:
- cool
ports:
- 6379:6379

6
jest.config.js

@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
};

93
package.json

@ -0,0 +1,93 @@
{
"name": "cool-admin",
"version": "8.0.0",
"description": "一个很酷的Ai快速开发框架",
"private": true,
"dependencies": {
"@cool-midway/core": "^8.0.7",
"@cool-midway/rpc": "^8.0.1",
"@cool-midway/task": "^8.0.2",
"@midwayjs/bootstrap": "^3.20.3",
"@midwayjs/cache-manager": "^3.20.3",
"@midwayjs/core": "^3.20.3",
"@midwayjs/cron": "^3.20.3",
"@midwayjs/cross-domain": "^3.20.3",
"@midwayjs/decorator": "^3.20.22",
"@midwayjs/info": "^3.20.3",
"@midwayjs/koa": "^3.20.3",
"@midwayjs/logger": "^3.4.2",
"@midwayjs/socketio": "^3.20.22",
"@midwayjs/static-file": "^3.20.3",
"@midwayjs/typeorm": "^3.20.3",
"@midwayjs/upload": "^3.20.3",
"@midwayjs/validate": "^3.20.3",
"@socket.io/redis-adapter": "^8.3.0",
"adm-zip": "^0.5.16",
"axios": "^1.8.4",
"cron": "^4.1.3",
"download": "^8.0.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"moment": "^2.30.1",
"mysql2": "^3.14.0",
"svg-captcha": "^1.4.0",
"tslib": "^2.8.1",
"typeorm": "npm:@cool-midway/typeorm@0.3.20",
"uuid": "^11.1.0",
"ws": "^8.18.1"
},
"devDependencies": {
"@midwayjs/bundle-helper": "^1.3.0",
"@midwayjs/mock": "^3.20.3",
"@types/jest": "^29.5.14",
"@types/node": "22",
"@yao-pkg/pkg": "^6.3.2",
"cross-env": "^7.0.3",
"jest": "^29.7.0",
"mwts": "^1.3.0",
"mwtsc": "^1.15.1",
"rimraf": "^6.0.1",
"ts-jest": "^29.3.0",
"typescript": "~5.8.2"
},
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"start": "NODE_ENV=production node ./bootstrap.js",
"dev": "rimraf src/index.ts && cool check && cross-env NODE_ENV=local mwtsc --cleanOutDir --watch --run @midwayjs/mock/app.js --keepalive",
"test": "cross-env NODE_ENV=unittest jest",
"cov": "jest --coverage",
"lint": "mwts check",
"lint:fix": "mwts fix",
"ci": "npm run cov",
"build": "cool entity && bundle && mwtsc --cleanOutDir",
"build:obfuscate": "cool entity && bundle && mwtsc --cleanOutDir && cool obfuscate",
"pkg": "rimraf build && mkdir build && npm run build && pkg . -d > build/pkg.log",
"pm2:start": "pm2 start ./bootstrap.js -i 1 --name cool-admin",
"pm2:stop": "pm2 stop cool-admin & pm2 delete cool-admin"
},
"bin": "./bootstrap.js",
"pkg": {
"scripts": [
"dist/**/*",
"node_modules/axios/dist/node/*"
],
"assets": [
"public/**/*",
"typings/**/*",
"src/locales/**/*"
],
"targets": [
"node20-win-x64"
],
"outputPath": "build"
},
"repository": {
"type": "git",
"url": "https://cool-js.com"
},
"author": "COOL",
"license": "MIT"
}

BIN
public/Thumbs.db

Binary file not shown.

92
public/css/welcome.css

@ -0,0 +1,92 @@
body {
display: flex;
min-height: 100vh;
margin: 0;
justify-content: center;
align-items: center;
text-align: center;
background: #222;
overflow-y: hidden;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
color: #6ee1f5;
padding: 10px 0 20px 0;
text-align: center;
opacity: 0;
animation: fadeIn 5s forwards;
background: #222;
}
.link {
color: #6ee1f5;
}
.reveal {
position: relative;
display: flex;
color: #6ee1f5;
font-size: 2em;
font-family: Raleway, sans-serif;
letter-spacing: 3px;
text-transform: uppercase;
white-space: pre;
}
.reveal span {
opacity: 0;
transform: scale(0);
animation: fadeIn 2.4s forwards;
}
.reveal::before, .reveal::after {
position: absolute;
content: "";
top: 0;
bottom: 0;
width: 2px;
height: 100%;
background: white;
opacity: 0;
transform: scale(0);
}
.reveal::before {
left: 50%;
animation: slideLeft 1.5s cubic-bezier(0.7, -0.6, 0.3, 1.5) forwards;
}
.reveal::after {
right: 50%;
animation: slideRight 1.5s cubic-bezier(0.7, -0.6, 0.3, 1.5) forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideLeft {
to {
left: -6%;
opacity: 1;
transform: scale(0.9);
}
}
@keyframes slideRight {
to {
right: -6%;
opacity: 1;
transform: scale(0.9);
}
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

30
public/index.html

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>COOL-ADMIN 一个很酷的后台权限管理系统</title>
<meta name="keywords" content="cool-admin,后台管理系统,vue,element-ui,nodejs" />
<meta name="description" content="element-ui、midway.js、mysql、redis、node.js、前后端分离、权限管理、快速开发, COOL-AMIND 一个很酷的后台权限管理系统" />
<link rel="stylesheet" href="css/welcome.css">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<body>
<div class="reveal">HELLO COOL-ADMIN AI快速开发框架</div>
<!-- 添加底部说明 -->
<div class="footer-bar">
<span>当前版本:v8.x</span>
<div class="notice">
<span>本项目采用前后端分离架构,这是后端服务。</span>
<span>前端项目请访问:</span>
<a class="link" target="_blank" href="https://vue.cool-admin.com/">COOL-ADMIN 前端</a>
</div>
</div>
<script src="js/welcome.js"></script>
</body>
</html>

14
public/js/welcome.js

@ -0,0 +1,14 @@
const duration = 0.8;
const delay = 0.3;
// eslint-disable-next-line no-undef
const revealText = document.querySelector('.reveal');
const letters = revealText.textContent.split('');
revealText.textContent = '';
const middle = letters.filter(e => e !== ' ').length / 2;
letters.forEach((letter, i) => {
// eslint-disable-next-line no-undef
const span = document.createElement('span');
span.textContent = letter;
span.style.animationDelay = `${delay + Math.abs(i - middle) * 0.1}s`;
revealText.append(span);
});

202
public/swagger/LICENSE

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

2
public/swagger/NOTICE

@ -0,0 +1,2 @@
swagger-ui
Copyright 2020-2021 SmartBear Software Inc.

22
public/swagger/README.md

@ -0,0 +1,22 @@
# Swagger UI Dist
[![NPM version](https://badge.fury.io/js/swagger-ui-dist.svg)](http://badge.fury.io/js/swagger-ui-dist)
# API
This module, `swagger-ui-dist`, exposes Swagger-UI's entire dist folder as a dependency-free npm module.
Use `swagger-ui` instead, if you'd like to have npm install dependencies for you.
`SwaggerUIBundle` and `SwaggerUIStandalonePreset` can be imported:
```javascript
import { SwaggerUIBundle, SwaggerUIStandalonePreset } from "swagger-ui-dist"
```
To get an absolute path to this directory for static file serving, use the exported `getAbsoluteFSPath` method:
```javascript
const swaggerUiAssetPath = require("swagger-ui-dist").getAbsoluteFSPath()
// then instantiate server that serves files from the swaggerUiAssetPath
```
For anything else, check the [Swagger-UI](https://github.com/swagger-api/swagger-ui) repository.

14
public/swagger/absolute-path.js

@ -0,0 +1,14 @@
/*
* getAbsoluteFSPath
* @return {string} When run in NodeJS env, returns the absolute path to the current directory
* When run outside of NodeJS, will return an error message
*/
const getAbsoluteFSPath = function () {
// detect whether we are running in a browser or nodejs
if (typeof module !== "undefined" && module.exports) {
return require("path").resolve(__dirname)
}
throw new Error('getAbsoluteFSPath can only be called within a Nodejs environment');
}
module.exports = getAbsoluteFSPath

BIN
public/swagger/favicon-16x16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

BIN
public/swagger/favicon-32x32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

16
public/swagger/index.css

@ -0,0 +1,16 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}

19
public/swagger/index.html

@ -0,0 +1,19 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="index.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="./swagger-initializer.js" charset="UTF-8"> </script>
</body>
</html>

17
public/swagger/index.js

@ -0,0 +1,17 @@
try {
module.exports.SwaggerUIBundle = require("./swagger-ui-bundle.js")
module.exports.SwaggerUIStandalonePreset = require("./swagger-ui-standalone-preset.js")
} catch(e) {
// swallow the error if there's a problem loading the assets.
// allows this module to support providing the assets for browserish contexts,
// without exploding in a Node context.
//
// see https://github.com/swagger-api/swagger-ui/issues/3291#issuecomment-311195388
// for more information.
}
// `absolutePath` and `getAbsoluteFSPath` are both here because at one point,
// we documented having one and actually implemented the other.
// They were both retained so we don't break anyone's code.
module.exports.absolutePath = require("./absolute-path.js")
module.exports.getAbsoluteFSPath = require("./absolute-path.js")

79
public/swagger/oauth2-redirect.html

@ -0,0 +1,79 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>

18
public/swagger/package.json

@ -0,0 +1,18 @@
{
"name": "swagger-ui-dist",
"version": "5.10.0",
"main": "index.js",
"repository": "git@github.com:swagger-api/swagger-ui.git",
"contributors": [
"(in alphabetical order)",
"Anna Bodnia <anna.bodnia@gmail.com>",
"Buu Nguyen <buunguyen@gmail.com>",
"Josh Ponelat <jponelat@gmail.com>",
"Kyle Shockey <kyleshockey1@gmail.com>",
"Robert Barnwell <robert@robertismy.name>",
"Sahar Jafari <shr.jafari@gmail.com>"
],
"license": "Apache-2.0",
"dependencies": {},
"devDependencies": {}
}

20
public/swagger/swagger-initializer.js

@ -0,0 +1,20 @@
window.onload = function() {
//<editor-fold desc="Changeable Configuration Block">
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
url: "/swagger/json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
//</editor-fold>
};

3
public/swagger/swagger-ui-bundle.js

File diff suppressed because one or more lines are too long

1
public/swagger/swagger-ui-bundle.js.map

File diff suppressed because one or more lines are too long

3
public/swagger/swagger-ui-es-bundle-core.js

File diff suppressed because one or more lines are too long

1
public/swagger/swagger-ui-es-bundle-core.js.map

File diff suppressed because one or more lines are too long

3
public/swagger/swagger-ui-es-bundle.js

File diff suppressed because one or more lines are too long

1
public/swagger/swagger-ui-es-bundle.js.map

File diff suppressed because one or more lines are too long

3
public/swagger/swagger-ui-standalone-preset.js

File diff suppressed because one or more lines are too long

1
public/swagger/swagger-ui-standalone-preset.js.map

File diff suppressed because one or more lines are too long

3
public/swagger/swagger-ui.css

File diff suppressed because one or more lines are too long

1
public/swagger/swagger-ui.css.map

File diff suppressed because one or more lines are too long

2
public/swagger/swagger-ui.js

File diff suppressed because one or more lines are too long

1
public/swagger/swagger-ui.js.map

File diff suppressed because one or more lines are too long

66
src/comm/path.ts

@ -0,0 +1,66 @@
import * as path from 'path';
import * as os from 'os';
import * as md5 from 'md5';
import * as fs from 'fs';
/**
* keys
* @returns
*/
const getKeys = () => {
const configFile = path.join(__dirname, '../config/config.default.js');
const configContent = fs.readFileSync(configFile, 'utf8');
const keys = configContent.match(/keys: '([^']+)'/)?.[1];
return keys;
};
/**
*
* @returns
*/
export const pDataPath = () => {
const dirPath = path.join(os.homedir(), '.cool-admin', md5(getKeys()));
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
return dirPath;
};
/**
*
* @returns
*/
export const pUploadPath = () => {
const uploadPath = path.join(pDataPath(), 'upload');
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}
return uploadPath;
};
/**
*
* @returns
*/
export const pPluginPath = () => {
const pluginPath = path.join(pDataPath(), 'plugin');
if (!fs.existsSync(pluginPath)) {
fs.mkdirSync(pluginPath, { recursive: true });
}
return pluginPath;
};
/**
* sqlite
*/
export const pSqlitePath = () => {
return path.join(pDataPath(), 'cool.sqlite');
};
/**
*
* @returns
*/
export const pCachePath = () => {
return path.join(pDataPath(), 'cache');
};

51
src/comm/port.ts

@ -0,0 +1,51 @@
import { execSync } from 'child_process';
/**
*
* @param {number} port -
* @returns {boolean} -
*/
function isPortAvailableSync(port: number): boolean {
try {
if (process.platform === 'win32') {
// Windows 使用 netstat 检查端口,排除 TIME_WAIT 状态
const result = execSync(`netstat -ano | findstr :${port}`, {
encoding: 'utf-8',
});
// 如果端口只处于 TIME_WAIT 状态,则认为端口可用
return !result || result.toLowerCase().includes('time_wait');
} else {
// Linux/Mac 使用 lsof 检查端口,只检查 LISTEN 状态
const result = execSync(`lsof -i :${port} -sTCP:LISTEN`, {
encoding: 'utf-8',
});
return !result;
}
} catch (error) {
// 命令执行失败,端口可用
return true;
}
}
/**
*
* @param {number} startPort -
* @returns {number} -
*/
export function availablePort(startPort: number): number {
if (!process['pkg']) return startPort;
let port = startPort;
while (port <= 8010) {
if (isPortAvailableSync(port)) {
if (port !== startPort) {
console.warn(
'\x1b[33m%s\x1b[0m',
`Port ${startPort} is occupied, using port ${port}`
);
}
return port;
}
port++;
}
return 8001;
}

271
src/comm/utils.ts

@ -0,0 +1,271 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import * as moment from 'moment';
import * as path from 'path';
/**
*
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class Utils {
@Inject()
baseDir;
/**
* dist路径
*/
getDistPath() {
const runPath = __dirname;
const distIndex =
runPath.lastIndexOf('/dist/') !== -1
? runPath.lastIndexOf('/dist/')
: runPath.lastIndexOf('\\dist\\');
if (distIndex !== -1) {
return path.join(runPath.substring(0, distIndex), 'dist');
}
return path.join(runPath, 'dist');
}
/**
* IP
*/
async getReqIP(ctx: Context) {
const req = ctx.req;
return (
req.headers['x-forwarded-for'] ||
req.socket.remoteAddress?.replace('::ffff:', '') ||
''
);
}
/**
*
* @param obj
*/
async removeEmptyP(obj) {
Object.keys(obj).forEach(key => {
if (obj[key] === null || obj[key] === '' || obj[key] === 'undefined') {
delete obj[key];
}
});
}
/**
* 线
* @param ms
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
*
* @param recently
*/
getRecentlyDates(recently, format = 'YYYY-MM-DD') {
moment.locale('zh-cn');
const dates = [];
for (let i = 0; i < recently; i++) {
dates.push(moment().subtract(i, 'days').format(format));
}
return dates.reverse();
}
/**
*
* @param recently
*/
getRecentlyMonths(recently, format = 'YYYY-MM') {
moment.locale('zh-cn');
const dates = [];
const date = moment(Date.now()).format('YYYY-MM');
for (let i = 0; i < recently; i++) {
dates.push(moment(date).subtract(i, 'months').format(format));
}
return dates.reverse();
}
/**
*
* @param start
* @param end
*/
getBetweenDays(start, end, format = 'YYYY-MM-DD') {
moment.locale('zh-cn');
const dates = [];
const startTime = moment(start).format(format);
const endTime = moment(end).format(format);
const days = moment(endTime).diff(moment(startTime), 'days');
for (let i = 0; i <= days; i++) {
dates.push(moment(startTime).add(i, 'days').format(format));
}
return dates;
}
/**
*
* @param start
* @param end
*/
getBetweenMonths(start, end, format = 'YYYY-MM') {
moment.locale('zh-cn');
const dates = [];
const startTime = moment(start).format(format);
const endTime = moment(end).format(format);
const months = moment(endTime).diff(moment(startTime), 'months');
for (let i = 0; i <= months; i++) {
dates.push(moment(startTime).add(i, 'months').format(format));
}
return dates;
}
/**
*
* @param start
* @param end
*/
getBetweenHours(start, end, format = 'YYYY-MM-DD HH') {
moment.locale('zh-cn');
const dates = [];
const startTime = moment(start).format(format);
const endTime = moment(end).format(format);
const hours = moment(endTime).diff(moment(startTime), 'hours');
for (let i = 0; i <= hours; i++) {
dates.push(moment(startTime).add(i, 'hours').format(format));
}
return dates;
}
/**
*
* @param obj
* @returns
*/
toCamelCase(obj) {
const camelCaseObject = {};
for (const i in obj) {
const camelCase = i.replace(/([-_][a-z])/gi, $1 => {
return $1.toUpperCase().replace('-', '').replace('_', '');
});
camelCaseObject[camelCase] = obj[i];
}
return camelCaseObject;
}
/**
* URL
* @param pattern
* @param url
* @returns
*/
matchUrl(pattern, url) {
// 将 pattern 和 url 按 `/` 分割
const patternParts = pattern.split('/').filter(Boolean);
const urlParts = url.split('/').filter(Boolean);
// 如果长度不匹配且 pattern 不包含 **,直接返回 false
if (patternParts.length !== urlParts.length && !pattern.includes('**')) {
return false;
}
for (let i = 0; i < patternParts.length; i++) {
const patternPart = patternParts[i];
const urlPart = urlParts[i];
// 如果 patternPart 是 **,匹配剩余的所有部分
if (patternPart === '**') {
return true;
}
// 如果 patternPart 以 : 开头,说明是参数,直接匹配任意非空值
if (patternPart.startsWith(':')) {
if (!urlPart) {
return false;
}
continue;
}
// 如果 patternPart 是 *,匹配任意非空部分
if (patternPart === '*') {
if (!urlPart) {
return false;
}
} else if (patternPart !== urlPart) {
return false;
}
}
// 如果 pattern 和 url 的部分数量一致,则匹配成功
return patternParts.length === urlParts.length;
}
/**
* JSON
* @param {string} text - JSON
* @returns {Object|Array|null} - JSON JSON null
*/
extractJSONFromText(text) {
if (!text || typeof text !== 'string') {
return null;
}
try {
// 尝试直接解析整个文本
return JSON.parse(text);
} catch (e) {
// 整个文本不是有效的 JSON,尝试提取 JSON 部分
}
// 查找可能的 JSON 开始位置({ 或 [)
const possibleStarts = [];
for (let i = 0; i < text.length; i++) {
if (text[i] === '{' || text[i] === '[') {
possibleStarts.push(i);
}
}
// 从每个可能的起始位置尝试提取 JSON
for (const startIndex of possibleStarts) {
let openBraces = 0;
let openBrackets = 0;
let inString = false;
let escapeNext = false;
for (let i = startIndex; i < text.length; i++) {
const char = text[i];
// 处理转义字符
if (inString && !escapeNext && char === '\\') {
escapeNext = true;
continue;
}
// 处理字符串边界
if (!escapeNext && char === '"') {
inString = !inString;
}
if (!inString) {
// 只在不在字符串内部时才计算括号
if (char === '{') openBraces++;
else if (char === '}') openBraces--;
else if (char === '[') openBrackets++;
else if (char === ']') openBrackets--;
}
escapeNext = false;
// 检查是否找到了完整的 JSON 结构
if (
(openBraces === 0 && text[startIndex] === '{' && char === '}') ||
(openBrackets === 0 && text[startIndex] === '[' && char === ']')
) {
const jsonStr = text.substring(startIndex, i + 1);
try {
const result = JSON.parse(jsonStr);
return result;
} catch (e) {
// 这个候选 JSON 无效,继续尝试下一个
break;
}
}
}
}
return null; // 没有找到有效的 JSON
}
}

110
src/config/config.default.ts

@ -0,0 +1,110 @@
import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core';
import { CoolCacheStore } from '@cool-midway/core';
import * as path from 'path';
import { pCachePath, pUploadPath } from '../comm/path';
import { availablePort } from '../comm/port';
import * as fsStore from '@cool-midway/cache-manager-fs-hash';
import { createAdapter } from '@socket.io/redis-adapter';
// redis缓存
// import { redisStore } from 'cache-manager-ioredis-yet';
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 {
// 确保每个项目唯一,项目首次启动会自动生成
keys: '3457c0e5-f8c6-44dc-a4ed-155891420a33',
koa: {
port: availablePort(8001),
},
// 开启异步上下文管理
asyncContextManager: {
enable: true,
},
socketIO: {
upgrades: ['websocket'], // 可升级的协议
adapter: createAdapter(pubClient, subClient),
cors: {
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
},
},
// 静态文件配置
staticFile: {
buffer: true,
dirs: {
default: {
prefix: '/',
dir: path.join(__dirname, '..', '..', 'public'),
},
static: {
prefix: '/upload',
dir: pUploadPath(),
},
},
},
// 文件上传
upload: {
fileSize: '200mb',
whitelist: null,
},
// 缓存 可切换成其他缓存如:redis http://www.midwayjs.org/docs/extensions/caching
cacheManager: {
clients: {
default: {
store: CoolCacheStore,
options: {
path: pCachePath(),
ttl: 0,
},
},
},
},
// cacheManager: {
// clients: {
// default: {
// store: redisStore,
// options: {
// port: 6379,
// host: '127.0.0.1',
// password: '',
// ttl: 0,
// db: 0,
// },
// },
// },
// },
cool: {
// 已经插件化,本地文件上传查看 plugin/config.ts,其他云存储查看对应插件的使用
file: {},
// 是否开启多租户
tenant: {
// 是否开启多租户
enable: false,
// 需要过滤多租户的url, 支持通配符, 如/admin/**/* 表示admin模块下的所有接口都进行多租户过滤
urls: [],
},
// 国际化配置
i18n: {
// 是否开启
enable: false,
// 语言
languages: ['zh-cn', 'zh-tw', 'en'],
},
// crud配置
crud: {
// 插入模式,save不会校验字段(允许传入不存在的字段),insert会校验字段
upsert: 'save',
// 软删除
softDelete: true,
},
} as CoolConfig,
} as MidwayConfig;

43
src/config/config.local.ts

@ -0,0 +1,43 @@
import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core';
import { TenantSubscriber } from '../modules/base/db/tenant';
/**
* npm run dev
*/
export default {
typeorm: {
dataSource: {
default: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: '123456',
database: 'node_cool',
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
synchronize: true,
// 打印日志
logging: false,
// 字符集
charset: 'utf8mb4',
// 是否开启缓存
cache: true,
// 实体路径
entities: ['**/modules/*/entity'],
// 订阅者
subscribers: [TenantSubscriber],
},
},
},
cool: {
// 实体与路径,跟生成代码、前端请求、swagger文档相关 注意:线上不建议开启,以免暴露敏感信息
eps: true,
// 是否自动导入模块数据库
initDB: true,
// 判断是否初始化的方式
initJudge: 'db',
// 是否自动导入模块菜单
initMenu: true,
} as CoolConfig,
} as MidwayConfig;

44
src/config/config.prod.ts

@ -0,0 +1,44 @@
import { CoolConfig } from '@cool-midway/core';
import { MidwayConfig } from '@midwayjs/core';
import { entities } from '../entities';
import { TenantSubscriber } from '../modules/base/db/tenant';
/**
* npm run prod
*/
export default {
typeorm: {
dataSource: {
default: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: '123456',
database: 'cool',
// 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
synchronize: false,
// 打印日志
logging: false,
// 字符集
charset: 'utf8mb4',
// 是否开启缓存
cache: true,
// 实体路径
entities,
// 订阅者
subscribers: [TenantSubscriber],
},
},
},
cool: {
// 实体与路径,跟生成代码、前端请求、swagger文档相关 注意:线上不建议开启,以免暴露敏感信息
eps: false,
// 是否自动导入模块数据库
initDB: false,
// 判断是否初始化的方式
initJudge: 'db',
// 是否自动导入模块菜单
initMenu: false,
} as CoolConfig,
} as MidwayConfig;

73
src/configuration.ts

@ -0,0 +1,73 @@
import * as orm from '@midwayjs/typeorm';
import {
Configuration,
App,
IMidwayApplication,
Inject,
ILogger,
MidwayWebRouterService,
} from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
// import * as crossDomain from '@midwayjs/cross-domain';
import * as validate from '@midwayjs/validate';
import * as info from '@midwayjs/info';
import * as staticFile from '@midwayjs/static-file';
import * as cron from '@midwayjs/cron';
import * as DefaultConfig from './config/config.default';
import * as LocalConfig from './config/config.local';
import * as ProdConfig from './config/config.prod';
import * as cool from '@cool-midway/core';
import * as upload from '@midwayjs/upload';
import * as socketio from '@midwayjs/socketio';
// import * as task from '@cool-midway/task';
// import * as rpc from '@cool-midway/rpc';
@Configuration({
imports: [
// https://koajs.com/
koa,
// 是否开启跨域(注:顺序不能乱放!!!) http://www.midwayjs.org/docs/extensions/cross_domain
// crossDomain,
// 静态文件托管 https://midwayjs.org/docs/extensions/static_file
staticFile,
// orm https://midwayjs.org/docs/extensions/orm
orm,
// 参数验证 https://midwayjs.org/docs/extensions/validate
validate,
// 本地任务 http://www.midwayjs.org/docs/extensions/cron
cron,
// 文件上传
upload,
// cool-admin 官方组件 https://cool-js.com
cool,
// rpc 微服务 远程调用
// rpc,
// 任务与队列
// task,
// socketio http://www.midwayjs.org/docs/extensions/socketio
socketio,
{
component: info,
enabledEnvironment: ['local', 'prod'],
},
],
importConfigs: [
{
default: DefaultConfig,
local: LocalConfig,
prod: ProdConfig,
},
],
})
export class MainConfiguration {
@App()
app: IMidwayApplication;
@Inject()
webRouterService: MidwayWebRouterService;
@Inject()
logger: ILogger;
async onReady() {}
}

2
src/entities.ts

@ -0,0 +1,2 @@
// 自动生成的文件,请勿手动修改
export const entities = [];

6
src/interface.ts

@ -0,0 +1,6 @@
/**
* @description User-Service parameters
*/
export interface IUserOptions {
uid: number;
}

40
src/modules/base/config.ts

@ -0,0 +1,40 @@
import { BaseLogMiddleware } from './middleware/log';
import { BaseAuthorityMiddleware } from './middleware/authority';
import { ModuleConfig } from '@cool-midway/core';
import { BaseTranslateMiddleware } from './middleware/translate';
/**
*
*/
export default () => {
return {
// 模块名称
name: '权限管理',
// 模块描述
description: '基础的权限管理功能,包括登录,权限校验',
// 中间件
globalMiddlewares: [
BaseTranslateMiddleware,
BaseAuthorityMiddleware,
BaseLogMiddleware,
],
// 模块加载顺序,默认为0,值越大越优先加载
order: 10,
// app参数配置允许读取的key
allowKeys: [],
// jwt 生成解密token的
jwt: {
// 单点登录
sso: false,
// 注意: 最好重新修改,防止破解
secret: '10199b2b-bb59-48c9-9a97-e63521071485',
// token
token: {
// 2小时过期,需要用刷新token
expire: 2 * 3600,
// 15天内,如果没操作过就需要重新登录
refreshExpire: 24 * 3600 * 15,
},
},
} as ModuleConfig;
};

29
src/modules/base/controller/admin/coding.ts

@ -0,0 +1,29 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { Body, Get, Inject, Post } from '@midwayjs/core';
import { BaseCodingService } from '../../service/coding';
/**
* Ai编码
*/
@CoolController()
export class AdminCodingController extends BaseController {
@Inject()
baseCodingService: BaseCodingService;
@Get('/getModuleTree', { summary: '获取模块目录结构' })
async getModuleTree() {
return this.ok(await this.baseCodingService.getModuleTree());
}
@Post('/createCode', { summary: '创建代码' })
async createCode(
@Body('codes')
codes: {
path: string;
content: string;
}[]
) {
this.baseCodingService.createCode(codes);
return this.ok();
}
}

99
src/modules/base/controller/admin/comm.ts

@ -0,0 +1,99 @@
import {
BaseController,
CoolController,
CoolTag,
CoolUrlTag,
TagTypes,
} from '@cool-midway/core';
import { ALL, Body, Get, Inject, Post, Provide } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { PluginService } from '../../../plugin/service/info';
import { BaseSysUserEntity } from '../../entity/sys/user';
import { BaseSysLoginService } from '../../service/sys/login';
import { BaseSysPermsService } from '../../service/sys/perms';
import { BaseSysUserService } from '../../service/sys/user';
/**
* Base
*/
@CoolUrlTag()
@Provide()
@CoolController()
export class BaseCommController extends BaseController {
@Inject()
baseSysUserService: BaseSysUserService;
@Inject()
baseSysPermsService: BaseSysPermsService;
@Inject()
baseSysLoginService: BaseSysLoginService;
@Inject()
ctx: Context;
@Inject()
pluginService: PluginService;
/**
*
*/
@Get('/person', { summary: '个人信息' })
async person() {
return this.ok(
await this.baseSysUserService.person(this.ctx.admin?.userId)
);
}
/**
*
*/
@Post('/personUpdate', { summary: '修改个人信息' })
async personUpdate(@Body(ALL) user: BaseSysUserEntity) {
await this.baseSysUserService.personUpdate(user);
return this.ok();
}
/**
*
*/
@Get('/permmenu', { summary: '权限与菜单' })
async permmenu() {
return this.ok(
await this.baseSysPermsService.permmenu(this.ctx.admin.roleIds)
);
}
/**
*
*/
@Post('/upload', { summary: '文件上传' })
async upload() {
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.upload(this.ctx));
}
/**
*
*/
@Get('/uploadMode', { summary: '文件上传模式' })
async uploadMode() {
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.getMode());
}
/**
* 退
*/
@Post('/logout', { summary: '退出' })
async logout() {
await this.baseSysLoginService.logout();
return this.ok();
}
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/program', { summary: '编程' })
async program() {
return this.ok('Node');
}
}

98
src/modules/base/controller/admin/open.ts

@ -0,0 +1,98 @@
import { Provide, Body, Inject, Post, Get, Query } from '@midwayjs/core';
import {
CoolController,
BaseController,
CoolEps,
CoolUrlTag,
CoolTag,
TagTypes,
RESCODE,
} from '@cool-midway/core';
import { LoginDTO } from '../../dto/login';
import { BaseSysLoginService } from '../../service/sys/login';
import { BaseSysParamService } from '../../service/sys/param';
import { Context } from '@midwayjs/koa';
import { Validate } from '@midwayjs/validate';
/**
*
*/
@Provide()
@CoolController({ description: '开放接口' })
@CoolUrlTag()
export class BaseOpenController extends BaseController {
@Inject()
baseSysLoginService: BaseSysLoginService;
@Inject()
baseSysParamService: BaseSysParamService;
@Inject()
ctx: Context;
@Inject()
eps: CoolEps;
/**
*
* @returns
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/eps', { summary: '实体信息与路径' })
public async getEps() {
return this.ok(this.eps.admin);
}
/**
* key获得网页内容()
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/html', { summary: '获得网页内容的参数值' })
async htmlByKey(@Query('key') key: string) {
this.ctx.body = await this.baseSysParamService.htmlByKey(key);
}
/**
*
* @param login
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Post('/login', { summary: '登录' })
@Validate()
async login(@Body() login: LoginDTO) {
return this.ok(await this.baseSysLoginService.login(login));
}
/**
*
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/captcha', { summary: '验证码' })
async captcha(
@Query('width') width: number,
@Query('height') height: number,
@Query('color') color: string
) {
return this.ok(
await this.baseSysLoginService.captcha(width, height, color)
);
}
/**
* token
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/refreshToken', { summary: '刷新token' })
async refreshToken(@Query('refreshToken') refreshToken: string) {
try {
const token = await this.baseSysLoginService.refreshToken(refreshToken);
return this.ok(token);
} catch (e) {
this.ctx.status = 401;
this.ctx.body = {
code: RESCODE.COMMFAIL,
message: '登录失效~',
};
}
}
}

32
src/modules/base/controller/admin/sys/department.ts

@ -0,0 +1,32 @@
import { ALL, Body, Inject, Post, Provide } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysDepartmentEntity } from '../../../entity/sys/department';
import { BaseSysDepartmentService } from '../../../service/sys/department';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'list'],
entity: BaseSysDepartmentEntity,
service: BaseSysDepartmentService,
insertParam: ctx => {
return {
userId: ctx.admin.userId,
};
},
})
export class BaseDepartmentController extends BaseController {
@Inject()
baseDepartmentService: BaseSysDepartmentService;
/**
*
*/
@Post('/order', { summary: '排序' })
async order(@Body(ALL) params: any) {
await this.baseDepartmentService.order(params);
return this.ok();
}
}

64
src/modules/base/controller/admin/sys/log.ts

@ -0,0 +1,64 @@
import { Provide, Post, Inject, Body, Get } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysLogEntity } from '../../../entity/sys/log';
import { BaseSysUserEntity } from '../../../entity/sys/user';
import { BaseSysConfService } from '../../../service/sys/conf';
import { BaseSysLogService } from '../../../service/sys/log';
/**
*
*/
@Provide()
@CoolController({
api: ['page'],
entity: BaseSysLogEntity,
urlTag: {
name: 'a',
url: ['add'],
},
pageQueryOp: {
keyWordLikeFields: ['b.name', 'a.action', 'a.ip'],
select: ['a.*', 'b.name'],
join: [
{
entity: BaseSysUserEntity,
alias: 'b',
condition: 'a.userId = b.id',
type: 'leftJoin',
},
],
},
})
export class BaseSysLogController extends BaseController {
@Inject()
baseSysLogService: BaseSysLogService;
@Inject()
baseSysConfService: BaseSysConfService;
/**
*
*/
@Post('/clear', { summary: '清理' })
public async clear() {
await this.baseSysLogService.clear(true);
return this.ok();
}
/**
*
*/
@Post('/setKeep', { summary: '日志保存时间' })
public async setKeep(@Body('value') value: number) {
await this.baseSysConfService.updateVaule('logKeep', value);
return this.ok();
}
/**
*
*/
@Get('/getKeep', { summary: '获得日志保存时间' })
public async getKeep() {
return this.ok(await this.baseSysConfService.getValue('logKeep'));
}
}

46
src/modules/base/controller/admin/sys/menu.ts

@ -0,0 +1,46 @@
import { Body, Inject, Post, Provide } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysMenuEntity } from '../../../entity/sys/menu';
import { BaseSysMenuService } from '../../../service/sys/menu';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: BaseSysMenuEntity,
service: BaseSysMenuService,
})
export class BaseSysMenuController extends BaseController {
@Inject()
baseSysMenuService: BaseSysMenuService;
@Post('/parse', { summary: '解析' })
async parse(
@Body('entity') entity: string,
@Body('controller') controller: string,
@Body('module') module: string
) {
return this.ok(
await this.baseSysMenuService.parse(entity, controller, module)
);
}
@Post('/create', { summary: '创建代码' })
async create(@Body() body) {
await this.baseSysMenuService.create(body);
return this.ok();
}
@Post('/export', { summary: '导出' })
async export(@Body('ids') ids: number[]) {
return this.ok(await this.baseSysMenuService.export(ids));
}
@Post('/import', { summary: '导入' })
async import(@Body('menus') menus: any[]) {
await this.baseSysMenuService.import(menus);
return this.ok();
}
}

34
src/modules/base/controller/admin/sys/param.ts

@ -0,0 +1,34 @@
import { Get, Inject, Provide, Query } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysParamEntity } from '../../../entity/sys/param';
import { BaseSysParamService } from '../../../service/sys/param';
import { Context } from '@midwayjs/koa';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'page'],
entity: BaseSysParamEntity,
service: BaseSysParamService,
pageQueryOp: {
keyWordLikeFields: ['name', 'keyName'],
fieldEq: ['dataType'],
},
})
export class BaseSysParamController extends BaseController {
@Inject()
baseSysParamService: BaseSysParamService;
@Inject()
ctx: Context;
/**
* key获得网页内容()
*/
@Get('/html', { summary: '获得网页内容的参数值' })
async htmlByKey(@Query('key') key: string) {
this.ctx.body = await this.baseSysParamService.htmlByKey(key);
}
}

38
src/modules/base/controller/admin/sys/role.ts

@ -0,0 +1,38 @@
import { Provide } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { Context } from 'vm';
import { BaseSysRoleEntity } from '../../../entity/sys/role';
import { BaseSysRoleService } from '../../../service/sys/role';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: BaseSysRoleEntity,
service: BaseSysRoleService,
// 新增的时候插入当前用户ID
insertParam: async (ctx: Context) => {
return {
userId: ctx.admin.userId,
};
},
pageQueryOp: {
keyWordLikeFields: ['a.name', 'a.label'],
where: async (ctx: Context) => {
const { userId, roleIds, username } = ctx.admin;
return [
// 超级管理员的角色不展示
['a.label != :label', { label: 'admin' }],
// 如果不是超管,只能看到自己新建的或者自己有的角色
[
`(a.userId=:userId or a.id in (${roleIds.join(',')}))`,
{ userId },
username !== 'admin',
],
];
},
},
})
export class BaseSysRoleController extends BaseController {}

35
src/modules/base/controller/admin/sys/user.ts

@ -0,0 +1,35 @@
import { Body, Inject, Post, Provide } from '@midwayjs/core';
import { CoolController, BaseController } from '@cool-midway/core';
import { BaseSysUserEntity } from '../../../entity/sys/user';
import { BaseSysUserService } from '../../../service/sys/user';
/**
*
*/
@Provide()
@CoolController({
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
entity: BaseSysUserEntity,
service: BaseSysUserService,
insertParam: ctx => {
return {
userId: ctx.admin.userId,
};
},
})
export class BaseSysUserController extends BaseController {
@Inject()
baseSysUserService: BaseSysUserService;
/**
*
*/
@Post('/move', { summary: '移动部门' })
async move(
@Body('departmentId') departmentId: number,
@Body('userIds') userIds: []
) {
await this.baseSysUserService.move(departmentId, userIds);
return this.ok();
}
}

1
src/modules/base/controller/app/README.md

@ -0,0 +1 @@
这里写对外的api接口

72
src/modules/base/controller/app/comm.ts

@ -0,0 +1,72 @@
import { Provide, Inject, Get, Post, Query, Config } from '@midwayjs/core';
import {
CoolController,
BaseController,
CoolEps,
TagTypes,
CoolUrlTag,
CoolTag,
} from '@cool-midway/core';
import { Context } from '@midwayjs/koa';
import { BaseSysParamService } from '../../service/sys/param';
import { PluginService } from '../../../plugin/service/info';
/**
*
*/
@CoolUrlTag()
@Provide()
@CoolController()
export class BaseAppCommController extends BaseController {
@Inject()
pluginService: PluginService;
@Inject()
ctx: Context;
@Config('module.base.allowKeys')
allowKeys: string[];
@Inject()
eps: CoolEps;
@Inject()
baseSysParamService: BaseSysParamService;
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/param', { summary: '参数配置' })
async param(@Query('key') key: string) {
if (!this.allowKeys.includes(key)) {
return this.fail('非法操作');
}
return this.ok(await this.baseSysParamService.dataByKey(key));
}
/**
*
* @returns
*/
@CoolTag(TagTypes.IGNORE_TOKEN)
@Get('/eps', { summary: '实体信息与路径' })
public async getEps() {
return this.ok(this.eps.app);
}
/**
*
*/
@Post('/upload', { summary: '文件上传' })
async upload() {
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.upload(this.ctx));
}
/**
*
*/
@Get('/uploadMode', { summary: '文件上传模式' })
async uploadMode() {
const file = await this.pluginService.getInstance('upload');
return this.ok(await file.getMode());
}
}

102
src/modules/base/db.json

@ -0,0 +1,102 @@
{
"base_sys_param": [
{
"keyName": "rich",
"name": "富文本参数",
"data": "<h3><strong>这是一个富文本</strong></h3><p>xxx</p><p>xxxxxxxxxx</p><p><br></p>",
"dataType": 1,
"remark": null
},
{
"keyName": "json",
"name": "JSON参数",
"data": "{\n \"code\": 111233\n}",
"dataType": 0,
"remark": null
},
{
"keyName": "file",
"name": "文件",
"data": "",
"dataType": 2,
"remark": null
},
{
"keyName": "text",
"name": "测试",
"data": "这是一段字符串",
"dataType": 0,
"remark": null
}
],
"base_sys_conf": [
{
"cKey": "logKeep",
"cValue": "31"
},
{
"cKey": "recycleKeep",
"cValue": "31"
}
],
"base_sys_department": [
{
"id": 1,
"name": "COOL",
"parentId": null,
"orderNum": 0
},
{
"id": 11,
"name": "开发",
"parentId": 12,
"orderNum": 2
},
{
"id": 12,
"name": "测试",
"parentId": 1,
"orderNum": 1
},
{
"id": 13,
"name": "游客",
"parentId": 1,
"orderNum": 3
}
],
"base_sys_role": [
{
"id": 1,
"userId": "1",
"name": "超管",
"label": "admin",
"remark": "最高权限的角色",
"relevance": 1,
"menuIdList": "null",
"departmentIdList": "null"
}
],
"base_sys_user": [
{
"id": 1,
"departmentId": 1,
"name": "超级管理员",
"username": "admin",
"password": "e10adc3949ba59abbe56e057f20f883e",
"passwordV": 7,
"nickName": "管理员",
"phone": "18000000000",
"email": "team@cool-js.com",
"status": 1,
"remark": "拥有最高权限的用户",
"socketId": null
}
],
"base_sys_user_role": [
{
"userId": 1,
"roleId": 1
}
]
}

213
src/modules/base/db/tenant.ts

@ -0,0 +1,213 @@
import { EventSubscriberModel } from '@midwayjs/typeorm';
import {
DeleteQueryBuilder,
EntitySubscriberInterface,
InsertQueryBuilder,
SelectQueryBuilder,
UpdateQueryBuilder,
} from 'typeorm';
import * as _ from 'lodash';
import {
App,
ASYNC_CONTEXT_KEY,
ASYNC_CONTEXT_MANAGER_KEY,
AsyncContextManager,
Config,
IMidwayApplication,
IMidwayContext,
Inject,
} from '@midwayjs/core';
import { Utils } from '../../../comm/utils';
import { CoolUrlTagData, TagTypes } from '@cool-midway/core';
/**
*
* @param ctx
* @param func
*/
export const noTenant = async (ctx, func) => {
let result;
const tenantId = ctx?.admin?.tenantId;
if (tenantId) {
ctx.admin.tenantId = null;
result = await func();
ctx.admin.tenantId = tenantId;
} else {
result = await func();
}
return result;
};
@EventSubscriberModel()
export class TenantSubscriber implements EntitySubscriberInterface<any> {
@App()
app: IMidwayApplication;
@Inject()
ctx: IMidwayContext;
@Inject()
coolUrlTagData: CoolUrlTagData;
@Config('cool.tenant')
tenant: {
// 是否开启多租户
enable: boolean;
// 需要过滤多租户的url
urls: string[];
};
// 系统接口不过滤
ignoreUrls = [
'/admin/base/open/login',
'/admin/base/comm/person',
'/admin/base/comm/permmenu',
'/admin/dict/info/data',
];
// 不进行租户过滤的用户
ignoreUsername = [];
@Inject()
utils: Utils;
/**
* url
*/
getAllIgnoreUrls() {
const adminIgnoreUrls = this.coolUrlTagData.byKey(
TagTypes.IGNORE_TOKEN,
'admin'
);
const appIgnoreUrls = this.coolUrlTagData.byKey(
TagTypes.IGNORE_TOKEN,
'app'
);
this.ignoreUrls = [
...this.ignoreUrls,
...adminIgnoreUrls,
...appIgnoreUrls,
];
// 去重
this.ignoreUrls = _.uniq(this.ignoreUrls);
return this.ignoreUrls;
}
/**
*
*/
checkHandler() {
const ctx = this.getCtx();
if (!ctx) return false;
const url = ctx?.url;
if (!url) return false;
if (this.tenant?.enable) {
const isNeedTenant = this.tenant.urls.some(pattern =>
this.utils.matchUrl(pattern, url)
);
return isNeedTenant;
}
return false;
}
/**
* ctx
*/
getCtx(): any {
try {
const contextManager: AsyncContextManager = this.app
.getApplicationContext()
.get(ASYNC_CONTEXT_MANAGER_KEY);
return contextManager.active().getValue(ASYNC_CONTEXT_KEY);
} catch (error) {
return null;
}
}
/**
* ID
* @returns string | undefined
*/
getTenantId(): number | undefined {
let ctx, url, tenantId;
ctx = this.getCtx();
if (!ctx || !this.checkHandler()) return undefined;
url = ctx?.url;
// 忽略用户
if (this.ignoreUsername.includes(ctx?.admin?.username)) {
return undefined;
}
// 忽略系统接口
if (
this.getAllIgnoreUrls().some(pattern => this.utils.matchUrl(pattern, url))
) {
return undefined;
}
if (_.startsWith(url, '/admin/')) {
tenantId = ctx?.admin?.tenantId;
} else if (_.startsWith(url, '/app/')) {
tenantId = ctx?.user?.tenantId;
}
if (tenantId && url) {
return tenantId;
}
return undefined;
}
/**
* ID条件
* @param queryBuilder
*/
afterSelectQueryBuilder(queryBuilder: SelectQueryBuilder<any>) {
if (!this.tenant?.enable) return;
const tenantId = this.getTenantId();
if (tenantId) {
queryBuilder.andWhere(
`${
queryBuilder.alias ? queryBuilder.alias + '.' : ''
}tenantId = '${tenantId}'`
);
}
}
/**
* ID
* @param queryBuilder
*/
afterInsertQueryBuilder(queryBuilder: InsertQueryBuilder<any>) {
if (!this.tenant?.enable) return;
const tenantId = this.getTenantId();
if (tenantId) {
const values = queryBuilder.expressionMap.valuesSet;
if (Array.isArray(values)) {
queryBuilder.values(values.map(item => ({ ...item, tenantId })));
} else if (typeof values === 'object') {
queryBuilder.values({ ...values, tenantId });
}
}
}
/**
* ID和条件
* @param queryBuilder
*/
afterUpdateQueryBuilder(queryBuilder: UpdateQueryBuilder<any>) {
if (!this.tenant?.enable) return;
const tenantId = this.getTenantId();
if (tenantId) {
queryBuilder.andWhere(`tenantId = '${tenantId}'`);
}
}
/**
* ID和条件
* @param queryBuilder
*/
afterDeleteQueryBuilder(queryBuilder: DeleteQueryBuilder<any>) {
if (!this.tenant?.enable) return;
const tenantId = this.getTenantId();
if (tenantId) {
queryBuilder.andWhere(`tenantId = '${tenantId}'`);
}
}
}

21
src/modules/base/dto/login.ts

@ -0,0 +1,21 @@
import { Rule, RuleType } from '@midwayjs/validate';
/**
*
*/
export class LoginDTO {
// 用户名
@Rule(RuleType.string().required())
username: string;
// 密码
@Rule(RuleType.string().required())
password: string;
// 验证码ID
@Rule(RuleType.string().required())
captchaId: string;
// 验证码
@Rule(RuleType.required())
verifyCode: number;
}

65
src/modules/base/entity/base.ts

@ -0,0 +1,65 @@
import { Index, PrimaryGeneratedColumn, Column } from 'typeorm';
import * as moment from 'moment';
import { CoolBaseEntity } from '@cool-midway/core';
/**
*
*/
export const transformerTime = {
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;
},
};
/**
* Json转换器
*/
export const transformerJson = {
to: value => value,
from: value => {
// 确保从数据库返回的是对象
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch (e) {
return value;
}
}
return value;
},
};
/**
*
*/
export abstract class BaseEntity extends CoolBaseEntity {
// 默认自增
@PrimaryGeneratedColumn('increment', {
comment: 'ID',
})
id: number;
@Index()
@Column({
comment: '创建时间',
type: 'varchar',
transformer: transformerTime,
})
createTime: Date;
@Index()
@Column({
comment: '更新时间',
type: 'varchar',
transformer: transformerTime,
})
updateTime: Date;
@Index()
@Column({ comment: '租户ID', nullable: true })
tenantId: number;
}

15
src/modules/base/entity/sys/conf.ts

@ -0,0 +1,15 @@
import { Column, Index, Entity } from 'typeorm';
import { BaseEntity } from '../base';
/**
*
*/
@Entity('base_sys_conf')
export class BaseSysConfEntity extends BaseEntity {
@Index({ unique: true })
@Column({ comment: '配置键' })
cKey: string;
@Column({ comment: '配置值' })
cValue: string;
}

23
src/modules/base/entity/sys/department.ts

@ -0,0 +1,23 @@
import { BaseEntity } from '../base';
import { Column, Entity, Index } from 'typeorm';
/**
*
*/
@Entity('base_sys_department')
export class BaseSysDepartmentEntity extends BaseEntity {
@Column({ comment: '部门名称' })
name: string;
@Index()
@Column({ comment: '创建者ID', nullable: true })
userId: number;
@Column({ comment: '上级部门ID', nullable: true })
parentId: number;
@Column({ comment: '排序', default: 0 })
orderNum: number;
// 父菜单名称
parentName: string;
}

28
src/modules/base/entity/sys/log.ts

@ -0,0 +1,28 @@
import { BaseEntity, transformerJson } from '../base';
import { Column, Index, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_log')
export class BaseSysLogEntity extends BaseEntity {
@Index()
@Column({ comment: '用户ID', nullable: true })
userId: number;
@Index()
@Column({ comment: '行为' })
action: string;
@Index()
@Column({ comment: 'ip', nullable: true })
ip: string;
@Column({
comment: '参数',
nullable: true,
type: 'json',
transformer: transformerJson,
})
params: string;
}

47
src/modules/base/entity/sys/menu.ts

@ -0,0 +1,47 @@
import { BaseEntity } from '../base';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_menu')
export class BaseSysMenuEntity extends BaseEntity {
@Column({ comment: '父菜单ID', nullable: true })
parentId: number;
@Column({ comment: '菜单名称' })
name: string;
@Column({ comment: '菜单地址', nullable: true })
router: string;
@Column({ comment: '权限标识', type: 'text', nullable: true })
perms: string;
@Column({
comment: '类型 0-目录 1-菜单 2-按钮',
default: 0,
})
type: number;
@Column({ comment: '图标', nullable: true })
icon: string;
@Column({ comment: '排序', default: 0 })
orderNum: number;
@Column({ comment: '视图地址', nullable: true })
viewPath: string;
@Column({ comment: '路由缓存', default: true })
keepAlive: boolean;
@Column({ comment: '是否显示', default: true })
isShow: boolean;
// 父菜单名称
parentName: string;
// 子菜单
childMenus: any;
}

27
src/modules/base/entity/sys/param.ts

@ -0,0 +1,27 @@
import { BaseEntity } from '../base';
import { Column, Index, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_param')
export class BaseSysParamEntity extends BaseEntity {
@Index({ unique: true })
@Column({ comment: '键' })
keyName: string;
@Column({ comment: '名称' })
name: string;
@Column({ comment: '数据', type: 'text' })
data: string;
@Column({
comment: '数据类型 0-字符串 1-富文本 2-文件 ',
default: 0,
})
dataType: number;
@Column({ comment: '备注', nullable: true })
remark: string;
}

31
src/modules/base/entity/sys/role.ts

@ -0,0 +1,31 @@
import { BaseEntity, transformerJson } from '../base';
import { Column, Index, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_role')
export class BaseSysRoleEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: string;
@Index({ unique: true })
@Column({ comment: '名称' })
name: string;
@Index({ unique: true })
@Column({ comment: '角色标签', nullable: true, length: 50 })
label: string;
@Column({ comment: '备注', nullable: true })
remark: string;
@Column({ comment: '数据权限是否关联上下级', default: false })
relevance: boolean;
@Column({ comment: '菜单权限', type: 'json', transformer: transformerJson })
menuIdList: number[];
@Column({ comment: '部门权限', type: 'json', transformer: transformerJson })
departmentIdList: number[];
}

14
src/modules/base/entity/sys/role_department.ts

@ -0,0 +1,14 @@
import { BaseEntity } from '../base';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_role_department')
export class BaseSysRoleDepartmentEntity extends BaseEntity {
@Column({ comment: '角色ID' })
roleId: number;
@Column({ comment: '部门ID' })
departmentId: number;
}

14
src/modules/base/entity/sys/role_menu.ts

@ -0,0 +1,14 @@
import { BaseEntity } from '../base';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_role_menu')
export class BaseSysRoleMenuEntity extends BaseEntity {
@Column({ comment: '角色ID' })
roleId: number;
@Column({ comment: '菜单ID' })
menuId: number;
}

58
src/modules/base/entity/sys/user.ts

@ -0,0 +1,58 @@
import { BaseEntity } from '../base';
import { Column, Index, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_user')
export class BaseSysUserEntity extends BaseEntity {
@Index()
@Column({ comment: '部门ID', nullable: true })
departmentId: number;
@Index()
@Column({ comment: '创建者ID', nullable: true })
userId: number;
@Column({ comment: '姓名', nullable: true })
name: string;
@Index({ unique: true })
@Column({ comment: '用户名', length: 100 })
username: string;
@Column({ comment: '密码' })
password: string;
@Column({
comment: '密码版本, 作用是改完密码,让原来的token失效',
default: 1,
})
passwordV: number;
@Column({ comment: '昵称', nullable: true })
nickName: string;
@Column({ comment: '头像', nullable: true })
headImg: string;
@Index()
@Column({ comment: '手机', nullable: true, length: 20 })
phone: string;
@Column({ comment: '邮箱', nullable: true })
email: string;
@Column({ comment: '备注', nullable: true })
remark: string;
@Column({ comment: '状态 0-禁用 1-启用', default: 1 })
status: number;
// 部门名称
departmentName: string;
// 角色ID列表
roleIdList: number[];
@Column({ comment: 'socketId', nullable: true })
socketId: string;
}

14
src/modules/base/entity/sys/user_role.ts

@ -0,0 +1,14 @@
import { BaseEntity } from '../base';
import { Column, Entity } from 'typeorm';
/**
*
*/
@Entity('base_sys_user_role')
export class BaseSysUserRoleEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '角色ID' })
roleId: number;
}

45
src/modules/base/event/app.ts

@ -0,0 +1,45 @@
import { CoolEvent, Event } from '@cool-midway/core';
import { App, ILogger, IMidwayApplication, Inject } from '@midwayjs/core';
/**
*
*/
@CoolEvent()
export class BaseAppEvent {
@App()
app: IMidwayApplication;
@Inject()
logger: ILogger;
@Event('onServerReady')
async onServerReady() {
if (!process['pkg']) return;
const port = this.app.getConfig('koa.port') || 8001;
this.logger.info(`Server is running at http://127.0.0.1:${port}`);
const url = `http://127.0.0.1:${port}`;
// 使用 child_process 打开浏览器
const { exec } = require('child_process');
let command;
switch (process.platform) {
case 'darwin': // macOS
command = `open ${url}`;
break;
case 'win32': // Windows
command = `start ${url}`;
break;
default: // Linux
command = `xdg-open ${url}`;
break;
}
console.log('url=>', url);
exec(command, (error: any) => {
if (!error) {
this.logger.info(`Application has opened in browser at ${url}`);
}
});
}
}

50
src/modules/base/event/menu.ts

@ -0,0 +1,50 @@
import { CoolEvent, CoolEventManager, Event } from '@cool-midway/core';
import { BaseSysMenuService } from '../service/sys/menu';
import {
App,
ILogger,
IMidwayApplication,
Inject,
Logger,
} from '@midwayjs/core';
import { BaseTranslateService } from '../service/translate';
/**
*
*/
@CoolEvent()
export class BaseMenuEvent {
@Logger()
coreLogger: ILogger;
@Inject()
baseSysMenuService: BaseSysMenuService;
@App()
app: IMidwayApplication;
@Inject()
coolEventManager: CoolEventManager;
@Inject()
baseTranslateService: BaseTranslateService;
@Event('onMenuImport')
async onMenuImport(datas) {
for (const module in datas) {
await this.baseSysMenuService.import(datas[module]);
this.coreLogger.info(
'\x1B[36m [cool:module:base] midwayjs cool module base import [' +
module +
'] module menu success \x1B[0m'
);
}
this.coolEventManager.emit('onMenuInit', {});
this.baseTranslateService.check();
}
@Event('onServerReady')
async onServerReady() {
this.baseTranslateService.loadTranslations();
}
}

25
src/modules/base/job/log.ts

@ -0,0 +1,25 @@
import { Job, IJob } from '@midwayjs/cron';
import { FORMAT, ILogger, Inject } from '@midwayjs/core';
import { BaseSysLogService } from '../service/sys/log';
/**
*
*/
@Job({
cronTime: FORMAT.CRONTAB.EVERY_DAY,
start: true,
})
export class BaseLogJob implements IJob {
@Inject()
baseSysLogService: BaseSysLogService;
@Inject()
logger: ILogger;
async onTick() {
this.logger.info('清除日志定时任务开始执行');
const startTime = Date.now();
await this.baseSysLogService.clear();
this.logger.info(`清除日志定时任务结束,耗时:${Date.now() - startTime}ms`);
}
}

943
src/modules/base/menu.json

@ -0,0 +1,943 @@
[
{
"name": "系统管理",
"router": "/sys",
"perms": null,
"type": 0,
"icon": "icon-set",
"orderNum": 2,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "权限管理",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-auth",
"orderNum": 1,
"viewPath": null,
"keepAlive": false,
"isShow": true,
"childMenus": [
{
"name": "菜单列表",
"router": "/sys/menu",
"perms": null,
"type": 1,
"icon": "icon-menu",
"orderNum": 2,
"viewPath": "modules/base/views/menu/index.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "新增",
"router": null,
"perms": "base:sys:menu:add",
"type": 2,
"icon": null,
"orderNum": 1,
"viewPath": null,
"keepAlive": false,
"isShow": true,
"childMenus": []
},
{
"name": "删除",
"router": null,
"perms": "base:sys:menu:delete",
"type": 2,
"icon": null,
"orderNum": 2,
"viewPath": null,
"keepAlive": false,
"isShow": true,
"childMenus": []
},
{
"name": "查询",
"router": null,
"perms": "base:sys:menu:page,base:sys:menu:list,base:sys:menu:info",
"type": 2,
"icon": null,
"orderNum": 4,
"viewPath": null,
"keepAlive": false,
"isShow": true,
"childMenus": []
},
{
"name": "参数",
"router": "/test/aa",
"perms": null,
"type": 1,
"icon": "icon-goods",
"orderNum": 0,
"viewPath": "modules/base/views/info.vue",
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "编辑",
"router": null,
"perms": "base:sys:menu:info,base:sys:menu:update",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
},
{
"name": "角色列表",
"router": "/sys/role",
"perms": null,
"type": 1,
"icon": "icon-dept",
"orderNum": 3,
"viewPath": "cool/modules/base/views/role.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "新增",
"router": null,
"perms": "base:sys:role:add",
"type": 2,
"icon": null,
"orderNum": 1,
"viewPath": null,
"keepAlive": false,
"isShow": true,
"childMenus": []
},
{
"name": "删除",
"router": null,
"perms": "base:sys:role:delete",
"type": 2,
"icon": null,
"orderNum": 2,
"viewPath": null,
"keepAlive": false,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "base:sys:role:update",
"type": 2,
"icon": null,
"orderNum": 3,
"viewPath": null,
"keepAlive": false,
"isShow": true,
"childMenus": []
},
{
"name": "查询",
"router": null,
"perms": "base:sys:role:page,base:sys:role:list,base:sys:role:info",
"type": 2,
"icon": null,
"orderNum": 4,
"viewPath": null,
"keepAlive": false,
"isShow": true,
"childMenus": []
}
]
},
{
"name": "用户列表",
"router": "/sys/user",
"perms": null,
"type": 1,
"icon": "icon-user",
"orderNum": 0,
"viewPath": "modules/base/views/user/index.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "部门列表",
"router": null,
"perms": "base:sys:department:list",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "新增部门",
"router": null,
"perms": "base:sys:department:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "更新部门",
"router": null,
"perms": "base:sys:department:update",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "删除部门",
"router": null,
"perms": "base:sys:department:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "部门排序",
"router": null,
"perms": "base:sys:department:order",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "用户转移",
"router": null,
"perms": "base:sys:user:move",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "新增",
"router": null,
"perms": "base:sys:user:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "删除",
"router": null,
"perms": "base:sys:user:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "base:sys:user:delete,base:sys:user:update",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "查询",
"router": null,
"perms": "base:sys:user:page,base:sys:user:list,base:sys:user:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
}
]
},
{
"name": "参数配置",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-params",
"orderNum": 3,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "参数列表",
"router": "/sys/param",
"perms": null,
"type": 1,
"icon": "icon-menu",
"orderNum": 0,
"viewPath": "cool/modules/base/views/param.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "新增",
"router": null,
"perms": "base:sys:param:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "base:sys:param:info,base:sys:param:update",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "删除",
"router": null,
"perms": "base:sys:param:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "查看",
"router": null,
"perms": "base:sys:param:page,base:sys:param:list,base:sys:param:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
}
]
},
{
"name": "监控管理",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-monitor",
"orderNum": 9,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "请求日志",
"router": "/sys/log",
"perms": null,
"type": 1,
"icon": "icon-log",
"orderNum": 1,
"viewPath": "cool/modules/base/views/log.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "权限",
"router": null,
"perms": "base:sys:log:page,base:sys:log:clear,base:sys:log:getKeep,base:sys:log:setKeep",
"type": 2,
"icon": null,
"orderNum": 1,
"viewPath": null,
"keepAlive": false,
"isShow": true,
"childMenus": []
}
]
}
]
},
{
"name": "任务管理",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-activity",
"orderNum": 9,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "任务列表",
"router": "/task/list",
"perms": null,
"type": 1,
"icon": "icon-menu",
"orderNum": 0,
"viewPath": "modules/task/views/list.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "权限",
"router": null,
"perms": "task:info:page,task:info:list,task:info:info,task:info:add,task:info:delete,task:info:update,task:info:stop,task:info:start,task:info:once,task:info:log",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
}
]
}
]
},
{
"name": "框架教程",
"router": "/tutorial",
"perms": null,
"type": 0,
"icon": "icon-task",
"orderNum": 98,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "文档官网",
"router": "/tutorial/doc",
"perms": null,
"type": 1,
"icon": "icon-log",
"orderNum": 0,
"viewPath": "https://admin.cool-js.com",
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "crud 示例",
"router": "/demo/crud",
"perms": null,
"type": 1,
"icon": "icon-favor",
"orderNum": 1,
"viewPath": "modules/demo/views/crud/index.vue",
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
},
{
"name": "通用",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-radioboxfill",
"orderNum": 99,
"viewPath": null,
"keepAlive": true,
"isShow": false,
"childMenus": [
{
"name": "图片上传",
"router": null,
"perms": "space:info:page,space:info:list,space:info:info,space:info:add,space:info:delete,space:info:update,space:type:page,space:type:list,space:type:info,space:type:add,space:type:delete,space:type:update",
"type": 2,
"icon": null,
"orderNum": 1,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
},
{
"name": "首页",
"router": "/",
"perms": null,
"type": 1,
"icon": null,
"orderNum": 0,
"viewPath": "modules/demo/views/home/index.vue",
"keepAlive": true,
"isShow": false,
"childMenus": []
},
{
"name": "数据管理",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-data",
"orderNum": 7,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "字典管理",
"router": "/dict/list",
"perms": null,
"type": 1,
"icon": "icon-dict",
"orderNum": 3,
"viewPath": "modules/dict/views/list.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "删除",
"router": null,
"perms": "dict:info:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "dict:info:update,dict:info:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "获得字典数据",
"router": null,
"perms": "dict:info:data",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "单个信息",
"router": null,
"perms": "dict:info:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "列表查询",
"router": null,
"perms": "dict:info:list",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "分页查询",
"router": null,
"perms": "dict:info:page",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "新增",
"router": null,
"perms": "dict:info:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "组权限",
"router": null,
"perms": "dict:type:list,dict:type:update,dict:type:delete,dict:type:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "字典类型",
"router": null,
"perms": "dict:type:delete,dict:type:update,dict:type:info,dict:type:list,dict:type:page,dict:type:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
},
{
"name": "数据回收站",
"router": "/recycle/data",
"perms": null,
"type": 1,
"icon": "icon-delete",
"orderNum": 6,
"viewPath": "modules/recycle/views/data.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "恢复数据",
"router": null,
"perms": "recycle:data:restore",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "单个信息",
"router": null,
"perms": "recycle:data:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "分页查询",
"router": null,
"perms": "recycle:data:page",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
},
{
"name": "文件管理",
"router": "/upload/list",
"perms": null,
"type": 1,
"icon": "icon-log",
"orderNum": 5,
"viewPath": "modules/space/views/list.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "权限",
"router": null,
"perms": "space:type:delete,space:type:update,space:type:info,space:type:list,space:type:page,space:type:add,space:info:getConfig,space:info:delete,space:info:update,space:info:info,space:info:list,space:info:page,space:info:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
}
]
},
{
"name": "用户管理",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-user",
"orderNum": 11,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "用户列表",
"router": "/user/list",
"perms": null,
"type": 1,
"icon": "icon-menu",
"orderNum": 1,
"viewPath": "modules/user/views/list.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"name": "删除",
"router": null,
"perms": "user:info:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "修改",
"router": null,
"perms": "user:info:update,user:info:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "单个信息",
"router": null,
"perms": "user:info:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "列表查询",
"router": null,
"perms": "user:info:list",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "分页查询",
"router": null,
"perms": "user:info:page",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"name": "新增",
"router": null,
"perms": "user:info:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
}
]
},
{
"name": "扩展管理",
"router": null,
"perms": null,
"type": 0,
"icon": "icon-favor",
"orderNum": 8,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"tenantId": null,
"name": "插件列表",
"router": "/helper/plugins",
"perms": null,
"type": 1,
"icon": "icon-list",
"orderNum": 1,
"viewPath": "modules/helper/views/plugins.vue",
"keepAlive": true,
"isShow": true,
"childMenus": [
{
"tenantId": null,
"name": "删除",
"router": null,
"perms": "plugin:info:delete",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"tenantId": null,
"name": "分页查询",
"router": null,
"perms": "plugin:info:page",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"tenantId": null,
"name": "单个信息",
"router": null,
"perms": "plugin:info:info",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"tenantId": null,
"name": "安装插件",
"router": null,
"perms": "plugin:info:install",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"tenantId": null,
"name": "修改",
"router": null,
"perms": "plugin:info:update",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"tenantId": null,
"name": "列表查询",
"router": null,
"perms": "plugin:info:list",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
},
{
"tenantId": null,
"name": "新增",
"router": null,
"perms": "plugin:info:add",
"type": 2,
"icon": null,
"orderNum": 0,
"viewPath": null,
"keepAlive": true,
"isShow": true,
"childMenus": []
}
]
}
]
}
]

153
src/modules/base/middleware/authority.ts

@ -0,0 +1,153 @@
import { App, Config, Inject, Middleware } from '@midwayjs/core';
import * as _ from 'lodash';
import { CoolCommException, CoolUrlTagData, TagTypes } from '@cool-midway/core';
import * as jwt from 'jsonwebtoken';
import { NextFunction, Context } from '@midwayjs/koa';
import {
IMiddleware,
IMidwayApplication,
Init,
InjectClient,
} from '@midwayjs/core';
import { CachingFactory, MidwayCache } from '@midwayjs/cache-manager';
import { Utils } from '../../../comm/utils';
/**
*
*/
@Middleware()
export class BaseAuthorityMiddleware
implements IMiddleware<Context, NextFunction>
{
@Config('koa.globalPrefix')
prefix;
@Config('module.base')
jwtConfig;
@InjectClient(CachingFactory, 'default')
midwayCache: MidwayCache;
@Inject()
coolUrlTagData: CoolUrlTagData;
@App()
app: IMidwayApplication;
@Inject()
utils: Utils;
ignoreUrls: string[] = [];
// 自定义忽略路径前缀(不需要权限校验的接口前缀)
customIgnorePrefixes: string[] = ['/admin/cs'];
@Init()
async init() {
this.ignoreUrls = this.coolUrlTagData.byKey(TagTypes.IGNORE_TOKEN, 'admin');
}
resolve() {
return async (ctx: Context, next: NextFunction) => {
let statusCode = 200;
let { url } = ctx;
url = url.replace(this.prefix, '').split('?')[0];
const token = ctx.get('Authorization');
const adminUrl = '/admin/';
// 路由地址为 admin前缀的 需要权限校验
if (_.startsWith(url, adminUrl)) {
// 检查是否为自定义忽略前缀(不需要权限校验的接口)
const isCustomIgnored = this.customIgnorePrefixes.some(prefix =>
_.startsWith(url, prefix)
);
if (isCustomIgnored) {
// 如果是自定义忽略的路径,直接放行,不进行任何权限校验
await next();
return;
}
try {
ctx.admin = jwt.verify(token, this.jwtConfig.jwt.secret);
if (ctx.admin.isRefresh) {
ctx.status = 401;
throw new CoolCommException('登录失效~', ctx.status);
}
} catch (error) {}
// 使用matchUrl方法来检查URL是否应该被忽略
const isIgnored = this.ignoreUrls.some(pattern =>
this.utils.matchUrl(pattern, url)
);
if (isIgnored) {
await next();
return;
}
if (ctx.admin) {
const rToken = await this.midwayCache.get(
`admin:token:${ctx.admin.userId}`
);
// 判断密码版本是否正确
const passwordV = await this.midwayCache.get(
`admin:passwordVersion:${ctx.admin.userId}`
);
if (passwordV != ctx.admin.passwordVersion) {
throw new CoolCommException('登录失效~', 401);
}
// 超管拥有所有权限
if (ctx.admin.username == 'admin' && !ctx.admin.isRefresh) {
if (rToken !== token && this.jwtConfig.jwt.sso) {
throw new CoolCommException('登录失效~', 401);
} else {
await next();
return;
}
}
// 要登录每个人都有权限的接口
if (
new RegExp(`^${adminUrl}?.*/comm/`).test(url) ||
// 字典接口
url == '/admin/dict/info/data'
) {
await next();
return;
}
// 如果传的token是refreshToken则校验失败
if (ctx.admin.isRefresh) {
throw new CoolCommException('登录失效~', 401);
}
if (!rToken) {
throw new CoolCommException('登录失效或无权限访问~', 401);
}
if (rToken !== token && this.jwtConfig.jwt.sso) {
statusCode = 401;
} else {
let perms: string[] = await this.midwayCache.get(
`admin:perms:${ctx.admin.userId}`
);
if (!_.isEmpty(perms)) {
perms = 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) {
throw new CoolCommException('登录失效或无权限访问~', statusCode);
}
}
await next();
};
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save