commit
e207c76516
236 changed files with 16841 additions and 0 deletions
@ -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 用户拥有所有的权限,无需配置,但是对于其他只拥有部分权限的用户,我们得选择他们的权限,在这之前我们得先录入我们的系统有哪些权限是可以配置的 |
|||
|
|||
可以登录后台管理系统,`系统管理/权限管理/菜单列表` |
|||
|
|||
 |
|||
|
|||
## 选择权限 |
|||
|
|||
新建一个角色,就可以为这个角色配置对应的权限,用户管理可以选择对应的角色,那么该用户就有对应的权限,一个用户可以选择多个角色 |
|||
|
|||
 |
|||
|
|||
## 全局校验 |
|||
|
|||
通过一个全局的中间件,我们在全局统一处理,这样就无需在每个 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 生成校验的实现方式 |
|||
@ -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 等其他位置使用 |
|||
::: |
|||
@ -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; |
|||
}; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
@ -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; |
|||
``` |
|||
@ -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[]) |
|||
``` |
|||
@ -0,0 +1,22 @@ |
|||
--- |
|||
description: 异常处理(Exception) |
|||
globs: |
|||
--- |
|||
# 异常处理 |
|||
|
|||
框架自带有: `CoolCommException` |
|||
|
|||
## 通用异常 |
|||
|
|||
CoolCommException |
|||
|
|||
返回码: 1001 |
|||
|
|||
返回消息:comm fail |
|||
|
|||
用法: |
|||
|
|||
```ts |
|||
// 可以自定义返回消息 |
|||
throw new CoolCommException('用户不存在~'); |
|||
``` |
|||
@ -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 |
|||
``` |
|||
@ -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); |
|||
} |
|||
} |
|||
``` |
|||
@ -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 }), |
|||
}, |
|||
}; |
|||
``` |
|||
@ -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 * *` |
|||
|
|||
 |
|||
|
|||
## 分布式任务(管理后台配置) |
|||
|
|||
当需要分布式部署时,需要开启分布式任务,通过 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); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 配置定时任务 |
|||
|
|||
登录后台 任务管理/任务列表 |
|||
|
|||
 |
|||
|
|||
::: 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) |
|||
::: |
|||
@ -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', {}); |
|||
} |
|||
} |
|||
|
|||
``` |
|||
@ -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根据需要进行参考 |
|||
@ -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 |
|||
@ -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" |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
*.js text eol=lf |
|||
*.json text eol=lf |
|||
*.ts text eol=lf |
|||
*.code-snippets text eol=lf |
|||
@ -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/* |
|||
@ -0,0 +1,3 @@ |
|||
module.exports = { |
|||
...require('mwts/.prettierrc.json') |
|||
} |
|||
@ -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代码片段" |
|||
} |
|||
} |
|||
@ -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代码片段" |
|||
} |
|||
} |
|||
@ -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代码片段" |
|||
} |
|||
} |
|||
@ -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代码片段" |
|||
} |
|||
} |
|||
@ -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代码片段" |
|||
} |
|||
} |
|||
@ -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代码片段" |
|||
} |
|||
} |
|||
@ -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代码片段" |
|||
} |
|||
} |
|||
@ -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"] |
|||
@ -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] [厦门闪酷科技开发有限公司] |
|||
|
|||
特此免费授予获得本软件及相关文档文件(“软件”)副本的任何人无限制地处理本软件的权限,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件的副本,并允许软件提供给其的人员这样做,但须符合以下条件: |
|||
|
|||
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。 |
|||
|
|||
本软件按“原样”提供,不提供任何明示或暗示的担保,包括但不限于对适销性、特定用途适用性和非侵权的担保。在任何情况下,作者或版权持有人均不对因软件或软件使用或其他交易而产生的任何索赔、损害或其他责任负责,无论是在合同诉讼、侵权诉讼或其他诉讼中。 |
|||
@ -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) |
|||
- **模块化**:代码是模块化的,清晰明了,方便维护 |
|||
- **插件化**:插件化的设计,可以通过安装插件的方式扩展如:支付、短信、邮件等功能 |
|||
- ...... |
|||
|
|||
 |
|||
|
|||
## 技术栈 |
|||
|
|||
- 后端:**`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) |
|||
@ -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(); |
|||
@ -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 |
|||
@ -0,0 +1,6 @@ |
|||
module.exports = { |
|||
preset: 'ts-jest', |
|||
testEnvironment: 'node', |
|||
testPathIgnorePatterns: ['<rootDir>/test/fixtures'], |
|||
coveragePathIgnorePatterns: ['<rootDir>/test/'], |
|||
}; |
|||
@ -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" |
|||
} |
|||
Binary file not shown.
@ -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); |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 5.7 KiB |
@ -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> |
|||
@ -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); |
|||
}); |
|||
@ -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. |
|||
@ -0,0 +1,2 @@ |
|||
swagger-ui |
|||
Copyright 2020-2021 SmartBear Software Inc. |
|||
@ -0,0 +1,22 @@ |
|||
# Swagger UI Dist |
|||
[](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. |
|||
@ -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 |
|||
|
After Width: | Height: | Size: 665 B |
|
After Width: | Height: | Size: 628 B |
@ -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; |
|||
} |
|||
@ -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> |
|||
@ -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") |
|||
@ -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> |
|||
@ -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": {} |
|||
} |
|||
@ -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>
|
|||
}; |
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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'); |
|||
}; |
|||
@ -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; |
|||
} |
|||
@ -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
|
|||
} |
|||
} |
|||
@ -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; |
|||
@ -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; |
|||
@ -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; |
|||
@ -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() {} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
// 自动生成的文件,请勿手动修改
|
|||
export const entities = []; |
|||
@ -0,0 +1,6 @@ |
|||
/** |
|||
* @description User-Service parameters |
|||
*/ |
|||
export interface IUserOptions { |
|||
uid: number; |
|||
} |
|||
@ -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; |
|||
}; |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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'); |
|||
} |
|||
} |
|||
@ -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: '登录失效~', |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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')); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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 {} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
这里写对外的api接口 |
|||
@ -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()); |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
] |
|||
} |
|||
@ -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}'`); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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[]; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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}`); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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`); |
|||
} |
|||
} |
|||
@ -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": [] |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
] |
|||
@ -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…
Reference in new issue