├── bootstrap.js ├── .prettierrc.js ├── src ├── module │ └── system │ │ ├── file │ │ ├── vo │ │ │ └── file.ts │ │ ├── dto │ │ │ └── file.ts │ │ ├── controller │ │ │ └── file.ts │ │ ├── entity │ │ │ └── file.ts │ │ └── service │ │ │ └── file.ts │ │ ├── role │ │ ├── vo │ │ │ ├── role.ts │ │ │ └── role-page.ts │ │ ├── entity │ │ │ ├── role-menu.ts │ │ │ └── role.ts │ │ ├── dto │ │ │ ├── role-page.ts │ │ │ ├── role-menu.ts │ │ │ └── role.ts │ │ ├── controller │ │ │ └── role.ts │ │ └── service │ │ │ └── role.ts │ │ ├── api-log │ │ ├── vo │ │ │ ├── api-log.ts │ │ │ ├── api-log-page.ts │ │ │ ├── body-params.ts │ │ │ ├── query-params.ts │ │ │ └── result-params.ts │ │ ├── dto │ │ │ ├── api-log.ts │ │ │ └── api-log-page.ts │ │ ├── entity │ │ │ └── api-log.ts │ │ ├── service │ │ │ └── api-log.ts │ │ └── controller │ │ │ └── api-log.ts │ │ ├── api │ │ ├── vo │ │ │ ├── api-page.ts │ │ │ └── api.ts │ │ ├── dto │ │ │ └── api.ts │ │ ├── controller │ │ │ └── api.ts │ │ └── service │ │ │ └── api.ts │ │ ├── login-log │ │ ├── vo │ │ │ └── login-log.ts │ │ ├── dto │ │ │ ├── login-log-page.ts │ │ │ └── login-log.ts │ │ ├── service │ │ │ └── login-log.ts │ │ ├── entity │ │ │ └── login-log.ts │ │ └── controller │ │ │ └── login-log.ts │ │ ├── menu │ │ ├── vo │ │ │ ├── menu-page.ts │ │ │ └── menu.ts │ │ ├── entity │ │ │ ├── menu-version.ts │ │ │ ├── menu-api.ts │ │ │ └── menu.ts │ │ ├── dto │ │ │ ├── update-menu-version.ts │ │ │ ├── menu-api.ts │ │ │ ├── menu-version.ts │ │ │ └── menu.ts │ │ ├── controller │ │ │ └── menu.ts │ │ └── service │ │ │ └── menu.ts │ │ ├── user │ │ ├── vo │ │ │ ├── user-page.ts │ │ │ └── user.ts │ │ ├── entity │ │ │ ├── user-role.ts │ │ │ └── user.ts │ │ ├── dto │ │ │ ├── user-page.ts │ │ │ └── user.ts │ │ ├── controller │ │ │ └── user.ts │ │ └── service │ │ │ └── user.ts │ │ ├── auth │ │ ├── vo │ │ │ ├── captcha.ts │ │ │ ├── current-user.ts │ │ │ └── token.ts │ │ ├── dto │ │ │ ├── refresh-token.ts │ │ │ ├── reset-password.ts │ │ │ └── login.ts │ │ ├── interface.ts │ │ ├── service │ │ │ ├── captcha.ts │ │ │ └── auth.ts │ │ └── controller │ │ │ └── auth.ts │ │ └── socket │ │ ├── type.ts │ │ ├── controller │ │ └── socket.ts │ │ └── service │ │ └── socket.ts ├── utils │ ├── typeorm-utils.ts │ ├── uuid.ts │ ├── assert.ts │ ├── filter-query.ts │ ├── utils.ts │ └── snow-flake.ts ├── common │ ├── common-error.ts │ ├── base-vo.ts │ ├── page-dto.ts │ ├── base-dto.ts │ ├── page-result-vo.ts │ ├── base-entity.ts │ ├── base-error-util.ts │ ├── mail-service.ts │ ├── common-validate-rules.ts │ ├── rsa-service.ts │ └── base-service.ts ├── basic_model.conf ├── task-queue │ ├── clear-file.ts │ └── init-database.ts ├── typeorm-event-subscriber.ts ├── filter │ ├── default-filter.ts │ ├── notfound-filter.ts │ ├── common-filter.ts │ ├── validate-filter.ts │ ├── forbidden-filter.ts │ └── unauthorized-filter.ts ├── autoload │ ├── minio.ts │ ├── casbin-watcher.ts │ └── sentry.ts ├── migration │ ├── 1687366030346-migration.ts │ ├── 1689004082896-migration.ts │ ├── 1689772199745-migration.ts │ ├── 1703083742862-migration.ts │ ├── 1692155209836-migration.ts │ ├── 1686884629903-migration.ts │ ├── 1687610837273-migration.ts │ └── 1688878440201-migration.ts ├── interface.ts ├── config │ ├── config.prod.ts │ └── config.default.ts ├── middleware │ ├── report.ts │ └── auth.ts ├── decorator │ ├── not-auth.ts │ └── not-login.ts └── configuration.ts ├── .vscode ├── settings.json └── launch.json ├── script ├── template │ ├── page-vo.template │ ├── page-dto.template │ ├── entity.template │ ├── vo.template │ ├── service.template │ ├── dto.template │ └── controller.template └── create-module.js ├── jest.config.js ├── .editorconfig ├── .gitignore ├── .eslintrc.json ├── README.md ├── tsconfig.json ├── Dockerfile ├── package.json ├── .github └── workflows │ └── docker-publish.yml └── LICENSE /bootstrap.js: -------------------------------------------------------------------------------- 1 | const { Bootstrap } = require('@midwayjs/bootstrap'); 2 | Bootstrap.run(); 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('mwts/.prettierrc.json'), 3 | arrowParens: "avoid", 4 | } 5 | -------------------------------------------------------------------------------- /src/module/system/file/vo/file.ts: -------------------------------------------------------------------------------- 1 | import { FileEntity } from '../entity/file'; 2 | 3 | export class FileVO extends FileEntity {} 4 | -------------------------------------------------------------------------------- /src/module/system/role/vo/role.ts: -------------------------------------------------------------------------------- 1 | import { RoleEntity } from '../entity/role'; 2 | 3 | export class RoleVO extends RoleEntity {} 4 | -------------------------------------------------------------------------------- /src/utils/typeorm-utils.ts: -------------------------------------------------------------------------------- 1 | import { FindOperator, Like } from 'typeorm'; 2 | 3 | export function like(val: string): FindOperator { 4 | return Like(`%${val}%`); 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.foldingImportsByDefault": true, 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": "always", 5 | "source.fixAll": "always" 6 | } 7 | } -------------------------------------------------------------------------------- /src/module/system/api-log/vo/api-log.ts: -------------------------------------------------------------------------------- 1 | import { ApiLogEntity } from '../entity/api-log'; 2 | 3 | // eslint-disable-next-line prettier/prettier 4 | export class ApiLogVO extends ApiLogEntity {} 5 | -------------------------------------------------------------------------------- /script/template/page-vo.template: -------------------------------------------------------------------------------- 1 | import { PageVOWrapper } from '../../../../common/page-result-vo'; 2 | import { $1VO } from './$2'; 3 | 4 | export class $1PageVO extends PageVOWrapper<$1VO>($1VO) {} 5 | -------------------------------------------------------------------------------- /src/common/common-error.ts: -------------------------------------------------------------------------------- 1 | import { MidwayError } from '@midwayjs/core'; 2 | 3 | export class CommonError extends MidwayError { 4 | constructor(message: string) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/module/system/api/vo/api-page.ts: -------------------------------------------------------------------------------- 1 | import { PageVOWrapper } from '../../../../common/page-result-vo'; 2 | import { ApiVO } from './api'; 3 | 4 | export class ApiPageVo extends PageVOWrapper(ApiVO) {} 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: ['/test/fixtures'], 5 | coveragePathIgnorePatterns: ['/test/'], 6 | }; -------------------------------------------------------------------------------- /src/module/system/api-log/dto/api-log.ts: -------------------------------------------------------------------------------- 1 | import { BaseDTO } from '../../../../common/base-dto'; 2 | import { ApiLogEntity } from '../entity/api-log'; 3 | 4 | export class ApiLogDTO extends BaseDTO {} 5 | -------------------------------------------------------------------------------- /src/module/system/login-log/vo/login-log.ts: -------------------------------------------------------------------------------- 1 | import { LoginLogEntity } from '../entity/login-log'; 2 | 3 | // eslint-disable-next-line prettier/prettier 4 | export class LoginLogVO extends LoginLogEntity { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/module/system/menu/vo/menu-page.ts: -------------------------------------------------------------------------------- 1 | import { PageVOWrapper } from '../../../../common/page-result-vo'; 2 | import { MenuVO } from './menu'; 3 | 4 | export class MenuPageVO extends PageVOWrapper(MenuVO) {} 5 | -------------------------------------------------------------------------------- /src/module/system/role/vo/role-page.ts: -------------------------------------------------------------------------------- 1 | import { PageVOWrapper } from '../../../../common/page-result-vo'; 2 | import { RoleVO } from './role'; 3 | 4 | export class RolePageVO extends PageVOWrapper(RoleVO) {} 5 | -------------------------------------------------------------------------------- /src/module/system/user/vo/user-page.ts: -------------------------------------------------------------------------------- 1 | import { PageVOWrapper } from '../../../../common/page-result-vo'; 2 | import { UserVO } from './user'; 3 | 4 | export class UserPageVO extends PageVOWrapper(UserVO) {} 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 🎨 editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true -------------------------------------------------------------------------------- /src/module/system/api-log/vo/api-log-page.ts: -------------------------------------------------------------------------------- 1 | import { PageVOWrapper } from '../../../../common/page-result-vo'; 2 | import { ApiLogVO } from './api-log'; 3 | 4 | export class ApiLogPageVO extends PageVOWrapper(ApiLogVO) {} 5 | -------------------------------------------------------------------------------- /src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid'; 2 | 3 | export const uuid = () => { 4 | return nanoid(); 5 | }; 6 | 7 | export const generateRandomCode = () => { 8 | return Math.floor(Math.random() * 9000) + 1000; 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | coverage/ 8 | dist/ 9 | .idea/ 10 | run/ 11 | .DS_Store 12 | *.sw* 13 | *.un~ 14 | .tsbuildinfo 15 | .tsbuildinfo.* 16 | .env 17 | -------------------------------------------------------------------------------- /src/module/system/menu/vo/menu.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { MenuEntity } from '../entity/menu'; 3 | 4 | export class MenuVO extends MenuEntity { 5 | @ApiProperty({ description: '子菜单' }) 6 | hasChild?: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/module/system/auth/vo/captcha.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | 3 | export class CaptchaVO { 4 | @ApiProperty({ description: 'id' }) 5 | id: string; 6 | @ApiProperty({ description: '验证码图片 base64' }) 7 | imageBase64: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/module/system/api-log/vo/body-params.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | 3 | export class BodyParamsVO { 4 | @ApiProperty({ description: 'id' }) 5 | id?: string; 6 | 7 | @ApiProperty({ description: 'body参数' }) 8 | body?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/module/system/api-log/vo/query-params.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | 3 | export class QueryParamsVO { 4 | @ApiProperty({ description: 'id' }) 5 | id?: string; 6 | 7 | @ApiProperty({ description: 'query参数' }) 8 | query?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/module/system/api-log/vo/result-params.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | 3 | export class ResultParamsVO { 4 | @ApiProperty({ description: 'id' }) 5 | id?: string; 6 | 7 | @ApiProperty({ description: 'result参数' }) 8 | result?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/base-vo.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | 3 | export class BaseVO { 4 | @ApiProperty({ description: 'id' }) 5 | id: string; 6 | @ApiProperty({ description: '创建时间' }) 7 | createDate: Date; 8 | @ApiProperty({ description: '更新时间' }) 9 | updateDate: Date; 10 | } 11 | -------------------------------------------------------------------------------- /script/template/page-dto.template: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { PageDTO } from '../../../../common/page-dto'; 3 | 4 | export class $1PageDTO extends PageDTO { 5 | @ApiProperty({ description: '代码' }) 6 | code: string; 7 | @ApiProperty({ description: '名称' }) 8 | name: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/module/system/auth/dto/refresh-token.ts: -------------------------------------------------------------------------------- 1 | import { Rule, RuleType } from '@midwayjs/validate'; 2 | import { ApiProperty } from '@midwayjs/swagger'; 3 | 4 | export class RefreshTokenDTO { 5 | @ApiProperty({ 6 | description: '刷新token', 7 | }) 8 | @Rule(RuleType.allow(null)) 9 | refreshToken?: string; 10 | } 11 | -------------------------------------------------------------------------------- /script/template/entity.template: -------------------------------------------------------------------------------- 1 | import { Entity, Column } from 'typeorm'; 2 | import { BaseEntity } from '../../../../common/base-entity'; 3 | 4 | @Entity('$5_$4') 5 | export class $1Entity extends BaseEntity { 6 | @Column({ comment: '名称' }) 7 | name?: string; 8 | @Column({ comment: '代码' }) 9 | code?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/basic_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | g2 = _, _ 10 | 11 | [policy_effect] 12 | e = some(where (p.eft == allow)) 13 | 14 | [matchers] 15 | m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && r.act == p.act || r.sub == "root" -------------------------------------------------------------------------------- /script/template/vo.template: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { BaseVO } from '../../../../common/base-vo'; 3 | 4 | export class $1VO extends BaseVO { 5 | @ApiProperty({ 6 | description: '代码', 7 | }) 8 | code?: string; 9 | @ApiProperty({ 10 | description: '名称', 11 | }) 12 | name?: string; 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/mwts/", 3 | "ignorePatterns": [ 4 | "node_modules", 5 | "dist", 6 | "test", 7 | "jest.config.js", 8 | "typings" 9 | ], 10 | "env": { 11 | "jest": true 12 | }, 13 | "rules": { 14 | "no-empty-function": "off", 15 | "space-before-function-paren": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/module/system/role/entity/role-menu.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../../../common/base-entity'; 3 | 4 | @Entity('sys_role_menu') 5 | export class RoleMenuEntity extends BaseEntity { 6 | @Column({ comment: '角色id' }) 7 | roleId?: string; 8 | @Column({ comment: '菜单id' }) 9 | menuId?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/module/system/user/entity/user-role.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../../../common/base-entity'; 3 | 4 | @Entity('sys_user_role') 5 | export class UserRoleEntity extends BaseEntity { 6 | @Column({ comment: '用户id' }) 7 | userId?: string; 8 | @Column({ comment: '角色id' }) 9 | roleId?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/module/system/auth/vo/current-user.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { MenuVO } from '../../menu/vo/menu'; 3 | import { UserVO } from '../../user/vo/user'; 4 | 5 | export class CurrentUserVO extends UserVO { 6 | @ApiProperty({ 7 | description: '用户分配的菜单列表', 8 | type: MenuVO, 9 | isArray: true, 10 | }) 11 | menus: MenuVO[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/common/page-dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule, RuleType } from '@midwayjs/validate'; 3 | 4 | export class PageDTO { 5 | @ApiProperty({ description: '页码', example: 0 }) 6 | @Rule(RuleType.allow(null)) 7 | page: number; 8 | @ApiProperty({ description: '每页数量', example: 10 }) 9 | @Rule(RuleType.allow(null)) 10 | size: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/module/system/auth/vo/token.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | 3 | export class TokenVO { 4 | @ApiProperty({ description: 'token的过期时间' }) 5 | expire: number; 6 | @ApiProperty({ description: 'token' }) 7 | token: string; 8 | @ApiProperty({ description: '刷新token的过期时间' }) 9 | refreshExpire: number; 10 | @ApiProperty({ description: '刷新token' }) 11 | refreshToken: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/module/system/menu/entity/menu-version.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../../../common/base-entity'; 3 | 4 | @Entity('sys_menu_version') 5 | export class MenuVersionEntity extends BaseEntity { 6 | @Column({ comment: '菜单id' }) 7 | menuId?: string; 8 | @Column({ comment: '版本号' }) 9 | version?: string; 10 | @Column({ comment: '版本描述' }) 11 | description?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/module/system/login-log/dto/login-log-page.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule } from '@midwayjs/validate'; 3 | import { string } from '../../../../common/common-validate-rules'; 4 | import { PageDTO } from '../../../../common/page-dto'; 5 | 6 | export class LoginLogPageDTO extends PageDTO { 7 | @ApiProperty({ description: '用户名' }) 8 | @Rule(string.allow(null, '')) 9 | userName?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/base-dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule, RuleType } from '@midwayjs/validate'; 3 | import { omit } from 'lodash'; 4 | import { BaseEntity } from './base-entity'; 5 | 6 | export class BaseDTO { 7 | @ApiProperty() 8 | @Rule(RuleType.allow(null)) 9 | id: string; 10 | toEntity(): T { 11 | return omit(this, ['createDate', 'updateDate']) as unknown as T; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/module/system/menu/dto/update-menu-version.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@midwayjs/validate'; 2 | import { R } from '../../../../common/base-error-util'; 3 | import { requiredString } from '../../../../common/common-validate-rules'; 4 | 5 | export class UpdateMenuVersionDTO { 6 | @Rule(requiredString.error(R.validateError('id不能为空'))) 7 | id: string; 8 | @Rule(requiredString.error(R.validateError('低代码配置不能为空'))) 9 | pageSetting: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/module/system/api/vo/api.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | 3 | export class ApiVO { 4 | @ApiProperty({ description: '接口url' }) 5 | path: string; 6 | 7 | @ApiProperty({ description: '接口前缀' }) 8 | prefix: string; 9 | 10 | @ApiProperty({ description: '接口名称' }) 11 | title: string; 12 | 13 | @ApiProperty({ description: '接口类型' }) 14 | type: string; 15 | 16 | @ApiProperty({ description: '请求方式' }) 17 | method: string; 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fluxy-admin-server 2 | 3 | ## 快速入门 4 | 5 | 6 | 7 | 如需进一步了解,参见 [midway 文档][midway]。 8 | 9 | ### 本地开发 10 | 11 | ```bash 12 | npm i 13 | npm run dev 14 | open http://localhost:7001/ 15 | ``` 16 | 17 | ### 部署 18 | 19 | ```bash 20 | npm start 21 | ``` 22 | 23 | ### 内置指令 24 | 25 | - 使用 `npm run lint` 来做代码风格检查。 26 | - 使用 `npm test` 来执行单元测试。 27 | 28 | [midway]: https://midwayjs.org 29 | 30 | **更多开发教程,请参考[开发文档](https://doc.fluxyadmin.cn)** 31 | -------------------------------------------------------------------------------- /src/module/system/role/entity/role.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Column, Entity } from 'typeorm'; 3 | import { BaseEntity } from '../../../../common/base-entity'; 4 | 5 | @Entity('sys_role') 6 | export class RoleEntity extends BaseEntity { 7 | @ApiProperty({ description: '名称' }) 8 | @Column({ comment: '名称' }) 9 | name?: string; 10 | 11 | @ApiProperty({ description: '代码' }) 12 | @Column({ comment: '代码' }) 13 | code?: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/module/system/user/dto/user-page.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { PageDTO } from '../../../../common/page-dto'; 3 | 4 | export class UserPageDTO extends PageDTO { 5 | @ApiProperty({ description: '用户名称' }) 6 | userName: string; 7 | 8 | @ApiProperty({ description: '用户昵称' }) 9 | nickName: string; 10 | 11 | @ApiProperty({ description: '手机号' }) 12 | phoneNumber: string; 13 | 14 | @ApiProperty({ description: '邮箱' }) 15 | email: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/task-queue/clear-file.ts: -------------------------------------------------------------------------------- 1 | import { IProcessor, Processor } from '@midwayjs/bull'; 2 | import { Inject } from '@midwayjs/core'; 3 | import { FileService } from '../module/system/file/service/file'; 4 | 5 | @Processor('clear_file', { 6 | repeat: { 7 | cron: '0 0 0 * * *', 8 | }, 9 | }) 10 | export class ClearFileProcessor implements IProcessor { 11 | @Inject() 12 | fileService: FileService; 13 | 14 | async execute() { 15 | this.fileService.clearEmptyPKValueFiles(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /script/template/service.template: -------------------------------------------------------------------------------- 1 | import { Provide } from '@midwayjs/core'; 2 | import { InjectEntityModel } from '@midwayjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { BaseService } from '../../../../common/base-service'; 5 | import { $1Entity } from '../entity/$2'; 6 | 7 | @Provide() 8 | export class $1Service extends BaseService<$1Entity> { 9 | @InjectEntityModel($1Entity) 10 | $3Model: Repository<$1Entity>; 11 | 12 | getModel(): Repository<$1Entity> { 13 | return this.$3Model; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/module/system/role/dto/role-page.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule } from '@midwayjs/validate'; 3 | import { string } from '../../../../common/common-validate-rules'; 4 | import { PageDTO } from '../../../../common/page-dto'; 5 | 6 | export class RolePageDTO extends PageDTO { 7 | @ApiProperty({ description: '角色编码' }) 8 | @Rule(string.allow(null, '')) 9 | code?: string; 10 | 11 | @ApiProperty({ description: '角色名称' }) 12 | @Rule(string.allow(null, '')) 13 | name?: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | import { R } from '../common/base-error-util'; 2 | 3 | export class AssertUtils { 4 | public static notEmpty(value: any, message: string): void { 5 | if (value === null || value === undefined) throw R.error(message); 6 | } 7 | public static arrNotEmpty(arr: any[], message: string): void { 8 | if (!arr || arr.length === 0) throw R.error(message); 9 | } 10 | public static isTrue(condition: boolean, message: string): void { 11 | if (condition !== true) throw R.error(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/common/page-result-vo.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, Type } from '@midwayjs/swagger'; 2 | 3 | export function PageVOWrapper(ResourceCls: Type): Type> { 4 | class Page extends PageResultVO { 5 | @ApiProperty({ 6 | description: '数据', 7 | type: ResourceCls, 8 | isArray: true, 9 | }) 10 | data: T[]; 11 | @ApiProperty({ description: '总条数' }) 12 | total: number; 13 | } 14 | return Page; 15 | } 16 | 17 | export class PageResultVO { 18 | data: T[]; 19 | total: number; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/filter-query.ts: -------------------------------------------------------------------------------- 1 | import { FindOptionsWhereProperty } from 'typeorm'; 2 | 3 | export class FilterQuery { 4 | where: any = {}; 5 | append( 6 | key: U, 7 | value: FindOptionsWhereProperty>, 8 | operator: boolean | (() => boolean) 9 | ) { 10 | if (typeof operator === 'function') { 11 | if (operator()) { 12 | this.where[key] = value; 13 | } 14 | } else if (operator) { 15 | this.where[key] = value; 16 | } 17 | return this; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/module/system/menu/dto/menu-api.ts: -------------------------------------------------------------------------------- 1 | import { Rule, RuleType } from '@midwayjs/validate'; 2 | import { R } from '../../../../common/base-error-util'; 3 | import { requiredString } from '../../../../common/common-validate-rules'; 4 | import { ApiDTO } from '../../api/dto/api'; 5 | import { MenuApiEntity } from '../entity/menu-api'; 6 | 7 | export class MenuInterfaceDTO extends MenuApiEntity { 8 | @Rule(requiredString.error(R.validateError('menu_id不能为空'))) 9 | menu_id?: string; 10 | @Rule(RuleType.array()) 11 | interface_infos: ApiDTO[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/module/system/api/dto/api.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule } from '@midwayjs/validate'; 3 | import { R } from '../../../../common/base-error-util'; 4 | import { requiredString } from '../../../../common/common-validate-rules'; 5 | 6 | export class ApiDTO { 7 | @ApiProperty({ description: '接口url' }) 8 | @Rule(requiredString.error(R.validateError('path不能为空'))) 9 | path: string; 10 | 11 | @ApiProperty({ description: '接口请求方式' }) 12 | @Rule(requiredString.error(R.validateError('method不能为空'))) 13 | method: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/module/system/api/controller/api.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Inject } from '@midwayjs/decorator'; 2 | import { ApiOkResponse } from '@midwayjs/swagger'; 3 | import { ApiService } from '../service/api'; 4 | import { ApiVO } from '../vo/api'; 5 | 6 | @Controller('/api', { description: '接口管理' }) 7 | export class ApiController { 8 | @Inject() 9 | apiService: ApiService; 10 | 11 | @Get('/list', { description: '获取接口列表' }) 12 | @ApiOkResponse({ 13 | type: ApiVO, 14 | isArray: true, 15 | }) 16 | async apiList() { 17 | return await this.apiService.getApiList(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/module/system/login-log/service/login-log.ts: -------------------------------------------------------------------------------- 1 | import { Provide } from '@midwayjs/decorator'; 2 | import { InjectEntityModel } from '@midwayjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { BaseService } from '../../../../common/base-service'; 5 | import { LoginLogEntity } from '../entity/login-log'; 6 | 7 | @Provide() 8 | export class LoginLogService extends BaseService { 9 | @InjectEntityModel(LoginLogEntity) 10 | loginLogModel: Repository; 11 | 12 | getModel(): Repository { 13 | return this.loginLogModel; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/module/system/menu/entity/menu-api.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Column, Entity } from 'typeorm'; 3 | import { BaseEntity } from '../../../../common/base-entity'; 4 | 5 | @Entity('sys_menu_api') 6 | export class MenuApiEntity extends BaseEntity { 7 | @ApiProperty({ description: '菜单id' }) 8 | @Column({ comment: '菜单id' }) 9 | menuId?: string; 10 | 11 | @ApiProperty({ description: '请求方式' }) 12 | @Column({ comment: '请求方式' }) 13 | method?: string; 14 | 15 | @ApiProperty({ description: '请求url' }) 16 | @Column({ comment: 'path' }) 17 | path?: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/common/base-entity.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { CreateDateColumn, PrimaryColumn, UpdateDateColumn } from 'typeorm'; 3 | 4 | export class BaseEntity { 5 | @ApiProperty({ description: 'id' }) 6 | @PrimaryColumn({ comment: '主键', name: 'id', type: 'bigint' }) 7 | id?: string; 8 | 9 | @ApiProperty({ description: '创建时间' }) 10 | @CreateDateColumn({ comment: '创建时间' }) 11 | createDate?: Date; 12 | 13 | @ApiProperty({ description: '更新时间' }) 14 | @UpdateDateColumn({ comment: '更新时间' }) 15 | updateDate?: Date; 16 | toVO?(): any { 17 | return this; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/typeorm-event-subscriber.ts: -------------------------------------------------------------------------------- 1 | import { CasbinRule } from '@midwayjs/casbin-typeorm-adapter'; 2 | import { EventSubscriberModel } from '@midwayjs/typeorm'; 3 | import { EntitySubscriberInterface, InsertEvent } from 'typeorm'; 4 | import { snowFlake } from './utils/snow-flake'; 5 | 6 | @EventSubscriberModel() 7 | export class EverythingSubscriber implements EntitySubscriberInterface { 8 | beforeInsert(event: InsertEvent) { 9 | if (event.entity instanceof CasbinRule) { 10 | return; 11 | } 12 | if (!event.entity.id) { 13 | event.entity.id = snowFlake.nextId(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/filter/default-filter.ts: -------------------------------------------------------------------------------- 1 | import { Catch } from '@midwayjs/core'; 2 | import { Context } from '@midwayjs/koa'; 3 | import { ApiLogService } from '../module/system/api-log/service/api-log'; 4 | @Catch() 5 | export class DefaultErrorFilter { 6 | async catch(err: Error, ctx: Context) { 7 | const apiLogService = await ctx.requestContext.getAsync( 8 | ApiLogService 9 | ); 10 | 11 | apiLogService.createApiLog(ctx, false, '500', err.message); 12 | 13 | ctx.logger.error(err); 14 | ctx.status = 500; 15 | 16 | return { 17 | code: 500, 18 | message: '系统错误', 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "es2018", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "inlineSourceMap": true, 10 | "noImplicitThis": true, 11 | "noUnusedLocals": true, 12 | "stripInternal": true, 13 | "skipLibCheck": true, 14 | "pretty": true, 15 | "declaration": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "typeRoots": ["./typings", "./node_modules/@types"], 18 | "outDir": "dist" 19 | }, 20 | "exclude": ["dist", "node_modules", "test"] 21 | } 22 | -------------------------------------------------------------------------------- /src/common/base-error-util.ts: -------------------------------------------------------------------------------- 1 | import { httpError } from '@midwayjs/core'; 2 | import { MidwayValidationError } from '@midwayjs/validate'; 3 | import { CommonError } from './common-error'; 4 | 5 | export class R { 6 | static error(message: string) { 7 | return new CommonError(message); 8 | } 9 | 10 | static validateError(message: string) { 11 | return new MidwayValidationError(message, 422, null); 12 | } 13 | 14 | static unauthorizedError(message: string) { 15 | return new httpError.UnauthorizedError(message); 16 | } 17 | 18 | static forbiddenError(message: string) { 19 | return new httpError.ForbiddenError(message); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/module/system/role/dto/role-menu.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule, RuleType } from '@midwayjs/validate'; 3 | import { R } from '../../../../common/base-error-util'; 4 | import { requiredString } from '../../../../common/common-validate-rules'; 5 | 6 | export class RoleMenuDTO { 7 | @ApiProperty({ description: '角色id' }) 8 | @Rule(requiredString.error(R.validateError('角色id不能为空'))) 9 | roleId?: string; 10 | 11 | @ApiProperty({ 12 | description: '菜单id列表', 13 | type: 'array', 14 | items: { type: 'string' }, 15 | }) 16 | @Rule(RuleType.array().items(RuleType.string())) 17 | menuIds?: string[]; 18 | } 19 | -------------------------------------------------------------------------------- /src/filter/notfound-filter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, httpError, MidwayHttpError } from '@midwayjs/core'; 2 | import { Context } from '@midwayjs/koa'; 3 | import { ApiLogService } from '../module/system/api-log/service/api-log'; 4 | 5 | @Catch(httpError.NotFoundError) 6 | export class NotFoundFilter { 7 | async catch(err: MidwayHttpError, ctx: Context) { 8 | const message = '请求地址不存在'; 9 | 10 | const apiLogService = await ctx.requestContext.getAsync( 11 | ApiLogService 12 | ); 13 | 14 | apiLogService.createApiLog(ctx, false, '404', message); 15 | 16 | ctx.status = 404; 17 | return { 18 | code: 404, 19 | message, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/autoload/minio.ts: -------------------------------------------------------------------------------- 1 | import { Config, IMidwayContainer, Singleton } from '@midwayjs/core'; 2 | import { ApplicationContext, Autoload, Init } from '@midwayjs/decorator'; 3 | import * as Minio from 'minio'; 4 | 5 | import { MinioConfig } from '../interface'; 6 | 7 | export type MinioClient = Minio.Client; 8 | 9 | @Autoload() 10 | @Singleton() 11 | export class MinioAutoLoad { 12 | @ApplicationContext() 13 | applicationContext: IMidwayContainer; 14 | @Config('minio') 15 | minioConfig: MinioConfig; 16 | @Init() 17 | async init() { 18 | const minioClient = new Minio.Client(this.minioConfig); 19 | this.applicationContext.registerObject('minioClient', minioClient); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /script/template/dto.template: -------------------------------------------------------------------------------- 1 | import { Rule } from '@midwayjs/validate'; 2 | import { ApiProperty } from '@midwayjs/swagger'; 3 | import { $1Entity } from '../entity/$2'; 4 | import { BaseDTO } from '../../../../common/base-dto'; 5 | import { requiredString } from '../../../../common/common-validate-rules'; 6 | import { R } from '../../../../common/base-error-util'; 7 | 8 | export class $1DTO extends BaseDTO<$1Entity> { 9 | @ApiProperty({ 10 | description: '代码', 11 | }) 12 | @Rule(requiredString.error(R.validateError('代码不能为空'))) 13 | code?: string; 14 | @ApiProperty({ 15 | description: '名称', 16 | }) 17 | @Rule(requiredString.error(R.validateError('名称不能为空'))) 18 | name?: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/module/system/socket/type.ts: -------------------------------------------------------------------------------- 1 | export enum SocketMessageType { 2 | /** 3 | * 权限变更 4 | */ 5 | PermissionChange = 'PermissionChange', 6 | /** 7 | * 密码重置 8 | */ 9 | PasswordChange = 'PasswordChange', 10 | /** 11 | * token过期 12 | */ 13 | TokenExpire = 'TokenExpire', 14 | /** 15 | * Ping 16 | */ 17 | Ping = 'Ping', 18 | /** 19 | * PONG 20 | */ 21 | Pong = 'Pong', 22 | } 23 | 24 | export class SocketMessage { 25 | /** 26 | * 消息类型 27 | */ 28 | type: SocketMessageType; 29 | /** 30 | * 消息内容 31 | */ 32 | data?: T; 33 | constructor(type: SocketMessageType, data?: T) { 34 | this.type = type; 35 | this.data = data; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/filter/common-filter.ts: -------------------------------------------------------------------------------- 1 | import { Catch } from '@midwayjs/decorator'; 2 | import { Context } from '@midwayjs/koa'; 3 | import { CommonError } from '../common/common-error'; 4 | import { ApiLogService } from '../module/system/api-log/service/api-log'; 5 | 6 | @Catch(CommonError) 7 | export class CommonErrorFilter { 8 | async catch(err: CommonError, ctx: Context) { 9 | const message = err.message; 10 | 11 | const apiLogService = await ctx.requestContext.getAsync( 12 | ApiLogService 13 | ); 14 | 15 | apiLogService.createApiLog(ctx, false, '400', message); 16 | 17 | ctx.status = 400; 18 | return { 19 | code: 400, 20 | message, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/module/system/file/dto/file.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule } from '@midwayjs/validate'; 3 | import { BaseDTO } from '../../../../common/base-dto'; 4 | import { R } from '../../../../common/base-error-util'; 5 | import { requiredString } from '../../../../common/common-validate-rules'; 6 | import { FileEntity } from '../entity/file'; 7 | 8 | export class FileDTO extends BaseDTO { 9 | @ApiProperty({ 10 | description: 'pkName', 11 | }) 12 | @Rule(requiredString.error(R.validateError('pkName不能为空'))) 13 | pkName: string; 14 | @ApiProperty({ 15 | type: 'string', 16 | format: 'binary', 17 | description: '文件', 18 | }) 19 | file: any; 20 | } 21 | -------------------------------------------------------------------------------- /src/module/system/file/controller/file.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Files, Inject, Post } from '@midwayjs/core'; 2 | import { ApiBody, ApiExcludeController } from '@midwayjs/swagger'; 3 | import { NotLogin } from '../../../../decorator/not-login'; 4 | import { FileService } from '../service/file'; 5 | 6 | @Controller('/file') 7 | @ApiExcludeController() 8 | export class FileController { 9 | @Inject() 10 | fileService: FileService; 11 | @Inject() 12 | minioClient; 13 | 14 | @Post('/upload') 15 | @ApiBody({ description: 'file' }) 16 | @NotLogin() 17 | async upload(@Files() files) { 18 | if (files.length) { 19 | return await this.fileService.upload(files[0]); 20 | } 21 | return {}; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/module/system/file/entity/file.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Column, Entity } from 'typeorm'; 3 | import { BaseEntity } from '../../../../common/base-entity'; 4 | 5 | @Entity('sys_file') 6 | export class FileEntity extends BaseEntity { 7 | @ApiProperty({ description: '文件名' }) 8 | @Column({ comment: '文件名' }) 9 | fileName?: string; 10 | 11 | @ApiProperty({ description: '文件路径' }) 12 | @Column({ comment: '文件路径' }) 13 | filePath?: string; 14 | 15 | @ApiProperty({ description: '外健名称' }) 16 | @Column({ comment: '外健名称', nullable: true }) 17 | pkName: string; 18 | 19 | @ApiProperty({ description: '外健值' }) 20 | @Column({ comment: '外健值', nullable: true }) 21 | pkValue?: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/filter/validate-filter.ts: -------------------------------------------------------------------------------- 1 | import { Catch } from '@midwayjs/decorator'; 2 | import { Context } from '@midwayjs/koa'; 3 | import { MidwayValidationError } from '@midwayjs/validate'; 4 | import { ApiLogService } from '../module/system/api-log/service/api-log'; 5 | 6 | @Catch(MidwayValidationError) 7 | export class ValidateErrorFilter { 8 | async catch(err: MidwayValidationError, ctx: Context) { 9 | const message = err.message; 10 | 11 | const apiLogService = await ctx.requestContext.getAsync( 12 | ApiLogService 13 | ); 14 | 15 | apiLogService.createApiLog(ctx, false, '422', message); 16 | 17 | ctx.status = 422; 18 | return { 19 | code: 422, 20 | message, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/module/system/login-log/dto/login-log.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule } from '@midwayjs/validate'; 3 | import { BaseDTO } from '../../../../common/base-dto'; 4 | import { R } from '../../../../common/base-error-util'; 5 | import { requiredString } from '../../../../common/common-validate-rules'; 6 | import { LoginLogEntity } from '../entity/login-log'; 7 | 8 | export class LoginLogDTO extends BaseDTO { 9 | @ApiProperty({ 10 | description: '代码', 11 | }) 12 | @Rule(requiredString.error(R.validateError('代码不能为空'))) 13 | code?: string; 14 | @ApiProperty({ 15 | description: '名称', 16 | }) 17 | @Rule(requiredString.error(R.validateError('名称不能为空'))) 18 | name?: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/module/system/auth/dto/reset-password.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule } from '@midwayjs/validate'; 3 | import { R } from '../../../../common/base-error-util'; 4 | import { 5 | email, 6 | requiredString, 7 | } from '../../../../common/common-validate-rules'; 8 | 9 | export class ResetPasswordDTO { 10 | @ApiProperty({ description: '密码' }) 11 | @Rule(requiredString.error(R.validateError('密码不能为空'))) 12 | password: string; 13 | @ApiProperty({ description: '邮箱' }) 14 | @Rule(email.error(R.validateError('无效的邮箱格式'))) 15 | email: string; 16 | @ApiProperty({ description: '邮箱验证码' }) 17 | emailCaptcha: string; 18 | @Rule(requiredString.error(R.validateError('公钥不能为空'))) 19 | publicKey: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/migration/1687366030346-migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class Migration1687366030346 implements MigrationInterface { 4 | name = 'Migration1687366030346'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `avatar`'); 8 | await queryRunner.query( 9 | "ALTER TABLE `sys_user` ADD `avatar` varchar(255) NULL COMMENT '头像'" 10 | ); 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `avatar`'); 15 | await queryRunner.query( 16 | "ALTER TABLE `sys_user` ADD `avatar` int NULL COMMENT '头像'" 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/module/system/menu/dto/menu-version.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@midwayjs/validate'; 2 | import { BaseDTO } from '../../../../common/base-dto'; 3 | import { R } from '../../../../common/base-error-util'; 4 | import { requiredString } from '../../../../common/common-validate-rules'; 5 | import { MenuVersionEntity } from '../entity/menu-version'; 6 | 7 | export class MenuVersionDTO extends BaseDTO { 8 | @Rule(requiredString.error(R.validateError('菜单id不能为空'))) 9 | menuId: string; 10 | @Rule(requiredString.error(R.validateError('版本号描述不能为空'))) 11 | description: string; 12 | @Rule(requiredString.error(R.validateError('版本号不能为空'))) 13 | version: string; 14 | @Rule(requiredString.error(R.validateError('低代码配置不能为空'))) 15 | pageSetting: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/filter/forbidden-filter.ts: -------------------------------------------------------------------------------- 1 | import { httpError } from '@midwayjs/core'; 2 | import { Catch } from '@midwayjs/decorator'; 3 | import { Context } from '@midwayjs/koa'; 4 | import { MidwayValidationError } from '@midwayjs/validate'; 5 | import { ApiLogService } from '../module/system/api-log/service/api-log'; 6 | 7 | @Catch(httpError.ForbiddenError) 8 | export class ForbiddenErrorFilter { 9 | async catch(err: MidwayValidationError, ctx: Context) { 10 | const message = err.message; 11 | 12 | const apiLogService = await ctx.requestContext.getAsync( 13 | ApiLogService 14 | ); 15 | 16 | apiLogService.createApiLog(ctx, false, '403', message); 17 | 18 | ctx.status = 403; 19 | return { 20 | code: 403, 21 | message, 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/filter/unauthorized-filter.ts: -------------------------------------------------------------------------------- 1 | import { httpError } from '@midwayjs/core'; 2 | import { UnauthorizedError } from '@midwayjs/core/dist/error/http'; 3 | import { Catch } from '@midwayjs/decorator'; 4 | import { Context } from '@midwayjs/koa'; 5 | import { ApiLogService } from '../module/system/api-log/service/api-log'; 6 | 7 | @Catch(httpError.UnauthorizedError) 8 | export class UnauthorizedErrorFilter { 9 | async catch(err: UnauthorizedError, ctx: Context) { 10 | const message = err.message; 11 | 12 | const apiLogService = await ctx.requestContext.getAsync( 13 | ApiLogService 14 | ); 15 | 16 | apiLogService.createApiLog(ctx, false, '401', message); 17 | 18 | ctx.status = 401; 19 | return { 20 | code: 401, 21 | message, 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [{ 7 | "name": "Midway Local", 8 | "type": "node", 9 | "request": "launch", 10 | "cwd": "${workspaceRoot}", 11 | "runtimeExecutable": "npm", 12 | "windows": { 13 | "runtimeExecutable": "npm.cmd" 14 | }, 15 | "runtimeArgs": [ 16 | "run", 17 | "dev" 18 | ], 19 | "env": { 20 | "NODE_ENV": "local" 21 | }, 22 | "console": "integratedTerminal", 23 | "protocol": "auto", 24 | "restart": true, 25 | "port": 7001, 26 | "autoAttachChildProcesses": true 27 | }] 28 | } 29 | -------------------------------------------------------------------------------- /src/common/mail-service.ts: -------------------------------------------------------------------------------- 1 | import { Config, Provide, Singleton } from '@midwayjs/core'; 2 | import * as nodemailer from 'nodemailer'; 3 | import { MailConfig } from '../interface'; 4 | 5 | interface MailInfo { 6 | // 目标邮箱 7 | to: string; 8 | // 标题 9 | subject: string; 10 | // 文本 11 | text?: string; 12 | // 富文本,如果文本和富文本同时设置,富文本生效。 13 | html?: string; 14 | } 15 | 16 | @Provide() 17 | @Singleton() 18 | export class MailService { 19 | @Config('mail') 20 | mailConfig: MailConfig; 21 | 22 | async sendMail(mailInfo: MailInfo) { 23 | const transporter = nodemailer.createTransport(this.mailConfig); 24 | 25 | // 定义transport对象并发送邮件 26 | const info = await transporter.sendMail({ 27 | from: this.mailConfig.auth.user, // 发送方邮箱的账号 28 | ...mailInfo, 29 | }); 30 | 31 | return info; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/common-validate-rules.ts: -------------------------------------------------------------------------------- 1 | import { RuleType } from '@midwayjs/validate'; 2 | 3 | // 手机号 4 | export const phone = RuleType.string().pattern( 5 | /^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$/ 6 | ); 7 | 8 | // 邮箱 9 | export const email = RuleType.string().pattern( 10 | /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ 11 | ); 12 | 13 | // 字符串 14 | export const string = RuleType.string(); 15 | // 字符串不能为空 16 | export const requiredString = string.required(); 17 | // 字符串最大长度 18 | export const maxString = (length: number) => string.max(length); 19 | // 字符最小串长度 20 | export const minString = (length: number) => string.min(length); 21 | 22 | // 数字 23 | export const number = RuleType.number(); 24 | // 数字不能为空 25 | export const requiredNumber = number.required(); 26 | 27 | // bool 28 | export const bool = RuleType.bool(); 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gplane/pnpm:node20 as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY pnpm-lock.yaml . 6 | COPY package.json . 7 | 8 | RUN pnpm install 9 | 10 | COPY . . 11 | 12 | RUN pnpm run build 13 | 14 | FROM upshow/pm2:node20 15 | 16 | WORKDIR /app 17 | 18 | COPY --from=builder /app/package.json ./ 19 | COPY --from=builder /app/pnpm-lock.yaml ./ 20 | COPY --from=builder /app/node_modules ./node_modules 21 | ENV TZ="Asia/Shanghai" 22 | 23 | RUN npm install pnpm -g 24 | 25 | RUN pnpm install --prod 26 | 27 | COPY --from=builder /app/dist ./dist 28 | COPY --from=builder /app/bootstrap.js ./ 29 | COPY --from=builder /app/script ./script 30 | COPY --from=builder /app/src/config ./src/config 31 | COPY --from=builder /app/tsconfig.json ./ 32 | COPY --from=builder /app/src/migration ./src/migration 33 | 34 | EXPOSE 7001 35 | 36 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /src/module/system/role/dto/role.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule, RuleType } from '@midwayjs/validate'; 3 | import { BaseDTO } from '../../../../common/base-dto'; 4 | import { R } from '../../../../common/base-error-util'; 5 | import { requiredString } from '../../../../common/common-validate-rules'; 6 | import { RoleEntity } from '../entity/role'; 7 | 8 | export class RoleDTO extends BaseDTO { 9 | @ApiProperty({ description: '角色编码' }) 10 | @Rule(requiredString.error(R.validateError('代码不能为空'))) 11 | code?: string; 12 | 13 | @ApiProperty({ description: '角色名称' }) 14 | @Rule(requiredString.error(R.validateError('名称不能为空'))) 15 | name?: string; 16 | 17 | @ApiProperty({ 18 | description: '分配菜单列表', 19 | type: 'array', 20 | items: { type: 'string' }, 21 | }) 22 | @Rule(RuleType.array()) 23 | menuIds?: string[]; 24 | } 25 | -------------------------------------------------------------------------------- /src/module/system/auth/interface.ts: -------------------------------------------------------------------------------- 1 | interface BaseCaptchaOptions { 2 | // 验证码长度,默认4 3 | size?: number; 4 | // 干扰线条的数量,默认1 5 | noise?: number; 6 | // 宽度、高度 7 | width?: number; 8 | height?: number; 9 | color?: boolean; 10 | background?: string; 11 | } 12 | 13 | export interface CaptchaOptions extends BaseCaptchaOptions { 14 | default?: BaseCaptchaOptions; 15 | image?: ImageCaptchaOptions; 16 | formula?: FormulaCaptchaOptions; 17 | text?: TextCaptchaOptions; 18 | // 验证码过期时间,默认为 1h 19 | expirationTime?: number; 20 | // 验证码key 前缀 21 | idPrefix?: string; 22 | } 23 | 24 | export interface ImageCaptchaOptions extends BaseCaptchaOptions { 25 | type?: 'number' | 'letter' | 'mixed'; 26 | } 27 | 28 | export type FormulaCaptchaOptions = BaseCaptchaOptions; 29 | 30 | export interface TextCaptchaOptions { 31 | size?: number; 32 | type?: 'number' | 'letter' | 'mixed'; 33 | } 34 | -------------------------------------------------------------------------------- /src/autoload/casbin-watcher.ts: -------------------------------------------------------------------------------- 1 | import { IMidwayContainer, Inject, Singleton } from '@midwayjs/core'; 2 | import { ApplicationContext, Autoload, Init } from '@midwayjs/decorator'; 3 | 4 | import { createWatcher } from '@midwayjs/casbin-redis-adapter'; 5 | import { CasbinEnforcerService } from '@midwayjs/casbin'; 6 | 7 | @Autoload() 8 | @Singleton() 9 | export class MinioAutoLoad { 10 | @ApplicationContext() 11 | applicationContext: IMidwayContainer; 12 | @Inject() 13 | casbinEnforcerService: CasbinEnforcerService; 14 | 15 | @Init() 16 | async init() { 17 | const casbinWatcher = await createWatcher({ 18 | pubClientName: 'node-casbin-official', 19 | subClientName: 'node-casbin-sub', 20 | })(this.applicationContext); 21 | 22 | this.casbinEnforcerService.setWatcher(casbinWatcher); 23 | this.applicationContext.registerObject('casbinWatcher', casbinWatcher); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | import '@midwayjs/core'; 2 | 3 | interface UserContext { 4 | userId: string; 5 | refreshToken: string; 6 | } 7 | 8 | declare module '@midwayjs/core' { 9 | interface Context { 10 | userInfo: UserContext; 11 | token: string; 12 | requestStartTime: Date; 13 | } 14 | } 15 | 16 | declare module 'koa' { 17 | interface Context { 18 | userInfo: UserContext; 19 | token: string; 20 | } 21 | } 22 | 23 | export interface MinioConfig { 24 | endPoint: string; 25 | port: number; 26 | useSSL: boolean; 27 | accessKey: string; 28 | secretKey: string; 29 | bucketName: string; 30 | } 31 | 32 | export interface MailConfig { 33 | host: string; 34 | port: number; 35 | secure: boolean; 36 | auth: { 37 | user: string; 38 | pass: string; 39 | }; 40 | } 41 | 42 | export interface TokenConfig { 43 | expire: number; 44 | refreshExpire: number; 45 | } 46 | -------------------------------------------------------------------------------- /src/module/system/user/vo/user.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, getSchemaPath } from '@midwayjs/swagger'; 2 | import { FileEntity } from '../../file/entity/file'; 3 | import { RoleVO } from '../../role/vo/role'; 4 | 5 | export class UserVO { 6 | @ApiProperty({ description: '用户ID' }) 7 | id: string; 8 | 9 | @ApiProperty({ description: '用户名称' }) 10 | userName: string; 11 | 12 | @ApiProperty({ description: '用户昵称' }) 13 | nickName: string; 14 | 15 | @ApiProperty({ description: '手机号' }) 16 | phoneNumber: string; 17 | 18 | @ApiProperty({ description: '邮箱' }) 19 | email: string; 20 | 21 | @ApiProperty({ description: '头像地址' }) 22 | avatarPath: string; 23 | 24 | @ApiProperty({ description: '头像', type: FileEntity }) 25 | avatar?: FileEntity; 26 | 27 | @ApiProperty({ 28 | description: '角色列表', 29 | type: 'array', 30 | items: { $ref: getSchemaPath(RoleVO) }, 31 | }) 32 | roles: RoleVO[]; 33 | } 34 | -------------------------------------------------------------------------------- /src/module/system/auth/dto/login.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule, RuleType } from '@midwayjs/validate'; 3 | import { R } from '../../../../common/base-error-util'; 4 | import { requiredString } from '../../../../common/common-validate-rules'; 5 | 6 | export class LoginDTO { 7 | @ApiProperty({ 8 | description: '登录账号', 9 | }) 10 | @Rule(requiredString.error(R.validateError('登录账号不能为空'))) 11 | accountNumber?: string; 12 | @ApiProperty({ 13 | description: '登录密码', 14 | }) 15 | @Rule(requiredString.error(R.validateError('登录密码不能为空'))) 16 | password?: string; 17 | @ApiProperty({ 18 | description: '验证码key', 19 | }) 20 | @Rule(RuleType.string()) 21 | captchaId: string; 22 | @ApiProperty({ 23 | description: '验证码', 24 | }) 25 | @Rule(requiredString.error(R.validateError('验证码不能为空'))) 26 | captcha: string; 27 | @Rule(requiredString.error(R.validateError('公钥不能为空'))) 28 | publicKey: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/module/system/user/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { omit } from 'lodash'; 2 | import { Column, Entity } from 'typeorm'; 3 | import { BaseEntity } from '../../../../common/base-entity'; 4 | import { FileEntity } from '../../file/entity/file'; 5 | import { RoleEntity } from '../../role/entity/role'; 6 | import { UserVO } from '../vo/user'; 7 | 8 | @Entity('sys_user') 9 | export class UserEntity extends BaseEntity { 10 | @Column({ comment: '用户名称' }) 11 | userName: string; 12 | @Column({ comment: '用户昵称' }) 13 | nickName: string; 14 | @Column({ comment: '手机号' }) 15 | phoneNumber: string; 16 | @Column({ comment: '邮箱', nullable: true }) 17 | email: string; 18 | @Column({ comment: '密码' }) 19 | password: string; 20 | toVO(): UserVO { 21 | const userVO = omit(this, ['password']) as UserVO; 22 | userVO.avatarPath = this.avatar?.filePath || null; 23 | return userVO; 24 | } 25 | 26 | avatar?: FileEntity; 27 | roles: RoleEntity[]; 28 | } 29 | -------------------------------------------------------------------------------- /src/autoload/sentry.ts: -------------------------------------------------------------------------------- 1 | import { Singleton } from '@midwayjs/core'; 2 | import { Autoload, Init } from '@midwayjs/decorator'; 3 | import * as Sentry from '@sentry/node'; 4 | import { ProfilingIntegration } from '@sentry/profiling-node'; 5 | 6 | @Autoload() 7 | @Singleton() 8 | export class SentryAutoLoad { 9 | @Init() 10 | async init() { 11 | Sentry.init({ 12 | dsn: 'https://a13145093dc52e958bad4bf774c0a68f@o4505777802444800.ingest.sentry.io/4505839443443712', 13 | integrations: [ 14 | // Automatically instrument Node.js libraries and frameworks 15 | ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), 16 | new ProfilingIntegration(), 17 | ], 18 | // Performance Monitoring 19 | tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! 20 | // Set sampling rate for profiling - this is relative to tracesSampleRate 21 | profilesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/module/system/login-log/entity/login-log.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Column, Entity } from 'typeorm'; 3 | import { BaseEntity } from '../../../../common/base-entity'; 4 | 5 | @Entity('sys_login_log') 6 | export class LoginLogEntity extends BaseEntity { 7 | @ApiProperty({ description: '用户名' }) 8 | @Column({ comment: '用户名' }) 9 | userName?: string; 10 | 11 | @ApiProperty({ description: '登录ip' }) 12 | @Column({ comment: '登录ip' }) 13 | ip?: string; 14 | 15 | @ApiProperty({ description: '登录地点' }) 16 | @Column({ comment: '登录地点' }) 17 | address?: string; 18 | 19 | @ApiProperty({ description: '浏览器' }) 20 | @Column({ comment: '浏览器' }) 21 | browser?: string; 22 | 23 | @ApiProperty({ description: '操作系统' }) 24 | @Column({ comment: '操作系统' }) 25 | os?: string; 26 | 27 | @ApiProperty({ description: '登录状态' }) 28 | @Column({ comment: '登录状态' }) 29 | status?: boolean; 30 | 31 | @ApiProperty({ description: '登录消息' }) 32 | @Column({ comment: '登录消息' }) 33 | message?: string; 34 | } 35 | -------------------------------------------------------------------------------- /src/module/system/api-log/dto/api-log-page.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { PageDTO } from '../../../../common/page-dto'; 3 | 4 | export class ApiLogPageDTO extends PageDTO { 5 | @ApiProperty({ description: '请求地址' }) 6 | url?: string; 7 | 8 | @ApiProperty({ description: '请求方式' }) 9 | method?: string; 10 | 11 | @ApiProperty({ description: '是否成功' }) 12 | success?: boolean; 13 | 14 | @ApiProperty({ description: '请求开始时间起始时间' }) 15 | startTimeStart?: Date; 16 | 17 | @ApiProperty({ description: '请求开始时间结束时间' }) 18 | startTimeEnd?: Date; 19 | 20 | @ApiProperty({ description: '请求结束时间起始时间' }) 21 | endTimeStart?: Date; 22 | 23 | @ApiProperty({ description: '请求结束时间结束时间' }) 24 | endTimeEnd?: Date; 25 | 26 | @ApiProperty({ description: '耗时开始' }) 27 | durationStart?: number; 28 | 29 | @ApiProperty({ description: '耗时结束' }) 30 | durationEnd?: number; 31 | 32 | @ApiProperty({ description: '请求IP' }) 33 | ip?: string; 34 | 35 | @ApiProperty({ description: '错误码' }) 36 | errorType?: string; 37 | } 38 | -------------------------------------------------------------------------------- /src/task-queue/init-database.ts: -------------------------------------------------------------------------------- 1 | import { IProcessor, Processor } from '@midwayjs/bull'; 2 | import { App, Config } from '@midwayjs/core'; 3 | import * as koa from '@midwayjs/koa'; 4 | import * as Importer from 'mysql-import'; 5 | import * as path from 'path'; 6 | 7 | @Processor('init-database') 8 | export class InitDatabaseProcessor implements IProcessor { 9 | @Config('typeorm') 10 | typeormConfig: any; 11 | @App() 12 | app: koa.Application; 13 | 14 | async execute() { 15 | const { 16 | host, 17 | password, 18 | database, 19 | username: user, 20 | port, 21 | } = this.typeormConfig.dataSource.default; 22 | 23 | const importer = new Importer({ 24 | host, 25 | user, 26 | password, 27 | database, 28 | port, 29 | }); 30 | 31 | try { 32 | console.log('正在初始化数据库数据...'); 33 | await importer.import(path.join(this.app.getBaseDir(), './init.sql')); 34 | console.log('初始化数据库数据成功'); 35 | } catch (err) { 36 | console.error(err); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/module/system/user/dto/user.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Rule, RuleType } from '@midwayjs/validate'; 3 | import { BaseDTO } from '../../../../common/base-dto'; 4 | import { R } from '../../../../common/base-error-util'; 5 | import { 6 | email, 7 | phone, 8 | requiredString, 9 | } from '../../../../common/common-validate-rules'; 10 | import { UserEntity } from '../entity/user'; 11 | 12 | export class UserDTO extends BaseDTO { 13 | @ApiProperty({ description: '用户名称' }) 14 | @Rule(requiredString.error(R.validateError('用户名称不能为空'))) 15 | userName: string; 16 | @ApiProperty({ description: '用户昵称' }) 17 | @Rule(requiredString.error(R.validateError('用户昵称不能为空'))) 18 | nickName: string; 19 | @ApiProperty({ description: '手机号' }) 20 | @Rule(phone.error(R.validateError('无效的手机号格式'))) 21 | phoneNumber: string; 22 | @ApiProperty({ description: '邮箱' }) 23 | @Rule(email.error(R.validateError('无效的邮箱格式'))) 24 | email: string; 25 | @ApiProperty({ description: '头像' }) 26 | avatar?: string; 27 | @Rule(RuleType.array().items(RuleType.string())) 28 | roleIds?: string[]; 29 | } 30 | -------------------------------------------------------------------------------- /src/migration/1689004082896-migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class Migration1689004082896 implements MigrationInterface { 4 | name = 'Migration1689004082896'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | "ALTER TABLE `sys_menu` ADD `authCode` varchar(255) NOT NULL COMMENT '按钮权限代码'" 9 | ); 10 | await queryRunner.query( 11 | "ALTER TABLE `sys_menu` CHANGE `route` `route` varchar(255) NULL COMMENT '路由'" 12 | ); 13 | await queryRunner.query( 14 | "ALTER TABLE `sys_menu` CHANGE `orderNumber` `orderNumber` int NULL COMMENT '排序号'" 15 | ); 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | await queryRunner.query( 20 | "ALTER TABLE `sys_menu` CHANGE `orderNumber` `orderNumber` int NOT NULL COMMENT '排序号'" 21 | ); 22 | await queryRunner.query( 23 | "ALTER TABLE `sys_menu` CHANGE `route` `route` varchar(255) NOT NULL COMMENT '路由'" 24 | ); 25 | await queryRunner.query('ALTER TABLE `sys_menu` DROP COLUMN `authCode`'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/common/rsa-service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Provide, Singleton } from '@midwayjs/core'; 2 | import { RedisService } from '@midwayjs/redis'; 3 | import * as NodeRSA from 'node-rsa'; 4 | import { R } from './base-error-util'; 5 | 6 | @Provide() 7 | @Singleton() 8 | export class RSAService { 9 | @Inject() 10 | redisService: RedisService; 11 | 12 | async getPublicKey(): Promise { 13 | const key = new NodeRSA({ b: 512 }); 14 | const publicKey = key.exportKey('public'); 15 | const privateKey = key.exportKey('private'); 16 | await this.redisService.set(`publicKey:${publicKey}`, privateKey); 17 | return publicKey; 18 | } 19 | 20 | async decrypt(publicKey: string, data: string): Promise { 21 | const privateKey = await this.redisService.get(`publicKey:${publicKey}`); 22 | 23 | await this.redisService.del(`publicKey:${publicKey}`); 24 | 25 | if (!privateKey) { 26 | throw R.error('解密私钥错误或已失效'); 27 | } 28 | 29 | const decrypt = new NodeRSA(privateKey); 30 | decrypt.setOptions({ encryptionScheme: 'pkcs1', environment: 'browser' }); 31 | return decrypt.decrypt(data, 'utf8'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/config/config.prod.ts: -------------------------------------------------------------------------------- 1 | import { CasbinRule } from '@midwayjs/casbin-typeorm-adapter'; 2 | import { MidwayConfig } from '@midwayjs/core'; 3 | import { env } from 'process'; 4 | import { EverythingSubscriber } from '../typeorm-event-subscriber'; 5 | 6 | export default { 7 | typeorm: { 8 | dataSource: { 9 | default: { 10 | type: 'mysql', 11 | host: env.DB_HOST, 12 | port: env.DB_PORT ? Number(env.DB_PORT) : 3306, 13 | username: env.DB_USERNAME, 14 | password: env.DB_PASSWORD, 15 | database: env.DB_NAME, 16 | synchronize: false, 17 | logging: false, 18 | entities: ['**/entity/*{.ts,.js}', CasbinRule], 19 | timezone: '+00:00', 20 | migrations: ['**/migration/*.ts', CasbinRule], 21 | cli: { 22 | migrationsDir: 'migration', 23 | }, 24 | subscribers: [EverythingSubscriber], 25 | maxQueryExecutionTime: 10, 26 | }, 27 | }, 28 | }, 29 | // 密码重置回调地址 30 | resetPasswordCallbackUrl: 'https://dev.fluxyadmin.cn/', 31 | // 发送给用户邮件中登录地址 32 | loginUrl: 'https://dev.fluxyadmin.cn/user/login', 33 | } as MidwayConfig; 34 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import IP2Region from 'ip2region'; 2 | import { Context } from 'koa'; 3 | import * as useragent from 'useragent'; 4 | 5 | export const getIp = (ctx: Context) => { 6 | const ips = 7 | (ctx.req.headers['x-forwarded-for'] as string) || 8 | (ctx.req.headers['X-Real-IP'] as string) || 9 | (ctx.req.socket.remoteAddress.replace('::ffff:', '') as string); 10 | 11 | if (ips === '::1') return '127.0.0.1'; 12 | 13 | return ips.split(',')?.[0]; 14 | }; 15 | 16 | export const getAddressByIp = (ip: string): string => { 17 | if (!ip) return ''; 18 | 19 | const query = new IP2Region(); 20 | const res = query.search(ip); 21 | return [res.province, res.city].join(' '); 22 | }; 23 | 24 | export const getUserAgent = (ctx: Context): useragent.Agent => { 25 | return useragent.parse(ctx.headers['user-agent'] as string); 26 | }; 27 | 28 | /** 29 | * 获取不包含前缀的api 30 | * @param globalPrefix 前缀 31 | * @param url url 32 | * @returns url 33 | */ 34 | export const getUrlExcludeGlobalPrefix = ( 35 | globalPrefix: string, 36 | url: string 37 | ) => { 38 | if (url.startsWith(globalPrefix)) { 39 | return url.substring(globalPrefix.length); 40 | } 41 | 42 | return url; 43 | }; 44 | -------------------------------------------------------------------------------- /src/middleware/report.ts: -------------------------------------------------------------------------------- 1 | import { CasbinEnforcerService } from '@midwayjs/casbin'; 2 | import { 3 | Config, 4 | IMiddleware, 5 | Inject, 6 | Middleware, 7 | MidwayWebRouterService, 8 | RouterInfo, 9 | } from '@midwayjs/core'; 10 | import { Context, NextFunction } from '@midwayjs/koa'; 11 | import { RedisService } from '@midwayjs/redis'; 12 | import { ApiLogService } from '../module/system/api-log/service/api-log'; 13 | 14 | @Middleware() 15 | export class ReportMiddleware implements IMiddleware { 16 | @Inject() 17 | redisService: RedisService; 18 | @Inject() 19 | webRouterService: MidwayWebRouterService; 20 | @Inject() 21 | notLoginRouters: RouterInfo[]; 22 | @Inject() 23 | notAuthRouters: RouterInfo[]; 24 | @Config('koa.globalPrefix') 25 | globalPrefix: string; 26 | @Inject() 27 | casbinEnforcerService: CasbinEnforcerService; 28 | 29 | resolve() { 30 | return async (ctx: Context, next: NextFunction) => { 31 | ctx.requestStartTime = new Date(); 32 | 33 | await next(); 34 | 35 | const apiLogService = await ctx.requestContext.getAsync( 36 | ApiLogService 37 | ); 38 | 39 | apiLogService.createApiLog(ctx, true); 40 | }; 41 | } 42 | 43 | static getName(): string { 44 | return 'report'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/migration/1689772199745-migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class Migration1689772199745 implements MigrationInterface { 4 | name = 'Migration1689772199745'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | "CREATE TABLE `sys_login_log` (`id` bigint NOT NULL COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `userName` varchar(255) NOT NULL COMMENT '用户名', `ip` varchar(255) NOT NULL COMMENT '登录ip', `address` varchar(255) NOT NULL COMMENT '登录地点', `browser` varchar(255) NOT NULL COMMENT '浏览器', `os` varchar(255) NOT NULL COMMENT '操作系统', `status` tinyint NOT NULL COMMENT '登录状态', `message` varchar(255) NOT NULL COMMENT '登录消息', PRIMARY KEY (`id`)) ENGINE=InnoDB" 9 | ); 10 | await queryRunner.query( 11 | "ALTER TABLE `sys_menu` CHANGE `authCode` `authCode` varchar(255) NULL COMMENT '按钮权限代码'" 12 | ); 13 | } 14 | 15 | public async down(queryRunner: QueryRunner): Promise { 16 | await queryRunner.query( 17 | "ALTER TABLE `sys_menu` CHANGE `authCode` `authCode` varchar(255) NOT NULL COMMENT '按钮权限代码'" 18 | ); 19 | await queryRunner.query('DROP TABLE `sys_login_log`'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/migration/1703083742862-migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class Migration1703083742862 implements MigrationInterface { 4 | name = 'Migration1703083742862'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | "CREATE TABLE `sys_menu_version` (`id` bigint NOT NULL COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `menuId` varchar(255) NOT NULL COMMENT '菜单id', `version` varchar(255) NOT NULL COMMENT '版本号', `description` varchar(255) NOT NULL COMMENT '版本描述', PRIMARY KEY (`id`)) ENGINE=InnoDB" 9 | ); 10 | await queryRunner.query( 11 | "ALTER TABLE `sys_menu` ADD `curVersion` varchar(255) NULL COMMENT '低代码页面当前版本号'" 12 | ); 13 | await queryRunner.query( 14 | "ALTER TABLE `sys_menu` CHANGE `type` `type` int NOT NULL COMMENT '类型,1:目录 2:菜单 3:按钮 4:低代码页面'" 15 | ); 16 | } 17 | 18 | public async down(queryRunner: QueryRunner): Promise { 19 | await queryRunner.query( 20 | "ALTER TABLE `sys_menu` CHANGE `type` `type` int NOT NULL COMMENT '类型,1:目录 2:菜单'" 21 | ); 22 | await queryRunner.query('ALTER TABLE `sys_menu` DROP COLUMN `curVersion`'); 23 | await queryRunner.query('DROP TABLE `sys_menu_version`'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/module/system/menu/entity/menu.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { Column, Entity } from 'typeorm'; 3 | import { BaseEntity } from '../../../../common/base-entity'; 4 | 5 | @Entity('sys_menu') 6 | export class MenuEntity extends BaseEntity { 7 | @ApiProperty({ description: '上级id' }) 8 | @Column({ comment: '上级id', nullable: true }) 9 | parentId?: string; 10 | 11 | @ApiProperty({ description: '名称' }) 12 | @Column({ comment: '名称' }) 13 | name?: string; 14 | 15 | @ApiProperty({ description: '图标' }) 16 | @Column({ comment: '图标', nullable: true }) 17 | icon?: string; 18 | 19 | @ApiProperty({ description: '类型' }) 20 | @Column({ comment: '类型,1:目录 2:菜单 3:按钮 4:低代码页面' }) 21 | type?: number; 22 | 23 | @ApiProperty({ description: '路由' }) 24 | @Column({ comment: '路由', nullable: true }) 25 | route?: string; 26 | 27 | @ApiProperty({ description: '本地组件地址' }) 28 | @Column({ comment: '本地组件地址', nullable: true }) 29 | filePath?: string; 30 | 31 | @ApiProperty({ description: '排序号' }) 32 | @Column({ comment: '排序号', nullable: true }) 33 | orderNumber?: number; 34 | 35 | @ApiProperty({ description: 'url' }) 36 | @Column({ comment: 'url', nullable: true }) 37 | url?: string; 38 | 39 | @ApiProperty({ description: '是否在菜单中显示' }) 40 | @Column({ comment: '是否在菜单中显示' }) 41 | show?: boolean; 42 | 43 | @ApiProperty({ description: '按钮权限代码' }) 44 | @Column({ comment: '按钮权限代码', nullable: true }) 45 | authCode?: string; 46 | 47 | @ApiProperty({ description: '低代码页面当前版本号' }) 48 | @Column({ comment: '低代码页面当前版本号', nullable: true }) 49 | curVersion?: string; 50 | } 51 | -------------------------------------------------------------------------------- /src/decorator/not-auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IMidwayContainer, 3 | MidwayWebRouterService, 4 | Singleton, 5 | } from '@midwayjs/core'; 6 | import { 7 | ApplicationContext, 8 | attachClassMetadata, 9 | Autoload, 10 | CONTROLLER_KEY, 11 | getClassMetadata, 12 | Init, 13 | Inject, 14 | listModule, 15 | } from '@midwayjs/decorator'; 16 | 17 | // 提供一个唯一 key 18 | export const NOT_AUTH_KEY = 'decorator:not.auth'; 19 | 20 | export function NotAuth(): MethodDecorator { 21 | return (target, key, descriptor: PropertyDescriptor) => { 22 | attachClassMetadata(NOT_AUTH_KEY, { methodName: key }, target); 23 | return descriptor; 24 | }; 25 | } 26 | 27 | @Autoload() 28 | @Singleton() 29 | export class NotAuthDecorator { 30 | @Inject() 31 | webRouterService: MidwayWebRouterService; 32 | @ApplicationContext() 33 | applicationContext: IMidwayContainer; 34 | 35 | @Init() 36 | async init() { 37 | const controllerModules = listModule(CONTROLLER_KEY); 38 | const whiteMethods = []; 39 | for (const module of controllerModules) { 40 | const methodNames = getClassMetadata(NOT_AUTH_KEY, module) || []; 41 | const className = module.name[0].toLowerCase() + module.name.slice(1); 42 | whiteMethods.push( 43 | ...methodNames.map(method => `${className}.${method.methodName}`) 44 | ); 45 | } 46 | 47 | const routerTables = await this.webRouterService.getFlattenRouterTable(); 48 | const whiteRouters = routerTables.filter(router => 49 | whiteMethods.includes(router.handlerName) 50 | ); 51 | this.applicationContext.registerObject('notAuthRouters', whiteRouters); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/migration/1692155209836-migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class Migration1692155209836 implements MigrationInterface { 4 | name = 'Migration1692155209836'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | "CREATE TABLE `sys_menu_api` (`id` bigint NOT NULL COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `menuId` varchar(255) NOT NULL COMMENT '菜单id', `method` varchar(255) NOT NULL COMMENT '请求方式', `path` varchar(255) NOT NULL COMMENT 'path', PRIMARY KEY (`id`)) ENGINE=InnoDB" 9 | ); 10 | await queryRunner.query( 11 | `CREATE TABLE \`casbin_rule\` ( 12 | \`id\` int NOT NULL AUTO_INCREMENT, 13 | \`ptype\` varchar(255) DEFAULT NULL, 14 | \`v0\` varchar(255) DEFAULT NULL, 15 | \`v1\` varchar(255) DEFAULT NULL, 16 | \`v2\` varchar(255) DEFAULT NULL, 17 | \`v3\` varchar(255) DEFAULT NULL, 18 | \`v4\` varchar(255) DEFAULT NULL, 19 | \`v5\` varchar(255) DEFAULT NULL, 20 | \`v6\` varchar(255) DEFAULT NULL, 21 | PRIMARY KEY (\`id\`) 22 | ) ENGINE=InnoDB AUTO_INCREMENT=73 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;` 23 | ); 24 | } 25 | 26 | public async down(queryRunner: QueryRunner): Promise { 27 | await queryRunner.query('DROP TABLE `sys_menu_api`'); 28 | await queryRunner.query('DROP TABLE `casbin_rule`'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/decorator/not-login.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IMidwayContainer, 3 | MidwayWebRouterService, 4 | Singleton, 5 | } from '@midwayjs/core'; 6 | import { 7 | ApplicationContext, 8 | attachClassMetadata, 9 | Autoload, 10 | CONTROLLER_KEY, 11 | getClassMetadata, 12 | Init, 13 | Inject, 14 | listModule, 15 | } from '@midwayjs/decorator'; 16 | 17 | // 提供一个唯一 key 18 | export const NOT_LOGIN_KEY = 'decorator:not.login'; 19 | 20 | export function NotLogin(): MethodDecorator { 21 | return (target, key, descriptor: PropertyDescriptor) => { 22 | attachClassMetadata(NOT_LOGIN_KEY, { methodName: key }, target); 23 | return descriptor; 24 | }; 25 | } 26 | 27 | @Autoload() 28 | @Singleton() 29 | export class NotLoginDecorator { 30 | @Inject() 31 | webRouterService: MidwayWebRouterService; 32 | @ApplicationContext() 33 | applicationContext: IMidwayContainer; 34 | 35 | @Init() 36 | async init() { 37 | const controllerModules = listModule(CONTROLLER_KEY); 38 | const whiteMethods = []; 39 | for (const module of controllerModules) { 40 | const methodNames = getClassMetadata(NOT_LOGIN_KEY, module) || []; 41 | const className = module.name[0].toLowerCase() + module.name.slice(1); 42 | whiteMethods.push( 43 | ...methodNames.map(method => `${className}.${method.methodName}`) 44 | ); 45 | } 46 | 47 | const routerTables = await this.webRouterService.getFlattenRouterTable(); 48 | const whiteRouters = routerTables.filter(router => 49 | whiteMethods.includes(router.handlerName) 50 | ); 51 | this.applicationContext.registerObject('notLoginRouters', whiteRouters); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/module/system/auth/service/captcha.ts: -------------------------------------------------------------------------------- 1 | import { Provide, Inject, Config, Scope, ScopeEnum } from '@midwayjs/core'; 2 | import { CacheManager } from '@midwayjs/cache'; 3 | import * as svgCaptcha from 'svg-captcha'; 4 | import * as svgBase64 from 'mini-svg-data-uri'; 5 | import { FormulaCaptchaOptions, CaptchaOptions } from '../interface'; 6 | import { uuid } from '../../../../utils/uuid'; 7 | 8 | @Provide() 9 | @Scope(ScopeEnum.Singleton) 10 | export class CaptchaService { 11 | @Inject() 12 | cacheManager: CacheManager; 13 | 14 | @Config('captcha') 15 | captcha: CaptchaOptions; 16 | 17 | async formula(options?: FormulaCaptchaOptions) { 18 | const { data, text } = svgCaptcha.createMathExpr(options); 19 | const id = await this.set(text); 20 | const imageBase64 = svgBase64(data); 21 | return { id, imageBase64 }; 22 | } 23 | 24 | async set(text: string): Promise { 25 | const id = uuid(); 26 | await this.cacheManager.set( 27 | this.getStoreId(id), 28 | (text || '').toLowerCase(), 29 | { ttl: this.captcha.expirationTime } 30 | ); 31 | return id; 32 | } 33 | 34 | async check(id: string, value: string): Promise { 35 | if (!id || !value) { 36 | return false; 37 | } 38 | const storeId = this.getStoreId(id); 39 | const storedValue = await this.cacheManager.get(storeId); 40 | if (value.toLowerCase() !== storedValue) { 41 | return false; 42 | } 43 | this.cacheManager.del(storeId); 44 | return true; 45 | } 46 | 47 | private getStoreId(id: string): string { 48 | if (!this.captcha.idPrefix) { 49 | return id; 50 | } 51 | return `${this.captcha.idPrefix}:${id}`; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/module/system/socket/controller/socket.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Inject, 3 | OnWSConnection, 4 | OnWSDisConnection, 5 | OnWSMessage, 6 | WSController, 7 | } from '@midwayjs/core'; 8 | import { RedisService } from '@midwayjs/redis'; 9 | import { Context } from '@midwayjs/ws'; 10 | import * as http from 'http'; 11 | import { SocketService } from '../service/socket'; 12 | import { SocketMessage, SocketMessageType } from '../type'; 13 | 14 | @WSController() 15 | export class SocketConnectController { 16 | @Inject() 17 | ctx: Context; 18 | @Inject() 19 | redisService: RedisService; 20 | @Inject() 21 | socketService: SocketService; 22 | 23 | @OnWSConnection() 24 | async onConnectionMethod(socket: Context, request: http.IncomingMessage) { 25 | // 获取url上token参数 26 | const token = new URLSearchParams(request.url.split('?').pop()).get( 27 | 'token' 28 | ); 29 | 30 | if (!token) { 31 | socket.close(); 32 | return; 33 | } 34 | 35 | const userInfoStr = await this.redisService.get(`token:${token}`); 36 | if (!userInfoStr) { 37 | socket.send( 38 | JSON.stringify({ 39 | type: SocketMessageType.TokenExpire, 40 | }) 41 | ); 42 | socket.close(); 43 | return; 44 | } 45 | 46 | const userInfo = JSON.parse(userInfoStr); 47 | this.socketService.addConnect(userInfo.userId, socket); 48 | } 49 | 50 | @OnWSMessage('message') 51 | async gotMessage(data: Buffer) { 52 | // 接受前端发送过来的消息 53 | try { 54 | const message = JSON.parse(data.toString()) as SocketMessage; 55 | // 如果前端发送过来的消息时ping,那么就返回pong给前端 56 | if (message.type === SocketMessageType.Ping) { 57 | return { 58 | type: SocketMessageType.Pong, 59 | }; 60 | } 61 | } catch { 62 | console.error('json parse error'); 63 | } 64 | } 65 | 66 | @OnWSDisConnection() 67 | async disconnect() { 68 | // 客户端断开连接后,从全局connects移除 69 | this.socketService.deleteConnect(this.ctx); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/module/system/menu/dto/menu.ts: -------------------------------------------------------------------------------- 1 | import { ApiExtraModel, ApiProperty, getSchemaPath } from '@midwayjs/swagger'; 2 | import { Rule, RuleType, getSchema } from '@midwayjs/validate'; 3 | import { BaseDTO } from '../../../../common/base-dto'; 4 | import { R } from '../../../../common/base-error-util'; 5 | import { 6 | bool, 7 | number, 8 | requiredNumber, 9 | requiredString, 10 | string, 11 | } from '../../../../common/common-validate-rules'; 12 | import { ApiDTO } from '../../api/dto/api'; 13 | import { MenuEntity } from '../entity/menu'; 14 | 15 | @ApiExtraModel(ApiDTO) 16 | export class MenuDTO extends BaseDTO { 17 | @ApiProperty({ description: '父级菜单id' }) 18 | @Rule(string.allow(null)) 19 | parentId?: string; 20 | 21 | @ApiProperty({ description: '菜单名称' }) 22 | @Rule(requiredString.error(R.validateError('名称不能为空'))) 23 | name?: string; 24 | 25 | @ApiProperty({ description: '菜单icon' }) 26 | @Rule(string.allow(null)) 27 | icon?: string; 28 | 29 | @ApiProperty({ description: '菜单类型' }) 30 | @Rule(requiredNumber.error(R.validateError('类型不能为空'))) 31 | type?: number; 32 | 33 | @ApiProperty({ description: '菜单路由' }) 34 | @Rule(string.allow(null)) 35 | route?: string; 36 | 37 | @ApiProperty({ description: '本地组件地址' }) 38 | @Rule(string.allow(null)) 39 | filePath?: string; 40 | 41 | @ApiProperty({ description: '菜单排序' }) 42 | @Rule(number.allow(null)) 43 | orderNumber?: number; 44 | 45 | @ApiProperty({ description: '菜单url' }) 46 | @Rule(string.allow(null)) 47 | url?: string; 48 | 49 | @ApiProperty({ description: '是否在菜单中显示' }) 50 | @Rule(bool.allow(null)) 51 | show?: boolean; 52 | 53 | @ApiProperty({ description: '权限码' }) 54 | authCode?: string; 55 | 56 | @ApiProperty({ description: '低代码页面配置数据' }) 57 | @Rule(string.allow(null)) 58 | pageSetting?: string; 59 | 60 | @ApiProperty({ 61 | description: '已分配接口列表', 62 | type: 'array', 63 | items: { $ref: getSchemaPath(ApiDTO) }, 64 | }) 65 | @Rule(RuleType.array().items(getSchema(ApiDTO)).allow(null)) 66 | apis?: ApiDTO[]; 67 | } 68 | -------------------------------------------------------------------------------- /src/module/system/login-log/controller/login-log.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ALL, 3 | Body, 4 | Controller, 5 | Del, 6 | Get, 7 | Inject, 8 | Param, 9 | Post, 10 | Put, 11 | Query, 12 | } from '@midwayjs/decorator'; 13 | import { ApiOkResponse } from '@midwayjs/swagger'; 14 | import { AssertUtils } from '../../../../utils/assert'; 15 | import { FilterQuery } from '../../../../utils/filter-query'; 16 | import { like } from '../../../../utils/typeorm-utils'; 17 | import { LoginLogDTO } from '../dto/login-log'; 18 | import { LoginLogPageDTO } from '../dto/login-log-page'; 19 | import { LoginLogEntity } from '../entity/login-log'; 20 | import { LoginLogService } from '../service/login-log'; 21 | import { LoginLogVO } from '../vo/login-log'; 22 | 23 | @Controller('/login-log', { description: '登录日志' }) 24 | export class LoginLogController { 25 | @Inject() 26 | loginLogService: LoginLogService; 27 | 28 | @Post('/', { description: '新建' }) 29 | async create(@Body(ALL) data: LoginLogDTO) { 30 | return await this.loginLogService.create(data.toEntity()); 31 | } 32 | 33 | @Put('/', { description: '编辑' }) 34 | async edit(@Body(ALL) data: LoginLogDTO) { 35 | const loginLog = await this.loginLogService.getById(data.id); 36 | // update 37 | return await this.loginLogService.edit(loginLog); 38 | } 39 | 40 | @Del('/:id', { description: '删除' }) 41 | async remove(@Param('id') id: string) { 42 | AssertUtils.notEmpty(id, 'id不能为空'); 43 | await this.loginLogService.removeById(id); 44 | } 45 | 46 | @Get('/:id', { description: '根据id查询' }) 47 | async getById(@Param('id') id: string) { 48 | return await this.loginLogService.getById(id); 49 | } 50 | 51 | @Get('/page', { description: '分页查询' }) 52 | async page(@Query() pageInfo: LoginLogPageDTO) { 53 | const query = new FilterQuery(); 54 | query.append('userName', like(pageInfo.userName), !!pageInfo.userName); 55 | return await this.loginLogService.page(pageInfo, query); 56 | } 57 | 58 | @Get('/list', { description: '查询全部' }) 59 | @ApiOkResponse({ type: LoginLogVO }) 60 | async list() { 61 | return await this.loginLogService.list(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/common/base-service.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@midwayjs/decorator'; 2 | import { Context } from '@midwayjs/koa'; 3 | import { InjectDataSource } from '@midwayjs/typeorm'; 4 | import { DataSource, FindManyOptions, Repository } from 'typeorm'; 5 | import { BaseEntity } from './base-entity'; 6 | import { PageDTO } from './page-dto'; 7 | 8 | export abstract class BaseService { 9 | @Inject() 10 | ctx: Context; 11 | 12 | @InjectDataSource() 13 | defaultDataSource: DataSource; 14 | 15 | abstract getModel(): Repository; 16 | 17 | async create(entity: T): Promise { 18 | return await this.getModel().save(entity); 19 | } 20 | 21 | async edit(entity: T): Promise { 22 | return await this.getModel().save(entity); 23 | } 24 | 25 | async remove(entity: T) { 26 | await this.getModel().remove(entity); 27 | } 28 | 29 | async removeById(id: string) { 30 | await this.getModel() 31 | .createQueryBuilder() 32 | .delete() 33 | .where('id = :id', { id }) 34 | .execute(); 35 | } 36 | 37 | async getById(id: string): Promise { 38 | return await this.getModel() 39 | .createQueryBuilder('model') 40 | .where('model.id = :id', { id }) 41 | .getOne(); 42 | } 43 | 44 | async page(pageDTO: PageDTO, options: FindManyOptions = {}) { 45 | if (!options.order) { 46 | options.order = { createDate: 'DESC' } as any; 47 | } 48 | 49 | const pageInfo = this.getPageByPageDTO(pageDTO); 50 | 51 | const [data, total] = await this.getModel().findAndCount({ 52 | ...options, 53 | skip: pageInfo.skip, 54 | take: pageInfo.take, 55 | }); 56 | 57 | return { 58 | data: data.map(entity => entity.toVO()), 59 | total, 60 | }; 61 | } 62 | 63 | async list(options: FindManyOptions = {}) { 64 | if (!options.order) { 65 | options.order = { createDate: 'DESC' } as any; 66 | } 67 | 68 | const data = await this.getModel().find(options); 69 | 70 | return data; 71 | } 72 | 73 | /** 74 | * 获取分页参数 75 | */ 76 | getPageByPageDTO(pageInfo: PageDTO) { 77 | return { 78 | skip: pageInfo.page * pageInfo.size, 79 | take: pageInfo.size, 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/migration/1686884629903-migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class Migration1686884629903 implements MigrationInterface { 4 | name = 'Migration1686884629903'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | "CREATE TABLE `sys_user` (`id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `userName` varchar(255) NOT NULL COMMENT '用户名称', `nickName` varchar(255) NOT NULL COMMENT '用户昵称', `phoneNumber` varchar(255) NOT NULL COMMENT '手机号', `email` varchar(255) NOT NULL COMMENT '邮箱', `avatar` varchar(255) NULL COMMENT '头像', `sex` int NULL COMMENT '性别(0:女,1:男)', `password` varchar(255) NOT NULL COMMENT '密码', PRIMARY KEY (`id`)) ENGINE=InnoDB" 9 | ); 10 | await queryRunner.query( 11 | "CREATE TABLE `sys_file` (`id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `fileName` varchar(255) NOT NULL COMMENT '文件名', `filePath` varchar(255) NOT NULL COMMENT '文件路径', `pkName` varchar(255) NULL COMMENT '外健名称', `pkValue` int NULL COMMENT '外健值', PRIMARY KEY (`id`)) ENGINE=InnoDB" 12 | ); 13 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `avatar`'); 14 | await queryRunner.query( 15 | "ALTER TABLE `sys_user` ADD `avatar` int NULL COMMENT '头像'" 16 | ); 17 | await queryRunner.query( 18 | "insert into `sys_user` (userName, nickName, password, phoneNumber, email, id) values ('admin', '管理员', '$2a$10$.OggYJaVe1OCLVSB/9wqk.bYYaSdvcHu7dcc0zpewfpzNKEDPh2Tu', '18222222222', 'admin@qq.com', '1')" 19 | ); 20 | } 21 | 22 | public async down(queryRunner: QueryRunner): Promise { 23 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `avatar`'); 24 | await queryRunner.query( 25 | "ALTER TABLE `sys_user` ADD `avatar` varchar(255) NULL COMMENT '头像'" 26 | ); 27 | await queryRunner.query('DROP TABLE `sys_file`'); 28 | await queryRunner.query('DROP TABLE `sys_user`'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/module/system/api-log/entity/api-log.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@midwayjs/swagger'; 2 | import { omit } from 'lodash'; 3 | import { Column, Entity } from 'typeorm'; 4 | import { BaseEntity } from '../../../../common/base-entity'; 5 | import { UserEntity } from '../../user/entity/user'; 6 | 7 | @Entity('sys_api_log') 8 | export class ApiLogEntity extends BaseEntity { 9 | @ApiProperty({ description: '接口url' }) 10 | @Column({ comment: '接口url' }) 11 | url: string; 12 | 13 | @ApiProperty({ description: '请求方式' }) 14 | @Column({ comment: '请求方式' }) 15 | method: string; 16 | 17 | @ApiProperty({ description: '是否成功' }) 18 | @Column({ comment: '是否成功' }) 19 | success: boolean; 20 | 21 | @ApiProperty({ description: '请求query参数' }) 22 | @Column({ 23 | comment: '请求query参数', 24 | type: 'longtext', 25 | nullable: true, 26 | select: false, 27 | }) 28 | query?: string; 29 | 30 | @ApiProperty({ description: '请求body参数' }) 31 | @Column({ 32 | comment: '请求body参数', 33 | type: 'longtext', 34 | nullable: true, 35 | select: false, 36 | }) 37 | body?: string; 38 | 39 | @ApiProperty({ description: '开始时间' }) 40 | @Column({ comment: '开始时间' }) 41 | startTime: Date; 42 | 43 | @ApiProperty({ description: '结束时间' }) 44 | @Column({ comment: '结束时间' }) 45 | endTime: Date; 46 | 47 | @ApiProperty({ description: '耗时' }) 48 | @Column({ comment: '耗时' }) 49 | duration: number; 50 | 51 | @ApiProperty({ description: '响应结果' }) 52 | @Column({ 53 | comment: '响应结果', 54 | type: 'longtext', 55 | select: false, 56 | nullable: true, 57 | }) 58 | result?: string; 59 | 60 | @ApiProperty({ description: '请求ip' }) 61 | @Column({ comment: '请求ip' }) 62 | ip: string; 63 | 64 | @ApiProperty({ description: '错误类型' }) 65 | @Column({ comment: '错误类型', nullable: true }) 66 | errorType?: string; 67 | 68 | @ApiProperty({ description: '错误消息' }) 69 | @Column({ comment: '错误消息', nullable: true }) 70 | errorMsg?: string; 71 | 72 | @ApiProperty({ description: '用户Id' }) 73 | @Column({ comment: '用户Id', nullable: true }) 74 | userId?: string; 75 | 76 | user: UserEntity; 77 | 78 | @ApiProperty({ description: '用户名' }) 79 | userName?: string; 80 | 81 | toVO() { 82 | this.userName = this.user?.userName; 83 | return omit(this, ['user']); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/module/system/menu/controller/menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Del, 5 | Get, 6 | Inject, 7 | Param, 8 | Post, 9 | Put, 10 | Query, 11 | } from '@midwayjs/decorator'; 12 | import { ApiOkResponse } from '@midwayjs/swagger'; 13 | import { PageDTO } from '../../../../common/page-dto'; 14 | import { AssertUtils } from '../../../../utils/assert'; 15 | import { MenuDTO } from '../dto/menu'; 16 | import { MenuEntity } from '../entity/menu'; 17 | import { MenuApiEntity } from '../entity/menu-api'; 18 | import { MenuService } from '../service/menu'; 19 | import { MenuVO } from '../vo/menu'; 20 | import { MenuPageVO } from '../vo/menu-page'; 21 | 22 | @Controller('/menu', { description: '菜单管理' }) 23 | export class MenuController { 24 | @Inject() 25 | menuService: MenuService; 26 | 27 | @Get('/page', { description: '分页查询菜单' }) 28 | @ApiOkResponse({ 29 | type: MenuPageVO, 30 | }) 31 | async page(@Query() pageInfo: PageDTO) { 32 | return await this.menuService.getMenusByPage(pageInfo); 33 | } 34 | 35 | @Get('/children', { description: '根据上级菜单查询子级菜单' }) 36 | @ApiOkResponse({ 37 | type: MenuVO, 38 | isArray: true, 39 | }) 40 | async children(@Query('parentId') parentId: string) { 41 | return await this.menuService.getChildren(parentId); 42 | } 43 | 44 | @Post('/', { description: '创建一个菜单' }) 45 | async create(@Body() data: MenuDTO) { 46 | return await this.menuService.createMenu(data); 47 | } 48 | 49 | @Put('/', { description: '更新菜单' }) 50 | @ApiOkResponse({ 51 | type: MenuEntity, 52 | }) 53 | async update(@Body() data: MenuDTO) { 54 | return await this.menuService.updateMenu(data); 55 | } 56 | 57 | @Del('/:id', { description: '删除一个菜单' }) 58 | async remove( 59 | @Param('id') 60 | id: string 61 | ) { 62 | AssertUtils.notEmpty(id, 'id不能为空'); 63 | return await this.menuService.removeMenu(id); 64 | } 65 | 66 | @Get('/alloc/api/list', { description: '根据菜单查询已分配接口' }) 67 | @ApiOkResponse({ 68 | type: MenuApiEntity, 69 | isArray: true, 70 | }) 71 | async getAllocAPIByMenu(@Query('menuId') menuId: string) { 72 | return await this.menuService.getAllocAPIByMenu(menuId); 73 | } 74 | 75 | @ApiOkResponse({ 76 | type: MenuVO, 77 | isArray: true, 78 | }) 79 | @Get('/list', { description: '查询全量菜单' }) 80 | async list() { 81 | return await this.menuService.list(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/migration/1687610837273-migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class Migration1687610837273 implements MigrationInterface { 4 | name = 'Migration1687610837273'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | "ALTER TABLE `sys_user` CHANGE `id` `id` int NOT NULL COMMENT '主键'" 9 | ); 10 | await queryRunner.query('ALTER TABLE `sys_user` DROP PRIMARY KEY'); 11 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `id`'); 12 | await queryRunner.query( 13 | "ALTER TABLE `sys_user` ADD `id` bigint NOT NULL PRIMARY KEY COMMENT '主键'" 14 | ); 15 | await queryRunner.query( 16 | "ALTER TABLE `sys_file` CHANGE `id` `id` int NOT NULL COMMENT '主键'" 17 | ); 18 | await queryRunner.query('ALTER TABLE `sys_file` DROP PRIMARY KEY'); 19 | await queryRunner.query('ALTER TABLE `sys_file` DROP COLUMN `id`'); 20 | await queryRunner.query( 21 | "ALTER TABLE `sys_file` ADD `id` bigint NOT NULL PRIMARY KEY COMMENT '主键'" 22 | ); 23 | await queryRunner.query('ALTER TABLE `sys_file` DROP COLUMN `pkValue`'); 24 | await queryRunner.query( 25 | "ALTER TABLE `sys_file` ADD `pkValue` varchar(255) NULL COMMENT '外健值'" 26 | ); 27 | } 28 | 29 | public async down(queryRunner: QueryRunner): Promise { 30 | await queryRunner.query('ALTER TABLE `sys_file` DROP COLUMN `pkValue`'); 31 | await queryRunner.query( 32 | "ALTER TABLE `sys_file` ADD `pkValue` int NULL COMMENT '外健值'" 33 | ); 34 | await queryRunner.query('ALTER TABLE `sys_file` DROP COLUMN `id`'); 35 | await queryRunner.query( 36 | "ALTER TABLE `sys_file` ADD `id` int NOT NULL AUTO_INCREMENT COMMENT '主键'" 37 | ); 38 | await queryRunner.query('ALTER TABLE `sys_file` ADD PRIMARY KEY (`id`)'); 39 | await queryRunner.query( 40 | "ALTER TABLE `sys_file` CHANGE `id` `id` int NOT NULL AUTO_INCREMENT COMMENT '主键'" 41 | ); 42 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `id`'); 43 | await queryRunner.query( 44 | "ALTER TABLE `sys_user` ADD `id` int NOT NULL AUTO_INCREMENT COMMENT '主键'" 45 | ); 46 | await queryRunner.query('ALTER TABLE `sys_user` ADD PRIMARY KEY (`id`)'); 47 | await queryRunner.query( 48 | "ALTER TABLE `sys_user` CHANGE `id` `id` int NOT NULL AUTO_INCREMENT COMMENT '主键'" 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /script/template/controller.template: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Del, 5 | Get, 6 | Inject, 7 | Param, 8 | Post, 9 | Put, 10 | Query, 11 | } from '@midwayjs/core'; 12 | import { ApiOkResponse } from '@midwayjs/swagger'; 13 | import { $1DTO } from '../dto/$2'; 14 | import { $1Service } from '../service/$2'; 15 | import { AssertUtils } from '../../../../utils/assert'; 16 | import { FilterQuery } from '../../../../utils/filter-query'; 17 | import { $1PageVO } from '../vo/$2-page'; 18 | import { $1PageDTO } from '../dto/$2-page'; 19 | import { $1Entity } from '../entity/$2'; 20 | import { $1VO } from '../vo/$2'; 21 | import { like } from '../../../../utils/typeorm-utils'; 22 | 23 | @Controller('/$4', { description: '$5' }) 24 | export class $1Controller { 25 | @Inject() 26 | $3Service: $1Service; 27 | 28 | @Get('/page', { description: '分页查询' }) 29 | @ApiOkResponse({ 30 | type: $1PageVO, 31 | }) 32 | async page(@Query() $3PageDTO: $1PageDTO) { 33 | const filterQuery = new FilterQuery<$1Entity>(); 34 | 35 | filterQuery 36 | .append('code', like($3PageDTO.code), !!$3PageDTO.code) 37 | .append('name', like($3PageDTO.name), !!$3PageDTO.name); 38 | 39 | return await this.$3Service.page($3PageDTO, { 40 | where: filterQuery.where, 41 | order: { createDate: 'DESC' }, 42 | }); 43 | } 44 | 45 | @Post('/', { description: '新建' }) 46 | async create(@Body() data: $1DTO) { 47 | return await this.$3Service.create(data.toEntity()); 48 | } 49 | 50 | @Put('/', { description: '编辑' }) 51 | async edit(@Body() data: $1DTO) { 52 | AssertUtils.notEmpty(data.id, 'id不能为空'); 53 | return await this.$3Service.edit(data.toEntity()); 54 | } 55 | 56 | @Del('/:id', { description: '删除' }) 57 | async remove(@Param('id') id: string) { 58 | AssertUtils.notEmpty(id, 'id不能为空'); 59 | await this.$3Service.removeById(id); 60 | } 61 | 62 | @Get('/:id', { description: '根据id查询' }) 63 | async getById(@Param('id') id: string) { 64 | return await this.$3Service.getById(id); 65 | } 66 | 67 | @Get('/list', { description: '全部列表' }) 68 | @ApiOkResponse({ 69 | type: $1VO, 70 | isArray: true, 71 | }) 72 | async list(@Query() $3PageDTO: $1PageDTO) { 73 | const filterQuery = new FilterQuery<$1Entity>(); 74 | 75 | return await this.$3Service.list({ 76 | where: filterQuery.where, 77 | order: { createDate: 'DESC' }, 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/module/system/api/service/api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CONTROLLER_KEY, 3 | Config, 4 | Inject, 5 | MidwayWebRouterService, 6 | Provide, 7 | RouterInfo, 8 | getClassMetadata, 9 | listModule, 10 | } from '@midwayjs/core'; 11 | 12 | @Provide() 13 | export class ApiService { 14 | @Config('koa') 15 | koaConfig: any; 16 | @Inject() 17 | webRouterService: MidwayWebRouterService; 18 | @Inject() 19 | notLoginRouters: RouterInfo[]; 20 | @Inject() 21 | notAuthRouters: RouterInfo[]; 22 | 23 | // 按获取所有controller获取接口列表 24 | async getApiList() { 25 | // 获取所有controller 26 | const controllerModules = listModule(CONTROLLER_KEY); 27 | 28 | const list = []; 29 | 30 | // 遍历controller,获取controller的信息存到list数组中 31 | for (const module of controllerModules) { 32 | const controllerInfo = getClassMetadata(CONTROLLER_KEY, module) || []; 33 | list.push({ 34 | title: 35 | controllerInfo?.routerOptions?.description || controllerInfo?.prefix, 36 | path: `${this.koaConfig.globalPrefix}${controllerInfo?.prefix}`, 37 | prefix: controllerInfo?.prefix, 38 | type: 'controller', 39 | }); 40 | } 41 | 42 | // 获取所有接口 43 | let routes = await this.webRouterService.getFlattenRouterTable(); 44 | 45 | // 把不用登录和鉴权的接口过滤掉 46 | routes = routes 47 | .filter( 48 | route => 49 | !this.notLoginRouters.some( 50 | r => r.url === route.url && r.requestMethod === route.requestMethod 51 | ) 52 | ) 53 | .filter( 54 | route => 55 | !this.notAuthRouters.some( 56 | r => r.url === route.url && r.requestMethod === route.requestMethod 57 | ) 58 | ); 59 | 60 | // 把接口按照controller分组 61 | const routesGroup = routes.reduce((prev, cur) => { 62 | if (prev[cur.prefix]) { 63 | prev[cur.prefix].push(cur); 64 | } else { 65 | prev[cur.prefix] = [cur]; 66 | } 67 | return prev; 68 | }, {}); 69 | 70 | // 返回controller和接口信息 71 | return list 72 | .map(item => { 73 | if (!routesGroup[item.path]?.length) { 74 | return null; 75 | } 76 | return { 77 | ...item, 78 | children: routesGroup[item.path]?.map(o => ({ 79 | title: o.description || o.url, 80 | path: o.url, 81 | method: o.requestMethod, 82 | type: 'route', 83 | })), 84 | }; 85 | }) 86 | .filter(o => !!o); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/module/system/socket/service/socket.ts: -------------------------------------------------------------------------------- 1 | import { Autoload, Init, InjectClient, Singleton } from '@midwayjs/core'; 2 | import { RedisService, RedisServiceFactory } from '@midwayjs/redis'; 3 | import { Context } from '@midwayjs/ws'; 4 | import { SocketMessage } from '../type'; 5 | 6 | const socketChannel = 'socket-message'; 7 | 8 | @Singleton() 9 | @Autoload() 10 | export class SocketService { 11 | connects = new Map(); 12 | @InjectClient(RedisServiceFactory, 'publish') 13 | publishRedisService: RedisService; 14 | @InjectClient(RedisServiceFactory, 'subscribe') 15 | subscribeRedisService: RedisService; 16 | 17 | @Init() 18 | async init() { 19 | // 系统启动的时候,这个方法会自动执行,监听频道。 20 | await this.subscribeRedisService.subscribe(socketChannel); 21 | 22 | // 如果接受到消息,通过userId获取连接,如果存在,通过连接给前端发消息 23 | this.subscribeRedisService.on( 24 | 'message', 25 | (channel: string, message: string) => { 26 | if (channel === socketChannel && message) { 27 | const messageData = JSON.parse(message); 28 | 29 | const { userId, data } = messageData; 30 | const clients = this.connects.get(userId); 31 | 32 | if (clients?.length) { 33 | clients.forEach(client => { 34 | client.send(JSON.stringify(data)); 35 | }); 36 | } 37 | } 38 | } 39 | ); 40 | } 41 | 42 | /** 43 | * 添加连接 44 | * @param userId 用户id 45 | * @param connect 用户socket连接 46 | */ 47 | addConnect(userId: string, connect: Context) { 48 | const curConnects = this.connects.get(userId); 49 | if (curConnects) { 50 | curConnects.push(connect); 51 | } else { 52 | this.connects.set(userId, [connect]); 53 | } 54 | } 55 | 56 | /** 57 | * 删除连接 58 | * @param connect 用户socket连接 59 | */ 60 | deleteConnect(connect: Context) { 61 | const connects = [...this.connects.values()]; 62 | 63 | for (let i = 0; i < connects.length; i += 1) { 64 | const sockets = connects[i]; 65 | const index = sockets.indexOf(connect); 66 | if (index >= 0) { 67 | sockets.splice(index, 1); 68 | break; 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * 给指定用户发消息 75 | * @param userId 用户id 76 | * @param data 数据 77 | */ 78 | sendMessage(userId: string, data: SocketMessage) { 79 | this.publishRedisService.publish( 80 | socketChannel, 81 | JSON.stringify({ 82 | userId, 83 | data, 84 | }) 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/module/system/file/service/file.ts: -------------------------------------------------------------------------------- 1 | import { Config, Inject, Provide } from '@midwayjs/decorator'; 2 | import { InjectDataSource, InjectEntityModel } from '@midwayjs/typeorm'; 3 | import { UploadFileInfo } from '@midwayjs/upload'; 4 | import { DataSource, Repository } from 'typeorm'; 5 | import { MinioClient } from '../../../../autoload/minio'; 6 | import { BaseService } from '../../../../common/base-service'; 7 | import { MinioConfig } from '../../../../interface'; 8 | import { FileEntity } from '../entity/file'; 9 | 10 | @Provide() 11 | export class FileService extends BaseService { 12 | @InjectEntityModel(FileEntity) 13 | fileModel: Repository; 14 | @Inject() 15 | minioClient: MinioClient; 16 | @Config('minio') 17 | minioConfig: MinioConfig; 18 | @InjectDataSource() 19 | defaultDataSource: DataSource; 20 | 21 | getModel(): Repository { 22 | return this.fileModel; 23 | } 24 | 25 | // 上传方法 26 | async upload(file: UploadFileInfo) { 27 | const fileName = `${new Date().getTime()}_${file.filename}`; 28 | 29 | const data = await this.defaultDataSource.transaction(async manager => { 30 | const fileEntity = new FileEntity(); 31 | fileEntity.fileName = fileName; 32 | fileEntity.filePath = `/file/${this.minioConfig.bucketName}/${fileName}`; 33 | await manager.save(FileEntity, fileEntity); 34 | 35 | await this.minioClient.fPutObject( 36 | this.minioConfig.bucketName, 37 | fileName, 38 | file.data 39 | ); 40 | 41 | return fileEntity; 42 | }); 43 | 44 | return data; 45 | } 46 | 47 | // 上传单据时,把单据id注入进去 48 | async setPKValue(id: string, pkValue: string, pkName: string) { 49 | const entity = await this.getById(id); 50 | if (!entity) return; 51 | entity.pkValue = pkValue; 52 | entity.pkName = pkName; 53 | await this.fileModel.save(entity); 54 | return entity; 55 | } 56 | 57 | // 清理脏数据 58 | async clearEmptyPKValueFiles() { 59 | const curDate = new Date(); 60 | curDate.setDate(curDate.getDate() - 1); 61 | 62 | const records = await this.fileModel 63 | .createQueryBuilder() 64 | .where('createDate < :date', { date: curDate }) 65 | .andWhere('pkValue is null') 66 | .getMany(); 67 | 68 | this.defaultDataSource.transaction(async manager => { 69 | await manager.remove(FileEntity, records); 70 | await Promise.all( 71 | records.map(record => 72 | this.minioClient.removeObject( 73 | this.minioConfig.bucketName, 74 | record.fileName 75 | ) 76 | ) 77 | ); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { CasbinEnforcerService } from '@midwayjs/casbin'; 2 | import { 3 | Config, 4 | IMiddleware, 5 | Inject, 6 | Middleware, 7 | MidwayWebRouterService, 8 | RouterInfo, 9 | } from '@midwayjs/core'; 10 | import { Context, NextFunction } from '@midwayjs/koa'; 11 | import { RedisService } from '@midwayjs/redis'; 12 | import { R } from '../common/base-error-util'; 13 | import { getUrlExcludeGlobalPrefix } from '../utils/utils'; 14 | 15 | @Middleware() 16 | export class AuthMiddleware implements IMiddleware { 17 | @Inject() 18 | redisService: RedisService; 19 | @Inject() 20 | webRouterService: MidwayWebRouterService; 21 | @Inject() 22 | notLoginRouters: RouterInfo[]; 23 | @Inject() 24 | notAuthRouters: RouterInfo[]; 25 | @Config('koa.globalPrefix') 26 | globalPrefix: string; 27 | @Inject() 28 | casbinEnforcerService: CasbinEnforcerService; 29 | 30 | resolve() { 31 | return async (ctx: Context, next: NextFunction) => { 32 | const routeInfo = await this.webRouterService.getMatchedRouterInfo( 33 | ctx.path, 34 | ctx.method 35 | ); 36 | 37 | if (!routeInfo) { 38 | await next(); 39 | return; 40 | } 41 | 42 | if ( 43 | this.notLoginRouters.some( 44 | o => 45 | o.prefix === routeInfo.prefix && 46 | o.requestMethod === routeInfo.requestMethod && 47 | o.url === routeInfo.url 48 | ) 49 | ) { 50 | await next(); 51 | return; 52 | } 53 | 54 | const token = ctx.header.authorization?.replace('Bearer ', ''); 55 | if (!token) { 56 | throw R.unauthorizedError('未授权'); 57 | } 58 | 59 | const userInfoStr = await this.redisService.get(`token:${token}`); 60 | if (!userInfoStr) { 61 | throw R.unauthorizedError('未授权'); 62 | } 63 | 64 | const userInfo = JSON.parse(userInfoStr); 65 | 66 | ctx.userInfo = userInfo; 67 | ctx.token = token; 68 | 69 | // 过滤掉不需要鉴权的接口 70 | if ( 71 | this.notAuthRouters.some( 72 | o => 73 | o.requestMethod === routeInfo.requestMethod && 74 | o.url === routeInfo.url 75 | ) 76 | ) { 77 | await next(); 78 | return; 79 | } 80 | 81 | const matched = await this.casbinEnforcerService.enforce( 82 | ctx.userInfo.userId, 83 | getUrlExcludeGlobalPrefix(this.globalPrefix, routeInfo.fullUrl), 84 | routeInfo.requestMethod 85 | ); 86 | 87 | if (!matched && ctx.userInfo.userId !== '1') { 88 | throw R.forbiddenError('你没有访问该资源的权限'); 89 | } 90 | 91 | return next(); 92 | }; 93 | } 94 | 95 | static getName(): string { 96 | return 'auth'; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/module/system/role/controller/role.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Del, 5 | Get, 6 | Inject, 7 | Param, 8 | Post, 9 | Put, 10 | Query, 11 | } from '@midwayjs/decorator'; 12 | import { ApiOkResponse } from '@midwayjs/swagger'; 13 | import { AssertUtils } from '../../../../utils/assert'; 14 | import { FilterQuery } from '../../../../utils/filter-query'; 15 | import { like } from '../../../../utils/typeorm-utils'; 16 | import { RoleDTO } from '../dto/role'; 17 | import { RoleMenuDTO } from '../dto/role-menu'; 18 | import { RolePageDTO } from '../dto/role-page'; 19 | import { RoleEntity } from '../entity/role'; 20 | import { RoleService } from '../service/role'; 21 | import { RolePageVO } from '../vo/role-page'; 22 | 23 | @Controller('/role', { description: '角色管理' }) 24 | export class RoleController { 25 | @Inject() 26 | roleService: RoleService; 27 | 28 | @Get('/page', { description: '分页获取角色列表' }) 29 | @ApiOkResponse({ 30 | type: RolePageVO, 31 | }) 32 | async page(@Query() rolePageDTO: RolePageDTO) { 33 | const filterQuery = new FilterQuery(); 34 | const { name, code } = rolePageDTO; 35 | 36 | filterQuery 37 | .append('name', like(name), !!name) 38 | .append('code', like(code), !!code); 39 | 40 | return await this.roleService.page(rolePageDTO, { 41 | where: filterQuery.where, 42 | order: { createDate: 'DESC' }, 43 | }); 44 | } 45 | 46 | @Post('/', { description: '创建角色' }) 47 | @ApiOkResponse({ 48 | type: RoleEntity, 49 | }) 50 | async create(@Body() data: RoleDTO) { 51 | return await this.roleService.createRole(data); 52 | } 53 | 54 | @Put('/', { description: '更新角色' }) 55 | @ApiOkResponse({ 56 | type: RoleEntity, 57 | }) 58 | async update(@Body() data: RoleDTO) { 59 | return await this.roleService.editRole(data); 60 | } 61 | 62 | @Del('/:id', { description: '删除角色' }) 63 | async remove( 64 | @Param('id') 65 | id: string 66 | ) { 67 | AssertUtils.notEmpty(id, 'id不能为空'); 68 | return await this.roleService.removeRole(id); 69 | } 70 | 71 | @Post('/alloc/menu', { description: '角色分配菜单' }) 72 | async allocMenu(@Body() roleMenuDTO: RoleMenuDTO) { 73 | return await this.roleService.allocMenu( 74 | roleMenuDTO.roleId, 75 | roleMenuDTO.menuIds 76 | ); 77 | } 78 | 79 | @Get('/menu/list', { description: '根据角色id获取菜单id列表' }) 80 | @ApiOkResponse({ 81 | type: String, 82 | isArray: true, 83 | }) 84 | async getMenusByRoleId(@Query('id') id: string) { 85 | return (await this.roleService.getMenusByRoleId(id)).map(o => o.menuId); 86 | } 87 | 88 | @Get('/list', { description: '分页获取角色列表' }) 89 | @ApiOkResponse({ type: RoleEntity, isArray: true }) 90 | async list() { 91 | return await this.roleService.list(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/module/system/user/controller/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Config, 4 | Controller, 5 | Del, 6 | Get, 7 | Inject, 8 | Param, 9 | Post, 10 | Put, 11 | Query, 12 | } from '@midwayjs/decorator'; 13 | import { RedisService } from '@midwayjs/redis'; 14 | import { ApiOkResponse } from '@midwayjs/swagger'; 15 | import { RuleType, Valid } from '@midwayjs/validate'; 16 | import { R } from '../../../../common/base-error-util'; 17 | import { MailService } from '../../../../common/mail-service'; 18 | import { generateRandomCode } from '../../../../utils/uuid'; 19 | import { FileService } from '../../file/service/file'; 20 | import { UserDTO } from '../dto/user'; 21 | import { UserPageDTO } from '../dto/user-page'; 22 | import { UserService } from '../service/user'; 23 | import { UserPageVO } from '../vo/user-page'; 24 | 25 | @Controller('/user', { description: '用户管理' }) 26 | export class UserController { 27 | @Inject() 28 | userService: UserService; 29 | @Inject() 30 | fileService: FileService; 31 | @Inject() 32 | mailService: MailService; 33 | @Inject() 34 | redisService: RedisService; 35 | @Config('title') 36 | title: string; 37 | 38 | @Get('/page', { description: '分页查询' }) 39 | @ApiOkResponse({ 40 | type: UserPageVO, 41 | }) 42 | async page(@Query() menuPageDTO: UserPageDTO) { 43 | return await this.userService.getUsersByPage(menuPageDTO); 44 | } 45 | 46 | @Post('/', { description: '创建用户' }) 47 | async create(@Body() data: UserDTO) { 48 | return await this.userService.createUser(data); 49 | } 50 | 51 | @Put('/', { description: '更新用户' }) 52 | async update(@Body() data: UserDTO) { 53 | return await this.userService.updateUser(data); 54 | } 55 | 56 | @Del('/:id', { description: '删除' }) 57 | async remove( 58 | @Valid(RuleType.string().required().error(R.error('id不能为空'))) 59 | @Param('id') 60 | id: string 61 | ) { 62 | await this.userService.removeUser(id); 63 | } 64 | 65 | @Get('/:id', { description: '根据id查询' }) 66 | async getById( 67 | @Param('id') 68 | id: string 69 | ) { 70 | return await this.userService.getById(id); 71 | } 72 | 73 | @Post('/send/email/captcha', { description: '发送邮箱验证码' }) 74 | async sendEmailCaptcha(@Body() emailInfo: { email: string }) { 75 | if (!emailInfo.email) { 76 | throw R.error('邮箱不能为空'); 77 | } 78 | 79 | // 生成随机4位数 80 | const emailCaptcha = generateRandomCode(); 81 | // 把生成的随机数存到redis中,后面添加用户的时候需要做验证 82 | await this.redisService.set( 83 | `emailCaptcha:${emailInfo.email}`, 84 | emailCaptcha, 85 | 'EX', 86 | 60 * 30 // 30分钟 87 | ); 88 | 89 | // 这里邮件内容支持html,后面会做一个在线自定义邮件模版功能,就不用写死在代码里了。 90 | this.mailService.sendMail({ 91 | to: emailInfo.email, 92 | html: `
93 | 您本次的验证码是${emailCaptcha},验证码有效期为30分钟。 94 | { 13 | @InjectEntityModel(ApiLogEntity) 14 | apiLogModel: Repository; 15 | 16 | getModel(): Repository { 17 | return this.apiLogModel; 18 | } 19 | 20 | async page( 21 | pageDTO: PageDTO, 22 | options?: FindManyOptions 23 | ): Promise<{ data: any[]; total: number }> { 24 | const [data, total] = await this.apiLogModel 25 | .createQueryBuilder('apiLog') 26 | .leftJoinAndMapOne( 27 | 'apiLog.user', 28 | UserEntity, 29 | 'user', 30 | 'apiLog.userId = user.id' 31 | ) 32 | .select(['apiLog']) 33 | .addSelect('user.userName') 34 | .take(pageDTO.size) 35 | .skip(pageDTO.page * pageDTO.size) 36 | .orderBy('apiLog.createDate', 'DESC') 37 | .addOrderBy('apiLog.updateDate', 'DESC') 38 | .where(options?.where) 39 | .getManyAndCount(); 40 | 41 | return { 42 | data: data.map(o => o.toVO()), 43 | total, 44 | }; 45 | } 46 | 47 | // 通过id查询请求query参数 48 | async getQueryData(id: string) { 49 | return await this.apiLogModel.findOne({ 50 | where: { id }, 51 | select: ['id', 'query'], 52 | }); 53 | } 54 | 55 | // 通过id查询请求body参数 56 | async getBodyData(id: string) { 57 | return await this.apiLogModel.findOne({ 58 | where: { id }, 59 | select: ['id', 'body'], 60 | }); 61 | } 62 | 63 | // 通过id查询响应结果 64 | async getResultData(id: string) { 65 | return await this.apiLogModel.findOne({ 66 | where: { id }, 67 | select: ['id', 'result'], 68 | }); 69 | } 70 | 71 | // 创建api日志 72 | async createApiLog( 73 | ctx: Context, 74 | success: boolean, 75 | errorType?: string, 76 | errorMsg?: string 77 | ) { 78 | const apiLog = new ApiLogEntity(); 79 | apiLog.url = ctx.path; 80 | apiLog.method = ctx.method; 81 | apiLog.duration = 82 | new Date().getTime() - 83 | (ctx.requestStartTime?.getTime() || new Date().getTime()); 84 | apiLog.startTime = ctx.requestStartTime || new Date(); 85 | apiLog.endTime = new Date(); 86 | apiLog.ip = getIp(ctx); 87 | apiLog.query = JSON.stringify(ctx.request.query); 88 | apiLog.body = JSON.stringify(ctx.request.body); 89 | apiLog.result = JSON.stringify(ctx.body); 90 | apiLog.userId = ctx.userInfo?.userId; 91 | 92 | apiLog.success = success; 93 | apiLog.errorType = errorType; 94 | apiLog.errorMsg = errorMsg; 95 | 96 | return await this.create(apiLog); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/utils/snow-flake.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'process'; 2 | 3 | export class SnowFlake { 4 | // 系统上线的时间戳,我这里设置为 2023-06-22 00:00:00 的时间戳 5 | epoch = BigInt(1687392000000); 6 | 7 | // 数据中心的位数 8 | dataCenterIdBits = 5; 9 | // 机器id的位数 10 | workerIdBits = 5; 11 | // 自增序列号的位数 12 | sequenceBits = 12; 13 | 14 | // 最大的数据中心id 这段位运算可以理解为2^5-1 = 31 15 | maxDataCenterId = (BigInt(1) << BigInt(this.workerIdBits)) - BigInt(1); 16 | // 最大的机器id 这段位运算可以理解为2^5-1 = 31 17 | maxWorkerId = (BigInt(1) << BigInt(this.workerIdBits)) - BigInt(1); 18 | 19 | // 时间戳偏移位数 20 | timestampShift = BigInt( 21 | this.dataCenterIdBits + this.workerIdBits + this.sequenceBits 22 | ); 23 | 24 | // 数据中心偏移位数 25 | dataCenterIdShift = BigInt(this.workerIdBits + this.sequenceBits); 26 | // 机器id偏移位数 27 | workerIdShift = BigInt(this.sequenceBits); 28 | // 自增序列号的掩码 29 | sequenceMask = (BigInt(1) << BigInt(this.sequenceBits)) - BigInt(1); 30 | // 记录上次生成id的时间戳 31 | lastTimestamp = BigInt(-1); 32 | // 数据中心id 33 | dataCenterId = BigInt(0); 34 | // 机器id 35 | workerId = BigInt(0); 36 | // 自增序列号 37 | sequence = BigInt(0); 38 | constructor(dataCenterId: number, workerId: number) { 39 | // 校验数据中心 ID 和工作节点 ID 的范围 40 | if (dataCenterId > this.maxDataCenterId || dataCenterId < 0) { 41 | throw new Error( 42 | `Data center ID must be between 0 and ${this.maxDataCenterId}` 43 | ); 44 | } 45 | 46 | if (workerId > this.maxWorkerId || workerId < 0) { 47 | throw new Error(`Worker ID must be between 0 and ${this.maxWorkerId}`); 48 | } 49 | 50 | this.dataCenterId = BigInt(dataCenterId); 51 | this.workerId = BigInt(workerId); 52 | } 53 | 54 | nextId() { 55 | let timestamp = BigInt(Date.now()); 56 | // 如果上一次生成id的时间戳比下一次生成的还大,说明服务器时间有问题,出现了回退,这时候再生成id,可能会生成重复的id,所以直接抛出异常。 57 | if (timestamp < this.lastTimestamp) { 58 | // 时钟回拨,抛出异常并拒绝生成 ID 59 | throw new Error('Clock moved backwards. Refusing to generate ID.'); 60 | } 61 | 62 | // 如果当前时间戳和上一次的时间戳相等,序列号加一 63 | if (timestamp === this.lastTimestamp) { 64 | // 同一毫秒内生成多个 ID,递增序列号,防止冲突 65 | this.sequence = (this.sequence + BigInt(1)) & this.sequenceMask; 66 | if (this.sequence === BigInt(0)) { 67 | // 序列号溢出,等待下一毫秒 68 | timestamp = this.waitNextMillis(this.lastTimestamp); 69 | } 70 | } else { 71 | // 不同毫秒,重置序列号 72 | this.sequence = BigInt(0); 73 | } 74 | 75 | this.lastTimestamp = timestamp; 76 | 77 | // 组合各部分生成最终的 ID,可以理解为把64位二进制转换位十进制数字 78 | const id = 79 | ((timestamp - this.epoch) << this.timestampShift) | 80 | (this.dataCenterId << this.dataCenterIdShift) | 81 | (this.workerId << this.workerIdShift) | 82 | this.sequence; 83 | 84 | return id.toString(); 85 | } 86 | 87 | waitNextMillis(lastTimestamp) { 88 | let timestamp = BigInt(Date.now()); 89 | while (timestamp <= lastTimestamp) { 90 | // 主动等待,直到当前时间超过上次记录的时间戳 91 | timestamp = BigInt(Date.now()); 92 | } 93 | return timestamp; 94 | } 95 | } 96 | 97 | export const snowFlake = new SnowFlake(0, +env.pm_id || 0); 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluxy-admin-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "dependencies": { 7 | "@midwayjs/bootstrap": "^3.11.15", 8 | "@midwayjs/bull": "^3.11.15", 9 | "@midwayjs/cache": "^3.11.15", 10 | "@midwayjs/captcha": "^3.11.15", 11 | "@midwayjs/casbin": "3.11.15", 12 | "@midwayjs/casbin-redis-adapter": "^3.11.15", 13 | "@midwayjs/casbin-typeorm-adapter": "^3.11.15", 14 | "@midwayjs/core": "^3.11.15", 15 | "@midwayjs/decorator": "^3.11.15", 16 | "@midwayjs/info": "^3.11.15", 17 | "@midwayjs/koa": "^3.11.15", 18 | "@midwayjs/logger": "^2.17.0", 19 | "@midwayjs/redis": "^3.11.15", 20 | "@midwayjs/swagger": "^3.11.16", 21 | "@midwayjs/typeorm": "^3.11.15", 22 | "@midwayjs/upload": "^3.11.15", 23 | "@midwayjs/validate": "^3.11.15", 24 | "@midwayjs/ws": "^3.11.15", 25 | "@sentry/node": "^7.68.0", 26 | "@sentry/profiling-node": "^1.2.1", 27 | "@sentry/utils": "^7.68.0", 28 | "@types/nodemailer": "^6.4.8", 29 | "bcryptjs": "^2.4.3", 30 | "cache-manager": "^5.2.3", 31 | "cache-manager-ioredis": "^2.1.0", 32 | "casbin": "^5.26.2", 33 | "crypto-js": "^4.1.1", 34 | "dotenv": "^16.3.1", 35 | "ip2region": "^2.3.0", 36 | "jsencrypt": "^3.3.2", 37 | "lodash": "^4.17.21", 38 | "mini-svg-data-uri": "^1.4.4", 39 | "minio": "^7.1.1", 40 | "mysql-import": "^5.0.26", 41 | "mysql2": "^3.5.1", 42 | "nanoid": "^3.3.6", 43 | "node-rsa": "^1.1.1", 44 | "nodemailer": "^6.9.3", 45 | "svg-captcha": "^1.4.0", 46 | "ts-node": "^10.9.1", 47 | "typeorm": "^0.3.17", 48 | "typescript": "~4.8.4", 49 | "useragent": "^2.3.0" 50 | }, 51 | "devDependencies": { 52 | "@midwayjs/cli": "^2.1.1", 53 | "@midwayjs/mock": "^3.11.15", 54 | "@types/bcryptjs": "^2.4.2", 55 | "@types/jest": "^29.5.3", 56 | "@types/koa": "^2.13.6", 57 | "@types/lodash": "^4.14.195", 58 | "@types/node": "^14.18.53", 59 | "@types/useragent": "^2.3.1", 60 | "@types/ws": "^8.5.5", 61 | "cross-env": "^6.0.3", 62 | "jest": "^29.6.1", 63 | "mwts": "^1.3.0", 64 | "swagger-ui-dist": "^4.19.1", 65 | "ts-jest": "^29.1.1" 66 | }, 67 | "engines": { 68 | "node": ">=12.0.0" 69 | }, 70 | "scripts": { 71 | "start": "NODE_ENV=production pm2-runtime start ./bootstrap.js --name midway_app -i 1", 72 | "dev": "cross-env NODE_ENV=local midway-bin dev --ts", 73 | "test": "midway-bin test --ts", 74 | "cov": "midway-bin cov --ts", 75 | "lint": "mwts check", 76 | "lint:fix": "mwts fix", 77 | "ci": "npm run cov", 78 | "build": "midway-bin build -c", 79 | "migration:generate:dev": "node ./node_modules/@midwayjs/typeorm/cli.js migration:generate -d ./src/config/config.default.ts ./src/migration/migration", 80 | "migration:generate": "node ./node_modules/@midwayjs/typeorm/cli.js migration:generate -d ./src/config/typeorm.prod.ts ./src/migration/migration", 81 | "migration:run": "node ./node_modules/@midwayjs/typeorm/cli.js migration:run -d ./src/config/typeorm.prod.ts" 82 | }, 83 | "midway-bin-clean": [ 84 | ".vscode/.tsbuildinfo", 85 | "dist" 86 | ], 87 | "repository": { 88 | "type": "git", 89 | "url": "" 90 | }, 91 | "author": "anonymous", 92 | "license": "MIT" 93 | } -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import * as bull from '@midwayjs/bull'; 2 | import * as cache from '@midwayjs/cache'; 3 | import { App, Config, Configuration, Inject } from '@midwayjs/core'; 4 | import * as info from '@midwayjs/info'; 5 | import * as koa from '@midwayjs/koa'; 6 | import * as redis from '@midwayjs/redis'; 7 | import * as swagger from '@midwayjs/swagger'; 8 | import * as orm from '@midwayjs/typeorm'; 9 | import * as upload from '@midwayjs/upload'; 10 | import * as validate from '@midwayjs/validate'; 11 | import { join } from 'path'; 12 | // import { DefaultErrorFilter } from './filter/default.filter'; 13 | import * as casbin from '@midwayjs/casbin'; 14 | import { CasbinEnforcerService } from '@midwayjs/casbin'; 15 | import { InjectDataSource } from '@midwayjs/typeorm'; 16 | import * as ws from '@midwayjs/ws'; 17 | import * as dotenv from 'dotenv'; 18 | import { DataSource } from 'typeorm'; 19 | import { CommonErrorFilter } from './filter/common-filter'; 20 | import { DefaultErrorFilter } from './filter/default-filter'; 21 | import { ForbiddenErrorFilter } from './filter/forbidden-filter'; 22 | import { NotFoundFilter } from './filter/notfound-filter'; 23 | import { UnauthorizedErrorFilter } from './filter/unauthorized-filter'; 24 | import { ValidateErrorFilter } from './filter/validate-filter'; 25 | import { AuthMiddleware } from './middleware/auth'; 26 | import { ReportMiddleware } from './middleware/report'; 27 | import { UserEntity } from './module/system/user/entity/user'; 28 | 29 | dotenv.config(); 30 | 31 | @Configuration({ 32 | imports: [ 33 | koa, 34 | validate, 35 | orm, 36 | redis, 37 | cache, 38 | upload, 39 | bull, 40 | ws, 41 | { 42 | component: swagger, 43 | enabledEnvironment: ['local'], 44 | }, 45 | { 46 | component: info, 47 | enabledEnvironment: ['local'], 48 | }, 49 | casbin, 50 | ], 51 | importConfigs: [join(__dirname, './config')], 52 | }) 53 | export class ContainerLifeCycle { 54 | @App() 55 | app: koa.Application; 56 | @Inject() 57 | casbinEnforcerService: CasbinEnforcerService; 58 | @Inject() 59 | bullFramework: bull.Framework; 60 | @InjectDataSource('default') 61 | defaultDataSource: DataSource; 62 | @Config('autoResetDataBase') 63 | autoResetDataBase: boolean; 64 | 65 | async onReady() { 66 | // add middleware 67 | this.app.useMiddleware([ReportMiddleware, AuthMiddleware]); 68 | // add filter 69 | this.app.useFilter([ 70 | ForbiddenErrorFilter, 71 | ValidateErrorFilter, 72 | CommonErrorFilter, 73 | NotFoundFilter, 74 | UnauthorizedErrorFilter, 75 | DefaultErrorFilter, 76 | ]); 77 | 78 | this.casbinEnforcerService.enableAutoSave(false); 79 | } 80 | 81 | async onServerReady() { 82 | // 检测数据库里有没有数据,如果没有数据,则初始化数据库 83 | if ( 84 | (await this.defaultDataSource.getRepository(UserEntity).count()) === 0 85 | ) { 86 | const initDatabaseQueue = this.bullFramework.getQueue('init-database'); 87 | // 立即执行这个任务 88 | await initDatabaseQueue?.runJob({}); 89 | } 90 | 91 | // 如果开启自动重置数据库,则设置一个定时任务,每天0点重置数据库 92 | if (this.autoResetDataBase) { 93 | const initDatabaseQueue = this.bullFramework.getQueue('init-database'); 94 | await initDatabaseQueue?.runJob({}, { repeat: { cron: '0 0 0 * * *' } }); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | push: 10 | branches: ['main'] 11 | # Publish semver tags as releases. 12 | tags: ['v*.*.*'] 13 | 14 | env: 15 | # Use docker.io for Docker Hub if empty 16 | REGISTRY: registry.cn-hangzhou.aliyuncs.com 17 | # github.repository as / 18 | IMAGE_NAME: ${{ github.repository }} 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | packages: write 26 | # This is used to complete the identity challenge 27 | # with sigstore/fulcio when running outside of PRs. 28 | id-token: write 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v3 33 | 34 | # Workaround: https://github.com/docker/build-push-action/issues/461 35 | - name: Setup Docker buildx 36 | uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf 37 | 38 | - name: Cache Docker layers 39 | uses: actions/cache@v2 40 | with: 41 | path: /tmp/.buildx-cache 42 | key: ${{ runner.os }}-buildx-${{ github.sha }} 43 | restore-keys: | 44 | ${{ runner.os }}-buildx- 45 | 46 | # Login against a Docker registry except on PR 47 | # https://github.com/docker/login-action 48 | - name: Log into registry ${{ env.REGISTRY }} 49 | if: github.event_name != 'pull_request' 50 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 51 | with: 52 | registry: ${{ env.REGISTRY }} 53 | username: ${{ secrets.USER_NAME }} 54 | password: ${{ secrets.DOCKER_TOKEN }} 55 | 56 | # Extract metadata (tags, labels) for Docker 57 | # https://github.com/docker/metadata-action 58 | - name: Extract Docker metadata 59 | id: meta 60 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 61 | with: 62 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 63 | 64 | # Build and push Docker image with Buildx (don't push on PR) 65 | # https://github.com/docker/build-push-action 66 | - name: Build and push Docker image 67 | id: build-and-push 68 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 69 | with: 70 | context: . 71 | push: ${{ github.event_name != 'pull_request' }} 72 | tags: ${{ steps.meta.outputs.tags }} 73 | labels: ${{ steps.meta.outputs.labels }} 74 | cache-from: type=local,src=/tmp/.buildx-cache 75 | cache-to: type=local,dest=/tmp/.buildx-cache-new 76 | 77 | - name: Move cache 78 | run: | 79 | rm -rf /tmp/.buildx-cache 80 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 81 | 82 | - name: SSH Command 83 | # You may pin to the exact commit or the version. 84 | # uses: D3rHase/ssh-command-action@981832f056c539720824429fa91df009db0ee9cd 85 | uses: D3rHase/ssh-command-action@v0.2.1 86 | with: 87 | # hostname / IP of the server 88 | HOST: ${{ secrets.SERVER_IP }} 89 | # ssh port of the server 90 | PORT: 22 # optional, default is 22 91 | # user of the server 92 | USER: root 93 | # private ssh key registered on the server 94 | PRIVATE_SSH_KEY: ${{ secrets.SERVER_KEY }} 95 | # command to be executed 96 | COMMAND: cd /project/docker-compose && sh ./run.sh 97 | -------------------------------------------------------------------------------- /src/migration/1688878440201-migration.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class Migration1688878440201 implements MigrationInterface { 4 | name = 'Migration1688878440201'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | "CREATE TABLE `sys_menu_interface` (`id` bigint NOT NULL COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `menuId` varchar(255) NOT NULL COMMENT '菜单id', `method` varchar(255) NOT NULL COMMENT '请求方式', `path` varchar(255) NOT NULL COMMENT 'path', PRIMARY KEY (`id`)) ENGINE=InnoDB" 9 | ); 10 | await queryRunner.query( 11 | "CREATE TABLE `sys_menu` (`id` bigint NOT NULL COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `parentId` varchar(255) NULL COMMENT '上级id', `name` varchar(255) NOT NULL COMMENT '名称', `icon` varchar(255) NULL COMMENT '图标', `type` int NOT NULL COMMENT '类型,1:目录 2:菜单', `route` varchar(255) NOT NULL COMMENT '路由', `filePath` varchar(255) NULL COMMENT '本地组件地址', `orderNumber` int NOT NULL COMMENT '排序号', `url` varchar(255) NULL COMMENT 'url', `show` tinyint NOT NULL COMMENT '是否在菜单中显示', PRIMARY KEY (`id`)) ENGINE=InnoDB" 12 | ); 13 | await queryRunner.query( 14 | "CREATE TABLE `sys_role_menu` (`id` bigint NOT NULL COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `roleId` varchar(255) NOT NULL COMMENT '角色id', `menuId` varchar(255) NOT NULL COMMENT '菜单id', PRIMARY KEY (`id`)) ENGINE=InnoDB" 15 | ); 16 | await queryRunner.query( 17 | "CREATE TABLE `sys_user_role` (`id` bigint NOT NULL COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `userId` varchar(255) NOT NULL COMMENT '用户id', `roleId` varchar(255) NOT NULL COMMENT '角色id', PRIMARY KEY (`id`)) ENGINE=InnoDB" 18 | ); 19 | await queryRunner.query( 20 | "CREATE TABLE `sys_role` (`id` bigint NOT NULL COMMENT '主键', `createDate` datetime(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), `updateDate` datetime(6) NOT NULL COMMENT '更新时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `name` varchar(255) NOT NULL COMMENT '名称', `code` varchar(255) NOT NULL COMMENT '代码', PRIMARY KEY (`id`)) ENGINE=InnoDB" 21 | ); 22 | await queryRunner.query( 23 | "ALTER TABLE `sys_user` ADD `avatar` varchar(255) NULL COMMENT '头像'" 24 | ); 25 | await queryRunner.query('ALTER TABLE `sys_user` DROP PRIMARY KEY'); 26 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `id`'); 27 | await queryRunner.query( 28 | "ALTER TABLE `sys_user` ADD `id` int NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键'" 29 | ); 30 | await queryRunner.query( 31 | "ALTER TABLE `sys_user` CHANGE `id` `id` int NOT NULL COMMENT '主键'" 32 | ); 33 | await queryRunner.query('ALTER TABLE `sys_user` DROP PRIMARY KEY'); 34 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `id`'); 35 | await queryRunner.query( 36 | "ALTER TABLE `sys_user` ADD `id` bigint NOT NULL PRIMARY KEY COMMENT '主键'" 37 | ); 38 | } 39 | 40 | public async down(queryRunner: QueryRunner): Promise { 41 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `id`'); 42 | await queryRunner.query( 43 | "ALTER TABLE `sys_user` ADD `id` int NOT NULL AUTO_INCREMENT COMMENT '主键'" 44 | ); 45 | await queryRunner.query('ALTER TABLE `sys_user` ADD PRIMARY KEY (`id`)'); 46 | await queryRunner.query( 47 | "ALTER TABLE `sys_user` CHANGE `id` `id` int NOT NULL AUTO_INCREMENT COMMENT '主键'" 48 | ); 49 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `id`'); 50 | await queryRunner.query( 51 | "ALTER TABLE `sys_user` ADD `id` bigint NOT NULL COMMENT '主键'" 52 | ); 53 | await queryRunner.query('ALTER TABLE `sys_user` ADD PRIMARY KEY (`id`)'); 54 | await queryRunner.query('ALTER TABLE `sys_user` DROP COLUMN `avatar`'); 55 | await queryRunner.query('DROP TABLE `sys_role`'); 56 | await queryRunner.query('DROP TABLE `sys_user_role`'); 57 | await queryRunner.query('DROP TABLE `sys_role_menu`'); 58 | await queryRunner.query('DROP TABLE `sys_menu`'); 59 | await queryRunner.query('DROP TABLE `sys_menu_interface`'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/module/system/api-log/controller/api-log.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ALL, 3 | Body, 4 | Controller, 5 | Del, 6 | Get, 7 | Inject, 8 | Param, 9 | Post, 10 | Put, 11 | Query, 12 | } from '@midwayjs/core'; 13 | import { ApiOkResponse, ApiResponse } from '@midwayjs/swagger'; 14 | import { Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; 15 | import { FilterQuery } from '../../../../utils/filter-query'; 16 | import { like } from '../../../../utils/typeorm-utils'; 17 | import { ApiLogDTO } from '../dto/api-log'; 18 | import { ApiLogPageDTO } from '../dto/api-log-page'; 19 | import { ApiLogEntity } from '../entity/api-log'; 20 | import { ApiLogService } from '../service/api-log'; 21 | import { ApiLogPageVO } from '../vo/api-log-page'; 22 | import { BodyParamsVO } from '../vo/body-params'; 23 | import { QueryParamsVO } from '../vo/query-params'; 24 | import { ResultParamsVO } from '../vo/result-params'; 25 | 26 | @Controller('/api-log', { description: '接口日志' }) 27 | export class ApiLogController { 28 | @Inject() 29 | apiLogService: ApiLogService; 30 | 31 | @Post('/', { description: '新建' }) 32 | async create(@Body(ALL) data: ApiLogDTO) { 33 | return await this.apiLogService.create(data.toEntity()); 34 | } 35 | 36 | @Put('/', { description: '编辑' }) 37 | async edit(@Body(ALL) data: ApiLogDTO) { 38 | const apiLog = await this.apiLogService.getById(data.id); 39 | // update 40 | return await this.apiLogService.edit(apiLog); 41 | } 42 | 43 | @Del('/:id', { description: '删除' }) 44 | async remove(@Param('id') id: string) { 45 | await this.apiLogService.removeById(id); 46 | } 47 | 48 | @Get('/:id', { description: '根据id查询' }) 49 | async getById(@Param('id') id: string) { 50 | return await this.apiLogService.getById(id); 51 | } 52 | 53 | @Get('/page', { description: '分页查询' }) 54 | @ApiResponse({ type: ApiLogPageVO }) 55 | async page(@Query() apiLogPageDTO: ApiLogPageDTO) { 56 | const filterQuery = new FilterQuery(); 57 | filterQuery.append('url', like(apiLogPageDTO.url), !!apiLogPageDTO.url); 58 | filterQuery.append('method', apiLogPageDTO.method, !!apiLogPageDTO.method); 59 | filterQuery.append( 60 | 'success', 61 | apiLogPageDTO.success, 62 | !!apiLogPageDTO.success 63 | ); 64 | 65 | filterQuery.append( 66 | 'startTime', 67 | Between(apiLogPageDTO.startTimeStart, apiLogPageDTO.startTimeEnd), 68 | !!apiLogPageDTO.startTimeStart && !!apiLogPageDTO.startTimeEnd 69 | ); 70 | 71 | filterQuery.append( 72 | 'endTime', 73 | Between(apiLogPageDTO.endTimeStart, apiLogPageDTO.endTimeEnd), 74 | !!apiLogPageDTO.endTimeStart && !!apiLogPageDTO.endTimeEnd 75 | ); 76 | 77 | filterQuery.append( 78 | 'duration', 79 | Between(apiLogPageDTO.durationStart, apiLogPageDTO.durationEnd), 80 | !!apiLogPageDTO.durationStart || !!apiLogPageDTO.durationEnd 81 | ); 82 | 83 | if (apiLogPageDTO.durationStart && !apiLogPageDTO.durationEnd) { 84 | filterQuery.append( 85 | 'duration', 86 | MoreThanOrEqual(apiLogPageDTO.durationStart), 87 | true 88 | ); 89 | } 90 | 91 | if (!apiLogPageDTO.durationStart && apiLogPageDTO.durationEnd) { 92 | filterQuery.append( 93 | 'duration', 94 | LessThanOrEqual(apiLogPageDTO.durationEnd), 95 | true 96 | ); 97 | } 98 | 99 | filterQuery.append('ip', like(apiLogPageDTO.ip), !!apiLogPageDTO.ip); 100 | 101 | filterQuery.append( 102 | 'errorType', 103 | like(apiLogPageDTO.errorType), 104 | !!apiLogPageDTO.errorType 105 | ); 106 | 107 | const { page, size } = apiLogPageDTO; 108 | 109 | return await this.apiLogService.page( 110 | { 111 | page, 112 | size, 113 | }, 114 | { where: filterQuery.where } 115 | ); 116 | } 117 | 118 | @Get('/list', { description: '查询全部' }) 119 | async list() { 120 | return await this.apiLogService.list(); 121 | } 122 | 123 | @Get('/query', { description: '查看请求query参数' }) 124 | @ApiOkResponse({ 125 | type: QueryParamsVO, 126 | }) 127 | async getQueryData(@Query('id') id: string) { 128 | return await this.apiLogService.getQueryData(id); 129 | } 130 | 131 | @Get('/body', { description: '查看请求body参数' }) 132 | @ApiOkResponse({ 133 | type: BodyParamsVO, 134 | }) 135 | async getBodyData(@Query('id') id: string) { 136 | return await this.apiLogService.getBodyData(id); 137 | } 138 | 139 | @Get('/result', { description: '查看响应结果' }) 140 | @ApiOkResponse({ 141 | type: ResultParamsVO, 142 | }) 143 | async getResultData(@Query('id') id: string) { 144 | return await this.apiLogService.getResultData(id); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /script/create-module.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | function firstCharToUpperCase(str) { 5 | return str[0].toUpperCase() + str.substring(1); 6 | } 7 | 8 | const [dir, moduleName, shortName, desc] = process.argv.slice(2, 6); 9 | 10 | if (!dir) { 11 | console.log('请输入文件夹名称'); 12 | process.exit(); 13 | } 14 | 15 | if (!moduleName) { 16 | console.log('请输入模块名称'); 17 | process.exit(); 18 | } 19 | 20 | if (!shortName) { 21 | console.log('请输入表名前缀'); 22 | process.exit(); 23 | } 24 | 25 | if (!desc) { 26 | console.log('请输入模块描述'); 27 | process.exit(); 28 | } 29 | 30 | if (!fs.existsSync(path.resolve(__dirname, `../src/module/${dir}`))) { 31 | fs.mkdirSync(path.resolve(__dirname, `../src/module/${dir}`)); 32 | } 33 | 34 | fs.mkdirSync(path.resolve(__dirname, `../src/module/${dir}/${moduleName}`)); 35 | fs.mkdirSync( 36 | path.resolve(__dirname, `../src/module/${dir}/${moduleName}/controller`) 37 | ); 38 | fs.mkdirSync( 39 | path.resolve(__dirname, `../src/module/${dir}/${moduleName}/service`) 40 | ); 41 | fs.mkdirSync( 42 | path.resolve(__dirname, `../src/module/${dir}/${moduleName}/entity`) 43 | ); 44 | fs.mkdirSync(path.resolve(__dirname, `../src/module/${dir}/${moduleName}/dto`)); 45 | fs.mkdirSync(path.resolve(__dirname, `../src/module/${dir}/${moduleName}/vo`)); 46 | 47 | let controllerContent = fs 48 | .readFileSync(path.resolve(__dirname, './template/controller.template')) 49 | .toString(); 50 | 51 | let serviceContent = fs 52 | .readFileSync(path.resolve(__dirname, './template/service.template')) 53 | .toString(); 54 | 55 | let entityContent = fs 56 | .readFileSync(path.resolve(__dirname, './template/entity.template')) 57 | .toString(); 58 | 59 | let dtoContent = fs 60 | .readFileSync(path.resolve(__dirname, './template/dto.template')) 61 | .toString(); 62 | 63 | let pageDtoContent = fs 64 | .readFileSync(path.resolve(__dirname, './template/page-dto.template')) 65 | .toString(); 66 | 67 | let voContent = fs 68 | .readFileSync(path.resolve(__dirname, './template/vo.template')) 69 | .toString(); 70 | 71 | let pageVoContent = fs 72 | .readFileSync(path.resolve(__dirname, './template/page-vo.template')) 73 | .toString(); 74 | 75 | let name; 76 | const filename = moduleName; 77 | let varName = moduleName; 78 | let tableName = moduleName; 79 | const route = moduleName; 80 | 81 | if (moduleName.includes('-')) { 82 | name = moduleName 83 | .split('-') 84 | .map(o => firstCharToUpperCase(o)) 85 | .join(''); 86 | 87 | varName = moduleName 88 | .split('-') 89 | .filter((_, index) => index > 0) 90 | .map(o => firstCharToUpperCase(o)) 91 | .join(''); 92 | varName = [moduleName.split('-')[0], varName].join(''); 93 | 94 | tableName = moduleName.replace(/\./g, '_'); 95 | } else { 96 | name = moduleName[0].toUpperCase() + moduleName.substring(1); 97 | } 98 | 99 | controllerContent = controllerContent 100 | .replace(/\$1/g, name) 101 | .replace(/\$2/g, filename) 102 | .replace(/\$3/g, varName) 103 | .replace(/\$4/g, route) 104 | .replace(/\$5/g, desc); 105 | 106 | serviceContent = serviceContent 107 | .replace(/\$1/g, name) 108 | .replace(/\$2/g, filename) 109 | .replace(/\$3/g, varName); 110 | 111 | entityContent = entityContent 112 | .replace(/\$1/g, name) 113 | .replace(/\$2/g, filename) 114 | .replace(/\$3/g, varName) 115 | .replace(/\$4/g, tableName) 116 | .replace(/\$5/g, shortName); 117 | 118 | dtoContent = dtoContent 119 | .replace(/\$1/g, name) 120 | .replace(/\$2/g, filename) 121 | .replace(/\$3/g, varName); 122 | 123 | pageDtoContent = pageDtoContent 124 | .replace(/\$1/g, name) 125 | .replace(/\$2/g, filename) 126 | .replace(/\$3/g, varName); 127 | 128 | voContent = voContent 129 | .replace(/\$1/g, name) 130 | .replace(/\$2/g, filename) 131 | .replace(/\$3/g, varName); 132 | 133 | pageVoContent = pageVoContent 134 | .replace(/\$1/g, name) 135 | .replace(/\$2/g, filename) 136 | .replace(/\$3/g, varName); 137 | 138 | fs.writeFileSync( 139 | path.resolve( 140 | __dirname, 141 | `../src/module/${dir}/${moduleName}/controller/${moduleName}.ts` 142 | ), 143 | controllerContent 144 | ); 145 | 146 | fs.writeFileSync( 147 | path.resolve( 148 | __dirname, 149 | `../src/module/${dir}/${moduleName}/service/${moduleName}.ts` 150 | ), 151 | serviceContent 152 | ); 153 | 154 | fs.writeFileSync( 155 | path.resolve( 156 | __dirname, 157 | `../src/module/${dir}/${moduleName}/entity/${moduleName}.ts` 158 | ), 159 | entityContent 160 | ); 161 | 162 | fs.writeFileSync( 163 | path.resolve( 164 | __dirname, 165 | `../src/module/${dir}/${moduleName}/dto/${moduleName}.ts` 166 | ), 167 | dtoContent 168 | ); 169 | 170 | fs.writeFileSync( 171 | path.resolve( 172 | __dirname, 173 | `../src/module/${dir}/${moduleName}/dto/${moduleName}-page.ts` 174 | ), 175 | pageDtoContent 176 | ); 177 | 178 | fs.writeFileSync( 179 | path.resolve( 180 | __dirname, 181 | `../src/module/${dir}/${moduleName}/vo/${moduleName}.ts` 182 | ), 183 | voContent 184 | ); 185 | 186 | fs.writeFileSync( 187 | path.resolve( 188 | __dirname, 189 | `../src/module/${dir}/${moduleName}/vo/${moduleName}-page.ts` 190 | ), 191 | pageVoContent 192 | ); 193 | -------------------------------------------------------------------------------- /src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { createWatcher } from '@midwayjs/casbin-redis-adapter'; 2 | import { MidwayAppInfo, RouterOption } from '@midwayjs/core'; 3 | import * as redisStore from 'cache-manager-ioredis'; 4 | import { join } from 'path'; 5 | import { env } from 'process'; 6 | import { MailConfig, MinioConfig, TokenConfig } from '../interface'; 7 | import { EverythingSubscriber } from '../typeorm-event-subscriber'; 8 | 9 | import { CasbinRule, createAdapter } from '@midwayjs/casbin-typeorm-adapter'; 10 | import { uploadWhiteList } from '@midwayjs/upload'; 11 | 12 | export default (appInfo: MidwayAppInfo) => { 13 | return { 14 | keys: '1684629293601_5943', 15 | // 邮件中的系统名称 16 | title: 'fluxy-admin', 17 | koa: { 18 | port: 7001, 19 | globalPrefix: '/api', 20 | }, 21 | typeorm: { 22 | dataSource: { 23 | default: { 24 | /** 25 | * 单数据库实例 26 | */ 27 | type: 'mysql', 28 | host: env.DB_HOST, 29 | port: env.DB_PORT ? Number(env.DB_PORT) : 3306, 30 | username: env.DB_USERNAME, 31 | password: env.DB_PASSWORD, 32 | database: env.DB_NAME, 33 | synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据 34 | logging: true, 35 | // 扫描entity文件夹 36 | entities: ['**/entity/*{.ts,.js}', CasbinRule], 37 | timezone: '+00:00', 38 | migrations: ['**/migration/*.ts', CasbinRule], 39 | cli: { 40 | migrationsDir: 'migration', 41 | }, 42 | subscribers: [EverythingSubscriber], 43 | maxQueryExecutionTime: 10, 44 | }, 45 | }, 46 | }, 47 | redis: { 48 | clients: { 49 | default: { 50 | port: env.REDIS_PORT, // Redis port 51 | host: env.REDIS_HOST, // Redis host 52 | password: env.REDIS_PASSWORD || '', 53 | db: 0, 54 | }, 55 | publish: { 56 | port: env.REDIS_PORT, // Redis port 57 | host: env.REDIS_HOST, // Redis host 58 | password: env.REDIS_PASSWORD || '', 59 | db: 1, 60 | }, 61 | subscribe: { 62 | port: env.REDIS_PORT, // Redis port 63 | host: env.REDIS_HOST, // Redis host 64 | password: env.REDIS_PASSWORD || '', 65 | db: 2, 66 | }, 67 | 'node-casbin-official': { 68 | port: env.REDIS_PORT, // Redis port 69 | host: env.REDIS_HOST, // Redis host 70 | password: env.REDIS_PASSWORD || '', 71 | db: 3, 72 | }, 73 | 'node-casbin-sub': { 74 | port: env.REDIS_PORT, // Redis port 75 | host: env.REDIS_HOST, // Redis host 76 | password: env.REDIS_PASSWORD || '', 77 | db: 3, 78 | }, 79 | }, 80 | }, 81 | validate: { 82 | validationOptions: { 83 | allowUnknown: true, 84 | }, 85 | }, 86 | token: { 87 | expire: 60 * 60 * 2, // 2小时 88 | refreshExpire: 60 * 60 * 24 * 7, // 7天 89 | } as TokenConfig, 90 | cache: { 91 | store: redisStore, 92 | options: { 93 | host: env.REDIS_HOST, // default value 94 | port: env.REDIS_PORT, // default value 95 | password: env.REDIS_PASSWORD || '', 96 | db: 0, 97 | keyPrefix: 'cache:', 98 | ttl: 100, 99 | }, 100 | }, 101 | captcha: { 102 | default: { 103 | size: 4, 104 | noise: 1, 105 | width: 120, 106 | height: 40, 107 | }, 108 | image: { 109 | type: 'mixed', 110 | }, 111 | formula: {}, 112 | text: {}, 113 | expirationTime: 3600, 114 | idPrefix: 'captcha', 115 | }, 116 | minio: { 117 | endPoint: env.MINIO_HOST, 118 | port: env.MINIO_PORT ? Number(env.MINIO_PORT) : 9002, 119 | useSSL: false, 120 | accessKey: env.MINIO_ACCESS_KEY, 121 | secretKey: env.MINIO_SECRET_KEY, 122 | bucketName: env.MINIO_BUCKET_NAME, 123 | } as MinioConfig, 124 | bull: { 125 | defaultQueueOptions: { 126 | redis: { 127 | port: env.REDIS_PORT, 128 | host: env.REDIS_HOST, 129 | password: env.REDIS_PASSWORD || '', 130 | db: 4, 131 | }, 132 | }, 133 | }, 134 | mail: { 135 | host: env.MAIL_HOST, 136 | port: env.MAIL_PORT ? Number(env.MAIL_PORT) : 465, 137 | secure: true, 138 | auth: { 139 | user: env.MAIL_USER, 140 | pass: env.MAIL_PASS, 141 | }, 142 | } as MailConfig, 143 | casbin: { 144 | modelPath: join(appInfo.baseDir, 'basic_model.conf'), 145 | policyAdapter: createAdapter({ 146 | dataSourceName: 'default', 147 | }), 148 | policyWatcher: createWatcher({ 149 | pubClientName: 'node-casbin-official', 150 | subClientName: 'node-casbin-sub', 151 | }), 152 | }, 153 | swagger: { 154 | documentOptions: { 155 | operationIdFactory(_: string, webRouter: RouterOption) { 156 | return `${webRouter.method}`; 157 | }, 158 | }, 159 | }, 160 | // 密码重置回调地址 161 | resetPasswordCallbackUrl: 'http://localhost:5173', 162 | // 发送给用户邮件中登录地址 163 | loginUrl: 'http://localhost:5173/user/login', 164 | // 上传文件后缀名白名单 165 | upload: { 166 | whitelist: [...uploadWhiteList, '.xlsx', '.xls'], 167 | }, 168 | // 创建用户的初始密码 169 | defaultPassword: '123456', 170 | autoResetDataBase: env.AUTO_RESET_DATABASE === 'true', 171 | }; 172 | }; 173 | -------------------------------------------------------------------------------- /src/module/system/auth/controller/auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Config, 4 | Controller, 5 | Get, 6 | Inject, 7 | Post, 8 | } from '@midwayjs/decorator'; 9 | import { RedisService } from '@midwayjs/redis'; 10 | import { ApiOkResponse, ApiResponse } from '@midwayjs/swagger'; 11 | 12 | import { Context } from 'koa'; 13 | import { R } from '../../../../common/base-error-util'; 14 | import { MailService } from '../../../../common/mail-service'; 15 | import { RSAService } from '../../../../common/rsa-service'; 16 | import { NotAuth } from '../../../../decorator/not-auth'; 17 | import { NotLogin } from '../../../../decorator/not-login'; 18 | import { AssertUtils } from '../../../../utils/assert'; 19 | import { getAddressByIp, getIp, getUserAgent } from '../../../../utils/utils'; 20 | import { uuid } from '../../../../utils/uuid'; 21 | import { LoginLogEntity } from '../../login-log/entity/login-log'; 22 | import { LoginLogService } from '../../login-log/service/login-log'; 23 | import { UserService } from '../../user/service/user'; 24 | import { UserVO } from '../../user/vo/user'; 25 | import { LoginDTO } from '../dto/login'; 26 | import { RefreshTokenDTO } from '../dto/refresh-token'; 27 | import { ResetPasswordDTO } from '../dto/reset-password'; 28 | import { AuthService } from '../service/auth'; 29 | import { CaptchaService } from '../service/captcha'; 30 | import { CaptchaVO } from '../vo/captcha'; 31 | import { CurrentUserVO } from '../vo/current-user'; 32 | import { TokenVO } from '../vo/token'; 33 | 34 | @Controller('/auth', { description: '权限管理' }) 35 | export class AuthController { 36 | @Inject() 37 | authService: AuthService; 38 | @Inject() 39 | captchaService: CaptchaService; 40 | @Inject() 41 | redisService: RedisService; 42 | @Inject() 43 | userService: UserService; 44 | @Inject() 45 | ctx: Context; 46 | @Inject() 47 | mailService: MailService; 48 | @Inject() 49 | rsaService: RSAService; 50 | @Inject() 51 | loginLogService: LoginLogService; 52 | @Config('resetPasswordCallbackUrl') 53 | resetPasswordCallbackUrl: string; 54 | @Config('title') 55 | title: string; 56 | 57 | @Post('/login', { description: '登录' }) 58 | @ApiResponse({ type: TokenVO }) 59 | @NotLogin() 60 | async login(@Body() loginDTO: LoginDTO) { 61 | const ip = getIp(this.ctx); 62 | const loginLog = new LoginLogEntity(); 63 | loginLog.ip = ip; 64 | loginLog.address = getAddressByIp(loginLog.ip); 65 | loginLog.browser = getUserAgent(this.ctx).family; 66 | loginLog.os = getUserAgent(this.ctx).os.toString(); 67 | loginLog.userName = loginDTO.accountNumber; 68 | 69 | try { 70 | const password = await this.rsaService.decrypt( 71 | loginDTO.publicKey, 72 | loginDTO.password 73 | ); 74 | 75 | AssertUtils.notEmpty(password, '登录出现异常,请重新登录'); 76 | loginDTO.password = password; 77 | 78 | const loginResult = await this.authService.login(loginDTO); 79 | 80 | loginLog.status = true; 81 | loginLog.message = '成功'; 82 | return loginResult; 83 | } catch (error) { 84 | loginLog.status = false; 85 | loginLog.message = error?.message || '登录失败'; 86 | 87 | throw R.error(error.message); 88 | } finally { 89 | this.loginLogService.create(loginLog); 90 | } 91 | } 92 | 93 | @Post('/refresh/token', { description: '刷新token' }) 94 | @ApiResponse({ type: TokenVO }) 95 | @NotLogin() 96 | async refreshToken(@Body() data: RefreshTokenDTO) { 97 | AssertUtils.notEmpty(data.refreshToken, '用户凭证已过期,请重新登录!'); 98 | 99 | return this.authService.refreshToken(data); 100 | } 101 | 102 | @Get('/captcha', { description: '获取验证码' }) 103 | @NotLogin() 104 | @ApiResponse({ type: CaptchaVO }) 105 | async getImageCaptcha() { 106 | const { id, imageBase64 } = await this.captchaService.formula({ 107 | height: 40, 108 | width: 120, 109 | noise: 1, 110 | color: true, 111 | }); 112 | return { 113 | id, 114 | imageBase64, 115 | }; 116 | } 117 | 118 | @Get('/publicKey') 119 | @NotLogin() 120 | async getPublicKey() { 121 | return await this.rsaService.getPublicKey(); 122 | } 123 | 124 | @Get('/current/user', { description: '获取当前用户信息' }) 125 | @NotAuth() 126 | @ApiOkResponse({ 127 | type: CurrentUserVO, 128 | }) 129 | async getCurrentUser(): Promise { 130 | return await this.authService.getUserById(this.ctx.userInfo.userId); 131 | } 132 | 133 | @Post('/logout') 134 | @NotAuth() 135 | async logout(): Promise { 136 | // 清除token和refreshToken 137 | await this.redisService 138 | .multi() 139 | .del(`token:${this.ctx.token}`) 140 | .del(`refreshToken:${this.ctx.userInfo.refreshToken}`) 141 | .exec(); 142 | 143 | return true; 144 | } 145 | 146 | @NotLogin() 147 | @NotAuth() 148 | @Post('/send/reset/password/email') 149 | async sendResetPasswordEmail(@Body() emailInfo: { checkEmail: string }) { 150 | if (!emailInfo.checkEmail) { 151 | throw R.error('邮箱不能为空'); 152 | } 153 | 154 | if (!(await this.userService.getUserByEmail(emailInfo.checkEmail))) { 155 | throw R.error('系统中不存在当前邮箱'); 156 | } 157 | 158 | const emailCaptcha = uuid(); 159 | 160 | await this.redisService.set( 161 | `resetPasswordEmailCaptcha:${emailInfo.checkEmail}`, 162 | emailCaptcha, 163 | 'EX', 164 | 60 * 30 165 | ); 166 | 167 | const resetPasswordUrl = `${this.resetPasswordCallbackUrl}/user/reset-password?email=${emailInfo.checkEmail}&emailCaptcha=${emailCaptcha}`; 168 | 169 | this.mailService.sendMail({ 170 | to: emailInfo.checkEmail, 171 | html: `
172 |

173 | ${emailInfo.checkEmail}, 你好! 174 |

175 |

请先确认本邮件是否是你需要的。

176 |

请点击下面的地址,根据提示进行密码重置:

177 | 点击跳转到密码重置页面 184 |

如果单击上面按钮没有反应,请复制下面链接到浏览器窗口中,或直接输入链接。

185 |

186 | ${resetPasswordUrl} 187 |

188 |

如您未提交该申请,请不要理会此邮件,对此为您带来的不便深表歉意。

189 |

本次链接30分钟后失效。

190 |
191 | ${this.title} 192 |
193 |
`, 194 | subject: `${this.title}平台密码重置提醒`, 195 | }); 196 | } 197 | 198 | @NotLogin() 199 | @Post('/reset/password') 200 | async resetPassword(@Body() resetPasswordDTO: ResetPasswordDTO) { 201 | await this.authService.resetPassword(resetPasswordDTO); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/module/system/auth/service/auth.ts: -------------------------------------------------------------------------------- 1 | import { Config, Inject, Provide } from '@midwayjs/decorator'; 2 | import { InjectDataSource, InjectEntityModel } from '@midwayjs/typeorm'; 3 | import * as bcrypt from 'bcryptjs'; 4 | import { DataSource, In, Repository } from 'typeorm'; 5 | 6 | import { RedisService } from '@midwayjs/redis'; 7 | import { Context } from 'koa'; 8 | import { R } from '../../../../common/base-error-util'; 9 | import { RSAService } from '../../../../common/rsa-service'; 10 | import { TokenConfig } from '../../../../interface'; 11 | import { AssertUtils } from '../../../../utils/assert'; 12 | import { uuid } from '../../../../utils/uuid'; 13 | import { FileEntity } from '../../file/entity/file'; 14 | import { LoginLogEntity } from '../../login-log/entity/login-log'; 15 | import { MenuEntity } from '../../menu/entity/menu'; 16 | import { RoleEntity } from '../../role/entity/role'; 17 | import { RoleMenuEntity } from '../../role/entity/role-menu'; 18 | import { SocketService } from '../../socket/service/socket'; 19 | import { SocketMessageType } from '../../socket/type'; 20 | import { UserEntity } from '../../user/entity/user'; 21 | import { UserRoleEntity } from '../../user/entity/user-role'; 22 | import { LoginDTO } from '../dto/login'; 23 | import { RefreshTokenDTO } from '../dto/refresh-token'; 24 | import { ResetPasswordDTO } from '../dto/reset-password'; 25 | import { CurrentUserVO } from '../vo/current-user'; 26 | import { TokenVO } from '../vo/token'; 27 | import { CaptchaService } from './captcha'; 28 | 29 | @Provide() 30 | export class AuthService { 31 | @InjectEntityModel(UserEntity) 32 | userModel: Repository; 33 | @Config('token') 34 | tokenConfig: TokenConfig; 35 | @Inject() 36 | redis: RedisService; 37 | @Inject() 38 | ctx: Context; 39 | @Inject() 40 | rsaService: RSAService; 41 | @InjectDataSource() 42 | defaultDataSource: DataSource; 43 | @Inject() 44 | captchaService: CaptchaService; 45 | @InjectEntityModel(UserRoleEntity) 46 | userRoleModel: Repository; 47 | @InjectEntityModel(RoleMenuEntity) 48 | roleMenuModel: Repository; 49 | @InjectEntityModel(LoginLogEntity) 50 | loginLogModel: Repository; 51 | @InjectEntityModel(MenuEntity) 52 | menuModel: Repository; 53 | @Inject() 54 | socketService: SocketService; 55 | 56 | /** 57 | * 登录 58 | * @param loginDTO 59 | */ 60 | async login(loginDTO: LoginDTO): Promise { 61 | const { accountNumber, captcha, captchaId } = loginDTO; 62 | 63 | const result = await this.captchaService.check(captchaId, captcha); 64 | 65 | AssertUtils.isTrue(result, '验证码错误'); 66 | 67 | const user = await this.userModel 68 | .createQueryBuilder('user') 69 | .where('user.phoneNumber = :accountNumber', { 70 | accountNumber, 71 | }) 72 | .orWhere('user.username = :accountNumber', { accountNumber }) 73 | .orWhere('user.email = :accountNumber', { accountNumber }) 74 | .select(['user.password', 'user.id', 'user.userName']) 75 | .getOne(); 76 | 77 | AssertUtils.notEmpty(user, '账号或密码错误!'); 78 | 79 | AssertUtils.isTrue( 80 | bcrypt.compareSync(loginDTO.password, user.password), 81 | '用户名或密码错误!' 82 | ); 83 | 84 | const { expire, refreshExpire } = this.tokenConfig; 85 | 86 | const token = uuid(); 87 | const refreshToken = uuid(); 88 | 89 | // multi可以实现redis指令并发执行 90 | await this.redis 91 | .multi() 92 | .set(`token:${token}`, JSON.stringify({ userId: user.id, refreshToken })) 93 | .expire(`token:${token}`, expire) 94 | .set(`refreshToken:${refreshToken}`, user.id) 95 | .expire(`refreshToken:${refreshToken}`, refreshExpire) 96 | .sadd(`userToken_${user.id}`, token) 97 | .sadd(`userRefreshToken_${user.id}`, refreshToken) 98 | .exec(); 99 | 100 | return { 101 | expire, 102 | token, 103 | refreshExpire, 104 | refreshToken, 105 | } as TokenVO; 106 | } 107 | 108 | /** 109 | * 刷新token 110 | * @param refreshToken 111 | */ 112 | async refreshToken(refreshToken: RefreshTokenDTO): Promise { 113 | const userId = await this.redis.get( 114 | `refreshToken:${refreshToken.refreshToken}` 115 | ); 116 | 117 | AssertUtils.notEmpty(userId, '用户凭证已过期,请重新登录!'); 118 | 119 | const { expire } = this.tokenConfig; 120 | 121 | const token = uuid(); 122 | 123 | await this.redis 124 | .multi() 125 | .set(`token:${token}`, JSON.stringify({ userId, refreshToken })) 126 | .expire(`token:${token}`, expire) 127 | .exec(); 128 | 129 | const refreshExpire = await this.redis.ttl( 130 | `refreshToken:${refreshToken.refreshToken}` 131 | ); 132 | 133 | return { 134 | expire, 135 | token, 136 | refreshExpire, 137 | refreshToken: refreshToken.refreshToken, 138 | } as TokenVO; 139 | } 140 | 141 | /** 142 | * 获取用户信息 143 | * @param userId 144 | * @returns 145 | */ 146 | async getUserById(userId: string): Promise { 147 | const entity = await this.userModel 148 | .createQueryBuilder('t') 149 | .leftJoinAndSelect(UserRoleEntity, 'user_role', 't.id = user_role.userId') 150 | .leftJoinAndMapOne( 151 | 't.avatar', 152 | FileEntity, 153 | 'file', 154 | 'file.pkValue = t.id and file.pkName = "user_avatar"' 155 | ) 156 | .leftJoinAndMapMany( 157 | 't.roles', 158 | RoleEntity, 159 | 'role', 160 | 'role.id = user_role.roleId' 161 | ) 162 | .where('t.id = :id', { id: userId }) 163 | .getOne(); 164 | 165 | AssertUtils.notEmpty(entity, '当前用户不存在!'); 166 | 167 | // 先把用户分配的角色查出来 168 | const userRoles = await this.userRoleModel.findBy({ userId: userId }); 169 | // 根据已分配角色查询已分配的菜单id数组 170 | const roleMenus = await this.roleMenuModel.find({ 171 | where: { roleId: In(userRoles.map(userRole => userRole.roleId)) }, 172 | }); 173 | // 根据菜单id数组查询菜单信息,这里加了个特殊判断,如果是管理员直接返回全部菜单,正常这个应该走数据迁移,数据迁移还没做,就先用这种方案。 174 | const query = { id: In(roleMenus.map(roleMenu => roleMenu.menuId)) }; 175 | const menus = await this.menuModel.find({ 176 | where: userId === '1' ? {} : query, 177 | order: { orderNumber: 'ASC', createDate: 'DESC' }, 178 | }); 179 | 180 | return { 181 | ...entity.toVO(), 182 | menus, 183 | }; 184 | } 185 | 186 | /** 187 | * 重置密码 188 | * @param resetPasswordDTO 189 | */ 190 | async resetPassword(resetPasswordDTO: ResetPasswordDTO) { 191 | const captcha = await this.redis.get( 192 | `resetPasswordEmailCaptcha:${resetPasswordDTO.email}` 193 | ); 194 | 195 | if (captcha !== resetPasswordDTO.emailCaptcha) { 196 | throw R.error('邮箱验证码错误或已失效'); 197 | } 198 | 199 | const user = await this.userModel.findOneBy({ 200 | email: resetPasswordDTO.email, 201 | }); 202 | 203 | if (!user) { 204 | throw R.error('邮箱不存在'); 205 | } 206 | 207 | const password = await this.rsaService.decrypt( 208 | resetPasswordDTO.publicKey, 209 | resetPasswordDTO.password 210 | ); 211 | 212 | // 获取当前用户颁发的token和refreshToken,然后再下面给移除掉。 213 | const tokens = await this.redis.smembers(`userToken_${user.id}`); 214 | const refreshTokens = await this.redis.smembers( 215 | `userRefreshToken_${user.id}` 216 | ); 217 | 218 | await this.defaultDataSource.transaction(async manager => { 219 | const hashPassword = bcrypt.hashSync(password, 10); 220 | user.password = hashPassword; 221 | await manager.save(UserEntity, user); 222 | 223 | await Promise.all([ 224 | ...tokens.map(token => this.redis.del(`token:${token}`)), 225 | ...refreshTokens.map(refreshToken => 226 | this.redis.del(`refreshToken:${refreshToken}`) 227 | ), 228 | this.redis.del(`resetPasswordEmailCaptcha:${resetPasswordDTO.email}`), 229 | ]); 230 | 231 | this.socketService.sendMessage(user.id, { 232 | type: SocketMessageType.PasswordChange, 233 | }); 234 | }); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/module/system/menu/service/menu.ts: -------------------------------------------------------------------------------- 1 | import { CasbinEnforcerService } from '@midwayjs/casbin'; 2 | import { NodeRedisWatcher } from '@midwayjs/casbin-redis-adapter'; 3 | import { CasbinRule } from '@midwayjs/casbin-typeorm-adapter'; 4 | import { Inject, Provide } from '@midwayjs/decorator'; 5 | import { InjectEntityModel } from '@midwayjs/typeorm'; 6 | import { IsNull, Repository } from 'typeorm'; 7 | import { MinioClient } from '../../../../autoload/minio'; 8 | import { BaseService } from '../../../../common/base-service'; 9 | import { PageDTO } from '../../../../common/page-dto'; 10 | import { AssertUtils } from '../../../../utils/assert'; 11 | import { RoleMenuEntity } from '../../role/entity/role-menu'; 12 | import { MenuDTO } from '../dto/menu'; 13 | import { MenuVersionDTO } from '../dto/menu-version'; 14 | import { UpdateMenuVersionDTO } from '../dto/update-menu-version'; 15 | import { MenuEntity } from '../entity/menu'; 16 | import { MenuApiEntity } from '../entity/menu-api'; 17 | import { MenuVersionEntity } from '../entity/menu-version'; 18 | import { MenuVO } from '../vo/menu'; 19 | import { MenuPageVO } from '../vo/menu-page'; 20 | 21 | enum MenuType { 22 | DIRECTORY = 1, 23 | MENU, 24 | BUTTON, 25 | LowCodePage, 26 | } 27 | 28 | @Provide() 29 | export class MenuService extends BaseService { 30 | @InjectEntityModel(MenuEntity) 31 | menuModel: Repository; 32 | @InjectEntityModel(MenuApiEntity) 33 | menuApiModel: Repository; 34 | @InjectEntityModel(RoleMenuEntity) 35 | roleMenuModel: Repository; 36 | @InjectEntityModel(MenuVersionEntity) 37 | menuVersionModel: Repository; 38 | @InjectEntityModel(CasbinRule) 39 | casbinModel: Repository; 40 | @Inject() 41 | casbinEnforcerService: CasbinEnforcerService; 42 | @Inject() 43 | casbinWatcher: NodeRedisWatcher; 44 | @Inject() 45 | minioClient: MinioClient; 46 | 47 | getModel(): Repository { 48 | return this.menuModel; 49 | } 50 | 51 | /** 52 | * 分页查询菜单 53 | */ 54 | async getMenusByPage(pageInfo: PageDTO): Promise { 55 | const { data, total } = await this.page(pageInfo, { 56 | where: { parentId: IsNull() }, 57 | order: { orderNumber: 'ASC' }, 58 | }); 59 | 60 | if (!data.length) return { data: [], total: 0 }; 61 | 62 | const ids = data.map(o => o.id); 63 | const countMap = await this.menuModel 64 | .createQueryBuilder('menu') 65 | .select('COUNT(menu.parentId)', 'count') 66 | .addSelect('menu.parentId', 'id') 67 | .where('menu.parentId IN (:...ids)', { ids }) 68 | .groupBy('menu.parentId') 69 | .getRawMany(); 70 | 71 | const result = data.map(item => { 72 | const count = 73 | countMap.find((o: { id: string; count: number }) => o.id === item.id) 74 | ?.count || 0; 75 | 76 | return { 77 | ...item, 78 | hasChild: Number(count) > 0, 79 | }; 80 | }); 81 | 82 | return { data: result, total }; 83 | } 84 | 85 | /** 86 | * 获取菜单子节点 87 | */ 88 | async getChildren(parentId?: string): Promise { 89 | AssertUtils.notEmpty(parentId, '父节点id不能为空'); 90 | 91 | const data = await this.menuModel.find({ 92 | where: { parentId }, 93 | order: { orderNumber: 'ASC' }, 94 | }); 95 | 96 | if (!data.length) return []; 97 | 98 | const ids = data.map(o => o.id); 99 | const countMap = await this.menuModel 100 | .createQueryBuilder('menu') 101 | .select('COUNT(menu.parentId)', 'count') 102 | .addSelect('menu.parentId', 'id') 103 | .where('menu.parentId IN (:...ids)', { ids }) 104 | .groupBy('menu.parentId') 105 | .getRawMany(); 106 | 107 | const result = data.map(item => { 108 | const count = countMap.find(o => o.id === item.id)?.count || 0; 109 | return { 110 | ...item, 111 | hasChild: Number(count) > 0, 112 | }; 113 | }); 114 | 115 | return result; 116 | } 117 | 118 | /** 119 | * 创建菜单 120 | */ 121 | async createMenu(data: MenuDTO) { 122 | AssertUtils.isTrue( 123 | !data.route || 124 | (await this.menuModel.countBy({ route: data.route })) === 0, 125 | '路由不能重复' 126 | ); 127 | 128 | AssertUtils.isTrue( 129 | !data.authCode || 130 | (await this.menuModel.countBy({ authCode: data.authCode })) === 0, 131 | '权限代码不能重复' 132 | ); 133 | 134 | const entity = data.toEntity(); 135 | const version = new MenuVersionEntity(); 136 | 137 | await this.defaultDataSource.transaction(async manager => { 138 | // 如果菜单类型为低代码页面,默认版本为v1.0.0 139 | if (entity.type === MenuType.LowCodePage) { 140 | entity.curVersion = 'v1.0.0'; 141 | } 142 | 143 | await manager.save(MenuEntity, entity); 144 | 145 | // 把低代码页面配置信息保存成json文件上传到minio文件服务器 146 | if (entity.type === MenuType.LowCodePage && data.pageSetting) { 147 | // 初始化版本 148 | version.menuId = entity.id; 149 | version.version = entity.curVersion; 150 | version.description = '初始化'; 151 | 152 | await manager.save(MenuVersionEntity, version); 153 | 154 | await this.minioClient.putObject( 155 | 'low-code', 156 | `${entity.id}/${entity.curVersion}.json`, 157 | Buffer.from(data.pageSetting, 'utf-8') 158 | ); 159 | } 160 | 161 | const menuApis = (data.apis || []).map(api => { 162 | const menuApi = new MenuApiEntity(); 163 | menuApi.menuId = entity.id; 164 | menuApi.path = api.path; 165 | menuApi.method = api.method; 166 | return menuApi; 167 | }); 168 | 169 | await manager.save(MenuApiEntity, menuApis); 170 | }); 171 | } 172 | 173 | /** 174 | * 更新菜单 175 | */ 176 | async updateMenu(data: MenuDTO) { 177 | const entity = data.toEntity(); 178 | 179 | await this.defaultDataSource.transaction(async manager => { 180 | await manager 181 | .createQueryBuilder() 182 | .delete() 183 | .from(MenuApiEntity) 184 | .where({ menuId: entity.id }) 185 | .execute(); 186 | 187 | await manager.save(MenuEntity, entity); 188 | 189 | const roleMenus = await this.roleMenuModel.findBy({ menuId: entity.id }); 190 | 191 | await manager 192 | .createQueryBuilder() 193 | .delete() 194 | .from(CasbinRule) 195 | .where({ v3: entity.id }) 196 | .execute(); 197 | 198 | const menuApis = data.apis.map(api => { 199 | const menuApi = new MenuApiEntity(); 200 | menuApi.menuId = entity.id; 201 | menuApi.path = api.path; 202 | menuApi.method = api.method; 203 | return menuApi; 204 | }); 205 | 206 | await manager 207 | .createQueryBuilder() 208 | .insert() 209 | .into(MenuApiEntity) 210 | .values(menuApis) 211 | .execute(); 212 | 213 | const casbinRules = roleMenus.reduce((prev, cur) => { 214 | prev.push( 215 | ...data.apis.map(api => { 216 | const casbinRule = new CasbinRule(); 217 | casbinRule.ptype = 'p'; 218 | casbinRule.v0 = cur.roleId; 219 | casbinRule.v1 = api.path; 220 | casbinRule.v2 = api.method; 221 | casbinRule.v3 = cur.menuId; 222 | return casbinRule; 223 | }) 224 | ); 225 | return prev; 226 | }, []); 227 | 228 | await manager.save(CasbinRule, casbinRules); 229 | }); 230 | 231 | await this.casbinWatcher.publishData(); 232 | } 233 | 234 | /** 235 | * 删除菜单 236 | */ 237 | async removeMenu(id: string) { 238 | await this.menuModel 239 | .createQueryBuilder() 240 | .delete() 241 | .where('id = :id', { id }) 242 | .orWhere('parentId = :id', { id }) 243 | .execute(); 244 | } 245 | 246 | /** 247 | * 获取菜单已分配的接口 248 | */ 249 | async getAllocAPIByMenu(menuId: string): Promise { 250 | const menuAPIs = await this.menuApiModel.findBy({ 251 | menuId, 252 | }); 253 | return menuAPIs; 254 | } 255 | 256 | // 分页查询低代码页面和版本 257 | async queryLowCodeMenus(page = 0, size = 10) { 258 | const [data, total] = await this.menuModel 259 | .createQueryBuilder('menu') 260 | .leftJoinAndMapMany( 261 | 'menu.versions', 262 | MenuVersionEntity, 263 | 'version', 264 | 'menu.id = version.menuId' 265 | ) 266 | .where('menu.type = :type', { type: MenuType.LowCodePage }) 267 | .orderBy('menu.createDate', 'DESC') 268 | .skip(page * size) 269 | .take(size) 270 | .getManyAndCount(); 271 | 272 | return { 273 | data, 274 | total, 275 | }; 276 | } 277 | 278 | async queryVersionById(versionId: string) { 279 | const version = await this.menuVersionModel 280 | .createQueryBuilder('t') 281 | .leftJoinAndMapOne('t.menu', MenuEntity, 'm', 't.menuId = m.id') 282 | .where('t.id = :versionId', { versionId }) 283 | .getOne(); 284 | return version; 285 | } 286 | 287 | // 更新版本配置 288 | async updateVersion(data: UpdateMenuVersionDTO) { 289 | const version = await this.menuVersionModel.findOneBy({ id: data.id }); 290 | 291 | // 更新时间 292 | version.updateDate = new Date(); 293 | 294 | await this.menuVersionModel.save(version); 295 | 296 | await this.minioClient.putObject( 297 | 'low-code', 298 | `${version.menuId}/${version.version}.json`, 299 | Buffer.from(data.pageSetting, 'utf-8') 300 | ); 301 | } 302 | 303 | async getLatestVersionByMenuId(menuId: string) { 304 | const version = await this.menuVersionModel 305 | .createQueryBuilder('t') 306 | .where('t.menuId = :menuId', { menuId }) 307 | .orderBy('t.id', 'DESC') 308 | .getOne(); 309 | return version; 310 | } 311 | 312 | // 创建新版本 313 | async createNewVersion(data: MenuVersionDTO) { 314 | const entity = data.toEntity(); 315 | await this.defaultDataSource.transaction(async manager => { 316 | await manager.save(MenuVersionEntity, entity); 317 | 318 | await this.minioClient.putObject( 319 | 'low-code', 320 | `${entity.menuId}/${entity.version}.json`, 321 | Buffer.from(data.pageSetting, 'utf-8') 322 | ); 323 | }); 324 | 325 | return entity; 326 | } 327 | 328 | // 发布版本 329 | async publishLowCodePage(menuId: string, version: string) { 330 | const menu = await this.menuModel.findOneBy({ id: menuId }); 331 | menu.curVersion = version; 332 | await this.menuModel.save(menu); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/module/system/role/service/role.ts: -------------------------------------------------------------------------------- 1 | import { CasbinEnforcerService } from '@midwayjs/casbin'; 2 | import { NodeRedisWatcher } from '@midwayjs/casbin-redis-adapter'; 3 | import { CasbinRule } from '@midwayjs/casbin-typeorm-adapter'; 4 | import { Inject, Provide } from '@midwayjs/decorator'; 5 | import { InjectDataSource, InjectEntityModel } from '@midwayjs/typeorm'; 6 | import { DataSource, In, Repository } from 'typeorm'; 7 | import { BaseService } from '../../../../common/base-service'; 8 | import { AssertUtils } from '../../../../utils/assert'; 9 | import { MenuApiEntity } from '../../menu/entity/menu-api'; 10 | import { SocketService } from '../../socket/service/socket'; 11 | import { SocketMessageType } from '../../socket/type'; 12 | import { UserRoleEntity } from '../../user/entity/user-role'; 13 | import { RoleDTO } from '../dto/role'; 14 | import { RoleEntity } from '../entity/role'; 15 | import { RoleMenuEntity } from '../entity/role-menu'; 16 | 17 | @Provide() 18 | export class RoleService extends BaseService { 19 | @InjectEntityModel(RoleEntity) 20 | roleModel: Repository; 21 | @InjectEntityModel(RoleMenuEntity) 22 | roleMenuModel: Repository; 23 | @InjectEntityModel(MenuApiEntity) 24 | menuApiModel: Repository; 25 | @InjectEntityModel(UserRoleEntity) 26 | userRoleModel: Repository; 27 | @InjectDataSource() 28 | defaultDataSource: DataSource; 29 | @Inject() 30 | socketService: SocketService; 31 | @Inject() 32 | casbinEnforcerService: CasbinEnforcerService; 33 | @Inject() 34 | casbinWatcher: NodeRedisWatcher; 35 | 36 | getModel(): Repository { 37 | return this.roleModel; 38 | } 39 | 40 | /** 41 | * 创建角色 42 | * @param data 43 | */ 44 | async createRole(data: RoleDTO): Promise { 45 | AssertUtils.isTrue( 46 | (await this.roleModel.countBy({ code: data.code })) === 0, 47 | '角色代码不能重复' 48 | ); 49 | 50 | const result = await this.defaultDataSource.transaction(async manager => { 51 | const entity = data.toEntity(); 52 | await manager.save(RoleEntity, entity); 53 | 54 | data.menuIds = data.menuIds || []; 55 | const roleMenus = data.menuIds.map(menuId => { 56 | const roleMenu = new RoleMenuEntity(); 57 | roleMenu.menuId = menuId; 58 | roleMenu.roleId = entity.id; 59 | return roleMenu; 60 | }); 61 | 62 | if (roleMenus.length) { 63 | // 批量插入 64 | await manager 65 | .createQueryBuilder() 66 | .insert() 67 | .into(RoleMenuEntity) 68 | .values(roleMenus) 69 | .execute(); 70 | } 71 | 72 | const apis = await this.menuApiModel.findBy({ menuId: In(data.menuIds) }); 73 | 74 | const casbinRules = apis.map(api => { 75 | const casbinRule = new CasbinRule(); 76 | casbinRule.ptype = 'p'; 77 | casbinRule.v0 = entity.id; 78 | casbinRule.v1 = api.path; 79 | casbinRule.v2 = api.method; 80 | casbinRule.v3 = api.menuId; 81 | return casbinRule; 82 | }); 83 | 84 | await manager 85 | .createQueryBuilder() 86 | .insert() 87 | .into(CasbinRule) 88 | .values(casbinRules) 89 | .execute(); 90 | 91 | return entity; 92 | }); 93 | this.casbinWatcher.publishData(); 94 | return result; 95 | } 96 | 97 | /** 98 | * 更新角色 99 | * @param data 100 | * @returns 101 | */ 102 | async editRole(data: RoleDTO): Promise { 103 | const result = await this.defaultDataSource.transaction(async manager => { 104 | const entity = data.toEntity(); 105 | await manager.save(RoleEntity, entity); 106 | if (Array.isArray(data.menuIds)) { 107 | const roleMenus = await this.roleMenuModel.findBy({ roleId: data.id }); 108 | 109 | await manager.delete(RoleMenuEntity, roleMenus); 110 | 111 | if (data.menuIds.length) { 112 | // 批量插入 113 | await manager 114 | .createQueryBuilder() 115 | .insert() 116 | .into(RoleMenuEntity) 117 | .values( 118 | data.menuIds.map(menuId => { 119 | const roleMenu = new RoleMenuEntity(); 120 | roleMenu.menuId = menuId; 121 | roleMenu.roleId = entity.id; 122 | return roleMenu; 123 | }) 124 | ) 125 | .execute(); 126 | 127 | const oldMenuIds = roleMenus.map(menu => menu.menuId); 128 | if (oldMenuIds.length !== data.menuIds.length) { 129 | // 如果有变化,查询所有分配了该角色的用户,给对应所有用户发通知 130 | const userIds = ( 131 | await this.userRoleModel.findBy({ roleId: data.id }) 132 | ).map(userRole => userRole.userId); 133 | 134 | userIds.forEach(userId => { 135 | this.socketService.sendMessage(userId, { 136 | type: SocketMessageType.PermissionChange, 137 | }); 138 | }); 139 | } 140 | 141 | // 因为数组都是数字,所以先排序,排序之后把数组转换为字符串比较,写法比较简单 142 | const sortOldMenuIds = oldMenuIds.sort(); 143 | const sortMenusIds = data.menuIds.sort(); 144 | 145 | if (sortOldMenuIds.join() !== sortMenusIds.join()) { 146 | // 如果有变化,查询所有分配了该角色的用户,给对应所有用户发通知 147 | const userIds = ( 148 | await this.userRoleModel.findBy({ roleId: data.id }) 149 | ).map(userRole => userRole.userId); 150 | 151 | userIds.forEach(userId => { 152 | this.socketService.sendMessage(userId, { 153 | type: SocketMessageType.PermissionChange, 154 | }); 155 | }); 156 | } 157 | 158 | await this.casbinEnforcerService.deletePermissionsForUser(data.id); 159 | 160 | await manager 161 | .createQueryBuilder() 162 | .delete() 163 | .from(CasbinRule) 164 | .where({ ptype: 'p', v0: data.id }) 165 | .execute(); 166 | 167 | const apis = await this.menuApiModel.findBy({ 168 | menuId: In(data.menuIds), 169 | }); 170 | 171 | const casbinRules = apis.map(api => { 172 | const casbinRule = new CasbinRule(); 173 | casbinRule.ptype = 'p'; 174 | casbinRule.v0 = data.id; 175 | casbinRule.v1 = api.path; 176 | casbinRule.v2 = api.method; 177 | casbinRule.v3 = api.menuId; 178 | 179 | return casbinRule; 180 | }); 181 | await manager 182 | .createQueryBuilder() 183 | .insert() 184 | .into(CasbinRule) 185 | .values(casbinRules) 186 | .execute(); 187 | } 188 | } 189 | return entity; 190 | }); 191 | this.casbinWatcher.publishData(); 192 | 193 | return result; 194 | } 195 | 196 | /** 197 | * 删除角色 198 | * @param id 199 | */ 200 | async removeRole(id: string) { 201 | await this.defaultDataSource.transaction(async manager => { 202 | await manager 203 | .createQueryBuilder() 204 | .delete() 205 | .from(RoleEntity) 206 | .where('id = :id', { id }) 207 | .execute(); 208 | await manager 209 | .createQueryBuilder() 210 | .delete() 211 | .from(RoleMenuEntity) 212 | .where('roleId = :id', { id }) 213 | .execute(); 214 | await manager 215 | .createQueryBuilder() 216 | .delete() 217 | .from(CasbinRule) 218 | .where({ ptype: 'p', v0: id }) 219 | .execute(); 220 | }); 221 | 222 | await this.casbinWatcher.publishData(); 223 | } 224 | 225 | async getMenusByRoleId(roleId: string) { 226 | const curRoleMenus = await this.roleMenuModel.find({ 227 | where: { roleId: roleId }, 228 | }); 229 | return curRoleMenus; 230 | } 231 | 232 | /** 233 | * 为角色分配菜单 234 | * @param roleId 235 | * @param menuIds 236 | */ 237 | async allocMenu(roleId: string, menuIds: string[]) { 238 | const curRoleMenus = await this.roleMenuModel.findBy({ 239 | roleId, 240 | }); 241 | 242 | const roleMenus = []; 243 | menuIds.forEach((menuId: string) => { 244 | const roleMenu = new RoleMenuEntity(); 245 | roleMenu.menuId = menuId; 246 | roleMenu.roleId = roleId; 247 | roleMenus.push(roleMenu); 248 | }); 249 | 250 | await this.defaultDataSource.transaction(async manager => { 251 | await manager.remove(RoleMenuEntity, curRoleMenus); 252 | await manager.save(RoleMenuEntity, roleMenus); 253 | 254 | await manager 255 | .createQueryBuilder() 256 | .delete() 257 | .from(CasbinRule) 258 | .where({ ptype: 'p', v0: roleId }) 259 | .execute(); 260 | 261 | const apis = await this.menuApiModel.findBy({ 262 | menuId: In(menuIds), 263 | }); 264 | 265 | const casbinRules = apis.map(api => { 266 | const casbinRule = new CasbinRule(); 267 | casbinRule.ptype = 'p'; 268 | casbinRule.v0 = roleId; 269 | casbinRule.v1 = api.path; 270 | casbinRule.v2 = api.method; 271 | casbinRule.v3 = api.menuId; 272 | 273 | return casbinRule; 274 | }); 275 | await manager 276 | .createQueryBuilder() 277 | .insert() 278 | .into(CasbinRule) 279 | .values(casbinRules) 280 | .execute(); 281 | 282 | const oldMenuIds = curRoleMenus.map(menu => menu.menuId); 283 | if (oldMenuIds.length !== menuIds.length) { 284 | // 如果有变化,查询所有分配了该角色的用户,给对应所有用户发通知 285 | const userIds = (await this.userRoleModel.findBy({ roleId })).map( 286 | userRole => userRole.userId 287 | ); 288 | 289 | userIds.forEach(userId => { 290 | this.socketService.sendMessage(userId, { 291 | type: SocketMessageType.PermissionChange, 292 | }); 293 | }); 294 | } 295 | 296 | // 因为数组都是数字,所以先排序,排序之后把数组转换为字符串比较,写法比较简单 297 | const sortOldMenuIds = oldMenuIds.sort(); 298 | const sortMenusIds = menuIds.sort(); 299 | 300 | if (sortOldMenuIds.join() !== sortMenusIds.join()) { 301 | // 如果有变化,查询所有分配了该角色的用户,给对应所有用户发通知 302 | const userIds = (await this.userRoleModel.findBy({ roleId })).map( 303 | userRole => userRole.userId 304 | ); 305 | 306 | userIds.forEach(userId => { 307 | this.socketService.sendMessage(userId, { 308 | type: SocketMessageType.PermissionChange, 309 | }); 310 | }); 311 | } 312 | }); 313 | 314 | await this.casbinWatcher.publishData(); 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Marsview 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | contact information: 204 | 205 | -Email: 206 | -Official website: 207 | -------------------------------------------------------------------------------- /src/module/system/user/service/user.ts: -------------------------------------------------------------------------------- 1 | import { Config, Inject, Provide } from '@midwayjs/decorator'; 2 | import { InjectEntityModel } from '@midwayjs/typeorm'; 3 | import * as bcrypt from 'bcryptjs'; 4 | import { Not, Repository } from 'typeorm'; 5 | 6 | import { CasbinEnforcerService } from '@midwayjs/casbin'; 7 | import { NodeRedisWatcher } from '@midwayjs/casbin-redis-adapter'; 8 | import { CasbinRule } from '@midwayjs/casbin-typeorm-adapter'; 9 | import { RedisService } from '@midwayjs/redis'; 10 | import { BaseService } from '../../../../common/base-service'; 11 | import { MailService } from '../../../../common/mail-service'; 12 | import { AssertUtils } from '../../../../utils/assert'; 13 | import { FilterQuery } from '../../../../utils/filter-query'; 14 | import { like } from '../../../../utils/typeorm-utils'; 15 | import { FileEntity } from '../../file/entity/file'; 16 | import { FileService } from '../../file/service/file'; 17 | import { RoleEntity } from '../../role/entity/role'; 18 | import { SocketService } from '../../socket/service/socket'; 19 | import { SocketMessageType } from '../../socket/type'; 20 | import { UserDTO } from '../dto/user'; 21 | import { UserPageDTO } from '../dto/user-page'; 22 | import { UserEntity } from '../entity/user'; 23 | import { UserRoleEntity } from '../entity/user-role'; 24 | import { UserVO } from '../vo/user'; 25 | import { UserPageVO } from '../vo/user-page'; 26 | 27 | @Provide() 28 | export class UserService extends BaseService { 29 | @InjectEntityModel(UserEntity) 30 | userModel: Repository; 31 | @InjectEntityModel(FileEntity) 32 | fileModel: Repository; 33 | @Inject() 34 | fileService: FileService; 35 | @Inject() 36 | redisService: RedisService; 37 | @Inject() 38 | mailService: MailService; 39 | @InjectEntityModel(UserRoleEntity) 40 | userRoleModel: Repository; 41 | @Inject() 42 | socketService: SocketService; 43 | @Inject() 44 | casbinEnforcerService: CasbinEnforcerService; 45 | @Inject() 46 | casbinWatcher: NodeRedisWatcher; 47 | @Config('title') 48 | title: string; 49 | @Config('loginUrl') 50 | loginUrl: string; 51 | @Config('defaultPassword') 52 | defaultPassword: string; 53 | 54 | getModel(): Repository { 55 | return this.userModel; 56 | } 57 | 58 | /** 59 | * 分页获取用户列表 60 | * @param userPageDTO 61 | * @returns 62 | */ 63 | async getUsersByPage(userPageDTO: UserPageDTO): Promise { 64 | const query = new FilterQuery(); 65 | 66 | query 67 | .append( 68 | 'phoneNumber', 69 | like(userPageDTO.phoneNumber), 70 | !!userPageDTO.phoneNumber 71 | ) 72 | .append('userName', like(userPageDTO.userName), !!userPageDTO.userName) 73 | .append('nickName', like(userPageDTO.nickName), !!userPageDTO.nickName) 74 | .append('email', like(userPageDTO.email), !!userPageDTO.email); 75 | 76 | const pageInfo = this.getPageByPageDTO(userPageDTO); 77 | 78 | const [data, total] = await this.userModel 79 | .createQueryBuilder('t') 80 | .leftJoinAndSelect(UserRoleEntity, 'user_role', 't.id = user_role.userId') 81 | .leftJoinAndMapMany( 82 | 't.roles', 83 | RoleEntity, 84 | 'role', 85 | 'role.id = user_role.roleId' 86 | ) 87 | .leftJoinAndMapOne( 88 | 't.avatar', 89 | FileEntity, 90 | 'file', 91 | 'file.pkValue = t.id and file.pkName = "user_avatar"' 92 | ) 93 | .where(query.where) 94 | .skip(pageInfo.skip) 95 | .take(pageInfo.take) 96 | .orderBy('t.createDate', 'DESC') 97 | .getManyAndCount(); 98 | 99 | return { 100 | data: data.map(entity => entity.toVO()), 101 | total, 102 | }; 103 | } 104 | 105 | /** 106 | * 创建用户 107 | * @param userDTO 108 | * @returns 109 | */ 110 | async createUser(userDTO: UserDTO) { 111 | const entity = userDTO.toEntity(); 112 | const { userName, phoneNumber, email } = userDTO; 113 | 114 | let notExist = (await this.userModel.countBy({ userName })) === 0; 115 | AssertUtils.isTrue(notExist, '当前用户名已存在'); 116 | 117 | notExist = (await this.userModel.countBy({ phoneNumber })) === 0; 118 | AssertUtils.isTrue(notExist, '当前手机号已存在'); 119 | 120 | notExist = (await this.userModel.countBy({ email })) === 0; 121 | AssertUtils.isTrue(notExist, '当前邮箱已存在'); 122 | 123 | // 添加用户,对密码进行加盐加密 124 | const hashPassword = bcrypt.hashSync(this.defaultPassword, 10); 125 | entity.password = hashPassword; 126 | 127 | // 使用事物 128 | await this.defaultDataSource.transaction(async manager => { 129 | await manager.save(UserEntity, entity); 130 | 131 | if (userDTO.avatar) { 132 | await manager 133 | .createQueryBuilder() 134 | .update(FileEntity) 135 | .set({ 136 | pkValue: entity.id, 137 | pkName: 'user_avatar', 138 | }) 139 | .where('id = :id', { id: userDTO.avatar }) 140 | .execute(); 141 | } 142 | 143 | await manager.save( 144 | UserRoleEntity, 145 | userDTO.roleIds.map(roleId => { 146 | const userRole = new UserRoleEntity(); 147 | userRole.roleId = roleId; 148 | userRole.userId = entity.id; 149 | return userRole; 150 | }) 151 | ); 152 | 153 | // 构造策略对象 154 | const casbinRules = userDTO.roleIds.map(roleId => { 155 | const casbinRule = new CasbinRule(); 156 | casbinRule.ptype = 'g'; 157 | casbinRule.v0 = entity.id; 158 | casbinRule.v1 = roleId; 159 | return casbinRule; 160 | }); 161 | 162 | // 保存策略 163 | await manager 164 | .createQueryBuilder() 165 | .insert() 166 | .into(CasbinRule) 167 | .values(casbinRules) 168 | .execute(); 169 | 170 | this.mailService.sendMail({ 171 | to: email, 172 | subject: `${this.title}平台账号创建成功`, 173 | html: `
174 |

${userDTO.nickName},你的账号已开通成功

175 |

登录地址:${this.loginUrl}

176 |

登录账号:${userDTO.email}

177 |

登录密码:${this.defaultPassword}

178 |
`, 179 | }); 180 | }); 181 | 182 | // 发消息给其它进程,同步最新的策略 183 | this.casbinWatcher.publishData(); 184 | } 185 | 186 | /** 187 | * 更新用户 188 | * @param userDTO 189 | * @returns 190 | */ 191 | async updateUser(userDTO: UserDTO) { 192 | const { userName, phoneNumber, email, id, nickName, avatar } = userDTO; 193 | 194 | let user = await this.userModel.findOneBy({ userName, id: Not(id) }); 195 | AssertUtils.isTrue(!user, '当前用户名已存在'); 196 | 197 | user = await this.userModel.findOneBy({ phoneNumber, id: Not(id) }); 198 | AssertUtils.notEmpty(!user, '当前手机号已存在'); 199 | 200 | user = await this.userModel.findOneBy({ email, id: Not(id) }); 201 | AssertUtils.notEmpty(!user, '当前邮箱已存在'); 202 | 203 | const userRolesMap = await this.userRoleModel.findBy({ 204 | userId: userDTO.id, 205 | }); 206 | 207 | await this.defaultDataSource.transaction(async manager => { 208 | const casbinRules = userDTO.roleIds.map(roleId => { 209 | const casbinRule = new CasbinRule(); 210 | casbinRule.ptype = 'g'; 211 | casbinRule.v0 = userDTO.id; 212 | casbinRule.v1 = roleId; 213 | return casbinRule; 214 | }); 215 | 216 | Promise.all([ 217 | manager 218 | .createQueryBuilder() 219 | .update(UserEntity) 220 | .set({ 221 | nickName, 222 | phoneNumber, 223 | }) 224 | .where('id = :id', { id: userDTO.id }) 225 | .execute(), 226 | manager.remove(UserRoleEntity, userRolesMap), 227 | manager.save( 228 | UserRoleEntity, 229 | userDTO.roleIds.map(roleId => { 230 | const userRole = new UserRoleEntity(); 231 | userRole.roleId = roleId; 232 | userRole.userId = userDTO.id; 233 | return userRole; 234 | }) 235 | ), 236 | await manager 237 | .createQueryBuilder() 238 | .delete() 239 | .from(CasbinRule) 240 | .where({ ptype: 'g', v0: userDTO.id }) 241 | .execute(), 242 | await manager 243 | .createQueryBuilder() 244 | .insert() 245 | .into(CasbinRule) 246 | .values(casbinRules) 247 | .execute(), 248 | ]); 249 | 250 | // 根据当前用户id在文件表里查询 251 | const fileRecord = await this.fileModel.findOneBy({ 252 | pkValue: id, 253 | pkName: 'user_avatar', 254 | }); 255 | 256 | // 如果查到文件,并且当前头像是空的,只需要给原来的文件给删除就行了。 257 | if (fileRecord && !avatar) { 258 | await this.fileModel.remove(fileRecord); 259 | } else if (fileRecord && avatar && fileRecord.id !== avatar) { 260 | // 如果查到文件,并且有当前头像,并且原来的文件id不等于当前传过来的文件id 261 | // 删除原来的文件 262 | // 把当前的用户id更新到新文件行数据中 263 | await Promise.all([ 264 | manager.delete(FileEntity, fileRecord.id), 265 | manager 266 | .createQueryBuilder() 267 | .update(FileEntity) 268 | .set({ 269 | pkValue: id, 270 | pkName: 'user_avatar', 271 | }) 272 | .where('id = :id', { id: userDTO.avatar }) 273 | .execute(), 274 | ]); 275 | } else if (!fileRecord && avatar) { 276 | // 如果以前没有文件,现在有文件,直接更新就行了 277 | manager 278 | .createQueryBuilder() 279 | .update(FileEntity) 280 | .set({ 281 | pkValue: id, 282 | pkName: 'user_avatar', 283 | }) 284 | .where('id = :id', { id: userDTO.avatar }) 285 | .execute(); 286 | } 287 | 288 | // 检测当前用户分配的角色有没有变化,如果有变化,发通知给前端 289 | const oldRoleIds = userRolesMap.map(role => role.roleId); 290 | // 先判断两个数量是不是一样的 291 | if (oldRoleIds.length !== userDTO.roleIds.length) { 292 | this.socketService.sendMessage(userDTO.id, { 293 | type: SocketMessageType.PermissionChange, 294 | }); 295 | } else { 296 | // 因为数组都是数字,所以先排序,排序之后把数组转换为字符串比较,写法比较简单 297 | const sortOldRoleIds = oldRoleIds.sort(); 298 | const sortRoleIds = userDTO.roleIds.sort(); 299 | 300 | if (sortOldRoleIds.join() !== sortRoleIds.join()) { 301 | this.socketService.sendMessage(userDTO.id, { 302 | type: SocketMessageType.PermissionChange, 303 | }); 304 | } 305 | } 306 | }); 307 | 308 | // 发消息给其它进程,同步最新的策略 309 | this.casbinWatcher.publishData(); 310 | } 311 | 312 | /** 313 | * 删除用户 314 | * @param id 315 | */ 316 | async removeUser(id: string) { 317 | await this.defaultDataSource.transaction(async manager => { 318 | const tokens = await this.redisService.smembers(`userToken_${id}`); 319 | const refreshTokens = await this.redisService.smembers( 320 | `userRefreshToken_${id}` 321 | ); 322 | 323 | await Promise.all([ 324 | manager 325 | .createQueryBuilder() 326 | .delete() 327 | .from(UserEntity) 328 | .where('id = :id', { id }) 329 | .execute(), 330 | manager 331 | .createQueryBuilder() 332 | .delete() 333 | .from(FileEntity) 334 | .where('pkValue = :pkValue', { pkValue: id }) 335 | .andWhere('pkName = "user_avatar"') 336 | .execute(), 337 | ...tokens.map(token => this.redisService.del(`token:${token}`)), 338 | ...refreshTokens.map(refreshToken => 339 | this.redisService.del(`refreshToken:${refreshToken}`) 340 | ), 341 | manager 342 | .createQueryBuilder() 343 | .delete() 344 | .from(CasbinRule) 345 | .where({ ptype: 'g', v0: id }) 346 | .execute(), 347 | ...tokens.map(token => this.redisService.del(`token:${token}`)), 348 | ...refreshTokens.map(refreshToken => 349 | this.redisService.del(`refreshToken:${refreshToken}`) 350 | ), 351 | ]); 352 | }); 353 | 354 | this.casbinWatcher.publishData(); 355 | } 356 | 357 | /** 358 | * 根据邮箱获取用户 359 | */ 360 | async getUserByEmail(email: string) { 361 | return await this.userModel.findOneBy({ email }); 362 | } 363 | 364 | /** 365 | * 根据用户id获取已分配角色id 列表 366 | */ 367 | async getRoleIdsByUserId(userId: string) { 368 | const query = this.userModel.createQueryBuilder('t'); 369 | 370 | const user = (await query 371 | .where('t.id = :id', { id: userId }) 372 | .leftJoinAndSelect(UserRoleEntity, 'userRole', 't.id = userRole.userId') 373 | .leftJoinAndMapMany( 374 | 't.roles', 375 | RoleEntity, 376 | 'role', 377 | 'role.id = userRole.roleId' 378 | ) 379 | .getOne()) as unknown as UserVO; 380 | 381 | return user?.roles?.map(o => o.id) || []; 382 | } 383 | } 384 | --------------------------------------------------------------------------------