├── .dockerignore ├── .env.development ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── ecosystem.config.js ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── public └── upload │ └── logo-202205152318102.jpg ├── sql └── kz-admin.sql ├── src ├── app.module.ts ├── common │ ├── class │ │ └── res.class.ts │ ├── contants │ │ ├── decorator.contants.ts │ │ ├── error-code.contants.ts │ │ └── param-config.contants.ts │ ├── decorators │ │ └── keep.decorator.ts │ ├── dto │ │ └── page.dto.ts │ ├── exceptions │ │ ├── api.exception.ts │ │ └── socket.exception.ts │ ├── filter │ │ └── api-exception.filter.ts │ ├── interceptors │ │ ├── api-transform.interceptor.ts │ │ └── timeout.interceptor.ts │ └── interfaces │ │ └── admin-user.interface.ts ├── config │ ├── configuration.ts │ ├── defineConfig.ts │ └── dev.config.ts ├── entities │ ├── admin │ │ ├── sys-config.entity.ts │ │ ├── sys-login-log.entity.ts │ │ ├── sys-menu.entity.ts │ │ ├── sys-role-menu.entity.ts │ │ ├── sys-role.entity.ts │ │ ├── sys-task-log.entity.ts │ │ ├── sys-task.entity.ts │ │ ├── sys-user-role.entity.ts │ │ ├── sys-user.entity.ts │ │ └── tool-storage.entity.ts │ └── base.entity.ts ├── main.ts ├── mission │ ├── README.md │ ├── jobs │ │ ├── email.job.ts │ │ ├── http-request.job.ts │ │ └── sys-log-clear.job.ts │ ├── mission.decorator.ts │ └── mission.module.ts ├── modules │ ├── admin │ │ ├── account │ │ │ ├── account.controller.ts │ │ │ └── account.module.ts │ │ ├── admin.constants.ts │ │ ├── admin.interface.ts │ │ ├── admin.module.ts │ │ ├── core │ │ │ ├── decorators │ │ │ │ ├── admin-user.decorator.ts │ │ │ │ ├── authorize.decorator.ts │ │ │ │ ├── log-disabled.decorator.ts │ │ │ │ └── permission-optional.decorator.ts │ │ │ ├── guards │ │ │ │ └── auth.guard.ts │ │ │ ├── permission │ │ │ │ └── index.ts │ │ │ └── provider │ │ │ │ └── root-role-id.provider.ts │ │ ├── login │ │ │ ├── login.class.ts │ │ │ ├── login.controller.ts │ │ │ ├── login.dto.ts │ │ │ ├── login.module.ts │ │ │ └── login.service.ts │ │ ├── system │ │ │ ├── log │ │ │ │ ├── log.class.ts │ │ │ │ ├── log.controller.ts │ │ │ │ ├── log.dto.ts │ │ │ │ └── log.service.ts │ │ │ ├── menu │ │ │ │ ├── menu.class.ts │ │ │ │ ├── menu.controller.ts │ │ │ │ ├── menu.dto.ts │ │ │ │ └── menu.service.ts │ │ │ ├── online │ │ │ │ ├── online.class.ts │ │ │ │ ├── online.controller.ts │ │ │ │ ├── online.dto.ts │ │ │ │ └── online.service.ts │ │ │ ├── param-config │ │ │ │ ├── param-config.controller.ts │ │ │ │ ├── param-config.dto.ts │ │ │ │ └── param-config.service.ts │ │ │ ├── role │ │ │ │ ├── role.class.ts │ │ │ │ ├── role.controller.ts │ │ │ │ ├── role.dto.ts │ │ │ │ └── role.service.ts │ │ │ ├── serve │ │ │ │ ├── serve.class.ts │ │ │ │ ├── serve.controller.ts │ │ │ │ └── serve.service.ts │ │ │ ├── system.module.ts │ │ │ ├── task │ │ │ │ ├── task.controller.ts │ │ │ │ ├── task.dto.ts │ │ │ │ ├── task.processor.ts │ │ │ │ └── task.service.ts │ │ │ └── user │ │ │ │ ├── user.class.ts │ │ │ │ ├── user.controller.ts │ │ │ │ ├── user.dto.ts │ │ │ │ └── user.service.ts │ │ ├── tools │ │ │ ├── email │ │ │ │ ├── email.controller.ts │ │ │ │ └── email.dto.ts │ │ │ ├── storage │ │ │ │ ├── storage.class.ts │ │ │ │ ├── storage.controller.ts │ │ │ │ ├── storage.dto.ts │ │ │ │ └── storage.service.ts │ │ │ └── tools.module.ts │ │ └── upload │ │ │ ├── upload.controller.ts │ │ │ └── upload.module.ts │ └── ws │ │ ├── admin-ws.gateway.ts │ │ ├── admin-ws.guard.ts │ │ ├── admin-ws.service.ts │ │ ├── auth.service.ts │ │ ├── ws.event.ts │ │ └── ws.module.ts ├── setup-swagger.ts ├── shared │ ├── logger │ │ ├── logger.constants.ts │ │ ├── logger.interface.ts │ │ ├── logger.module.ts │ │ ├── logger.service.ts │ │ ├── typeorm-logger.service.ts │ │ └── utils │ │ │ ├── app-root-path.util.ts │ │ │ └── home-dir.ts │ ├── redis │ │ ├── redis.constants.ts │ │ ├── redis.interface.ts │ │ └── redis.module.ts │ ├── services │ │ ├── email.service.ts │ │ ├── ip.service.ts │ │ ├── qq.service.ts │ │ ├── redis.service.ts │ │ └── util.service.ts │ └── shared.module.ts └── utils │ ├── captcha.ts │ ├── crypto.util.ts │ ├── file.util.ts │ ├── index.util.ts │ └── is.ts ├── tsconfig.build.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | package-lock.json 5 | yarn.lock 6 | /sql 7 | /test 8 | 9 | **/*.js.map 10 | **/*.d.ts 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | lerna-debug.log* 19 | 20 | # OS 21 | .DS_Store 22 | 23 | # Tests 24 | /coverage 25 | /.nyc_output 26 | 27 | # IDEs and editors 28 | /.idea 29 | .project 30 | .classpath 31 | .c9/ 32 | *.launch 33 | .settings/ 34 | *.sublime-workspace 35 | 36 | # IDE - VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | 43 | # Code 44 | src/config/config.development.* 45 | docs/* 46 | sql/* 47 | test/* 48 | README.md -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | APP_NAME = Kz Admin 2 | PORT = 5001 3 | WS_PORT = 5002 4 | WS_PATH = ws-api 5 | 6 | PREFIX = api 7 | 8 | JWT_SECRET = 123456 9 | 10 | MYSQL_HOST = 127.0.0.1 11 | MYSQL_PORT = 3306 12 | MYSQL_DATABASE = kz-admin 13 | MYSQL_USERNAME = kz-admin 14 | MYSQL_PASSWORD = Aa123456 15 | MYSQL_SYNC = true 16 | 17 | REDIS_PORT = 6379 18 | REDIS_HOST = 127.0.0.1 19 | REDIS_PASSWORD = 20 | REDIS_DATABASE = 0 21 | 22 | SWAGGER_ENABLE = true 23 | SWAGGER_TITLE = kz-admin 24 | SWAGGER_PARH = swagger-ui 25 | 26 | EMAIL_HOST = smtp.qq.com 27 | EMAIL_PORT = 465 28 | EMAIL_USER = 12345@qq.com 29 | EMAIL_PASS = 12345 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], 9 | root: true, 10 | env: { 11 | node: true, 12 | jest: true, 13 | }, 14 | ignorePatterns: ['.eslintrc.js'], 15 | rules: { 16 | '@typescript-eslint/interface-name-prefix': 'off', 17 | '@typescript-eslint/explicit-function-return-type': 'off', 18 | '@typescript-eslint/explicit-module-boundary-types': 'off', 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | '@typescript-eslint/no-unused-vars': 'off', 21 | '@typescript-eslint/no-var-requires': 'off', 22 | '@typescript-eslint/no-inferrable-types': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env.production -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "bracketSpacing": true, 9 | "arrowParens": "always", 10 | "trailingComma": "all", 11 | "nsertPragma": false, 12 | "endOfLine": "auto" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 as builder 2 | 3 | WORKDIR /kz-nest-admin 4 | 5 | COPY . . 6 | 7 | # RUN npm set registry https://registry.npmmirror.com 8 | RUN npm i -g pnpm 9 | RUN pnpm install 10 | # build 11 | RUN npm run build 12 | 13 | # httpserver set port 14 | EXPOSE 5001 15 | # websokcet set port 16 | EXPOSE 5002 17 | 18 | CMD ["npm", "run", "start:prod"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 kuizuo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kz-nest-admin 2 | 3 | **仓库已迁移至 [kuizuo/kz-admin](https://github.com/kuizuo/kz-admin),本仓库将不在维护。** 4 | 5 | 基于 NestJs + TypeScript + TypeORM + Redis + MySql + Vben Admin 编写的一款简单高效的前后端分离的权限管理系统。希望这个项目在全栈的路上能够帮助到你。 6 | 7 | - [在线预览](https://admin.kuizuo.cn) 8 | - [Swagger Api文档](https://admin.kuizuo.cn/swagger-ui/static/index.html) 9 | - [前端项目](https://github.com/kuizuo/kz-vue-admin) 10 | 11 | 演示环境 12 | 13 | - 账号: admin 14 | - 密码: 123456 15 | 16 | ## 安装使用 17 | 18 | - 获取项目代码 19 | 20 | ```bash 21 | git clone https://github.com/kuizuo/kz-nest-admin 22 | ``` 23 | 24 | - 安装依赖 25 | 26 | ```bash 27 | cd kz-nest-admin 28 | 29 | pnpm install 30 | ``` 31 | 32 | - 运行 33 | 34 | ```bash 35 | pnpm run dev 36 | ``` 37 | 38 | - 打包 39 | 40 | ```bash 41 | pnpm build 42 | ``` 43 | 44 | ## 系统截图 45 | 46 | ![image-20220505171231754](https://img.kuizuo.cn/image-20220505171231754.png) 47 | 48 | ![image-20220505171349742](https://img.kuizuo.cn/image-20220505171349742.png) 49 | 50 | ![image-20220505171210006](https://img.kuizuo.cn/image-20220505171210006.png) 51 | 52 | ![image-20220505171306785](https://img.kuizuo.cn/image-20220505171306785.png) 53 | 54 | ### 致谢 55 | 56 | - [sf-nest-admin](https://github.com/hackycy/sf-nest-admin) 57 | - [vite-vue3-admin](https://github.com/buqiyuan/vite-vue3-admin) 58 | - [vue-vben-admin](https://github.com/vbenjs/vue-vben-admin) 59 | 60 | ## LICENSE 61 | 62 | [MIT](https://github.com/kuizuo/kz-nest-admin/blob/dev/LICENSE) 63 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'kz-admin', // 项目名字,启动后的名字 5 | script: './dist/main.js', // 执行的文件 6 | cwd: './', // 根目录 7 | args: '', // 传递给脚本的参数 8 | watch: true, // 开启监听文件变动重启 9 | ignore_watch: ['node_modules', 'public', 'logs'], // 不用监听的文件 10 | instances: '1', // max表示最大的 应用启动实例个数,仅在 cluster 模式有效 默认为 fork 11 | autorestart: true, // 默认为 true, 发生异常的情况下自动重启 12 | max_memory_restart: '1G', 13 | error_file: './logs/app-err.log', // 错误日志文件 14 | out_file: './logs/app-out.log', // 正常日志文件 15 | merge_logs: true, // 设置追加日志而不是新建日志 16 | log_date_format: 'YYYY-MM-DD HH:mm:ss', // 指定日志文件的时间格式 17 | min_uptime: '60s', // 应用运行少于时间被认为是异常启动 18 | max_restarts: 30, // 最大异常重启次数 19 | restart_delay: 60, // 异常重启情况下,延时重启时间 20 | env: { 21 | NODE_ENV: 'development', 22 | }, 23 | env_development: { 24 | NODE_ENV: 'development', 25 | }, 26 | env_production: { 27 | NODE_ENV: 'production', 28 | }, 29 | env_test: { 30 | NODE_ENV: 'test', 31 | }, 32 | }, 33 | ], 34 | 35 | deploy: { 36 | production: { 37 | user: 'root', 38 | host: '39.108.99.86', 39 | ref: 'origin/master', 40 | repo: 'git@github.com:repo.git', 41 | path: '/var/www/AnJiaMallServer', 42 | 'post-deploy': 43 | 'npm install && npm run build && pm2 reload ecosystem.config.js --env production', 44 | }, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kz-nest-admin", 3 | "version": "0.1.0", 4 | "description": "基于NestJs + TypeScript + TypeORM + Redis + MySql + Vben Admin编写的一款前后端分离的权限管理系统", 5 | "packageManager": "pnpm@7.11.0", 6 | "author": { 7 | "name": "kuizuo", 8 | "email": "hi@kuizuo.cn", 9 | "url": "https://github.com/kuizuo" 10 | }, 11 | "license": "MIT", 12 | "scripts": { 13 | "prebuild": "rimraf dist", 14 | "build": "nest build", 15 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 16 | "start": "cross-env NODE_ENV=development nest start", 17 | "dev": "cross-env NODE_ENV=development nest start --watch", 18 | "start:debug": "cross-env NODE_ENV=development nest start --debug --watch", 19 | "start:prod": "cross-env NODE_ENV=production node dist/main", 20 | "pm2:prod": "pm2 start ecosystem.config.js --env production", 21 | "pm2:dev": "pm2 start ecosystem.config.js --env development", 22 | "pm2:test": "pm2 start ecosystem.config.js --env test", 23 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 24 | "test": "jest", 25 | "test:watch": "jest --watch", 26 | "test:cov": "jest --coverage", 27 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 28 | "test:e2e": "jest --config ./test/jest-e2e.json" 29 | }, 30 | "dependencies": { 31 | "@fastify/multipart": "^7.3.0", 32 | "@fastify/static": "^6.5.0", 33 | "@nestjs-modules/mailer": "^1.8.1", 34 | "@nestjs/axios": "^0.1.0", 35 | "@nestjs/bull": "^0.6.1", 36 | "@nestjs/common": "^9.1.6", 37 | "@nestjs/config": "^2.2.0", 38 | "@nestjs/core": "^9.1.6", 39 | "@nestjs/jwt": "^9.0.0", 40 | "@nestjs/passport": "^9.0.0", 41 | "@nestjs/platform-fastify": "^9.1.6", 42 | "@nestjs/platform-socket.io": "^9.1.6", 43 | "@nestjs/swagger": "^6.1.3", 44 | "@nestjs/typeorm": "^9.0.1", 45 | "@nestjs/websockets": "^9.1.6", 46 | "@types/ioredis": "^5.0.0", 47 | "@types/lodash": "^4.14.186", 48 | "axios": "^1.1.3", 49 | "bull": "^4.10.1", 50 | "cache-manager": "^5.1.1", 51 | "chalk": "^5.1.2", 52 | "class-transformer": "^0.5.1", 53 | "class-validator": "^0.13.2", 54 | "cron-parser": "^4.6.0", 55 | "cross-env": "^7.0.3", 56 | "crypto-js": "^4.1.1", 57 | "csurf": "^1.11.0", 58 | "dayjs": "^1.11.6", 59 | "dotenv": "16.0.3", 60 | "fastify": "^4.9.2", 61 | "fastify-multer": "^2.0.3", 62 | "helmet": "^6.0.0", 63 | "ioredis": "^5.2.3", 64 | "lodash": "^4.17.21", 65 | "mysql": "^2.18.1", 66 | "nanoid": "^4.0.0", 67 | "nodemailer": "^6.8.0", 68 | "passport": "^0.6.0", 69 | "passport-local": "^1.0.0", 70 | "reflect-metadata": "^0.1.13", 71 | "rimraf": "^3.0.2", 72 | "rxjs": "^7.5.7", 73 | "socket.io": "^4.5.3", 74 | "stacktrace-js": "^2.0.2", 75 | "svg-captcha": "^1.4.0", 76 | "systeminformation": "^5.12.11", 77 | "typeorm": "^0.3.10", 78 | "ua-parser-js": "^1.0.32", 79 | "winston": "^3.8.2", 80 | "winston-daily-rotate-file": "^4.7.1" 81 | }, 82 | "devDependencies": { 83 | "@nestjs/cli": "^9.1.5", 84 | "@nestjs/schematics": "^9.0.3", 85 | "@nestjs/testing": "^9.1.6", 86 | "@types/bull": "^4.10.0", 87 | "@types/cache-manager": "^4.0.2", 88 | "@types/jest": "29.2.0", 89 | "@types/multer": "^1.4.7", 90 | "@types/node": "^16.18.3", 91 | "@types/supertest": "^2.0.12", 92 | "@types/ua-parser-js": "^0.7.36", 93 | "@typescript-eslint/eslint-plugin": "^5.41.0", 94 | "@typescript-eslint/parser": "^5.41.0", 95 | "eslint": "^8.26.0", 96 | "eslint-config-prettier": "^8.5.0", 97 | "eslint-plugin-prettier": "^4.2.1", 98 | "jest": "^29.2.2", 99 | "prettier": "^2.7.1", 100 | "source-map-support": "^0.5.21", 101 | "supertest": "^6.3.1", 102 | "ts-jest": "^29.0.3", 103 | "ts-loader": "^9.4.1", 104 | "ts-node": "^10.9.1", 105 | "tsconfig-paths": "^4.1.0", 106 | "typescript": "^4.8.4" 107 | }, 108 | "jest": { 109 | "moduleFileExtensions": [ 110 | "js", 111 | "json", 112 | "ts" 113 | ], 114 | "rootDir": "src", 115 | "testRegex": ".*\\.spec\\.ts$", 116 | "transform": { 117 | "^.+\\.(t|j)s$": "ts-jest" 118 | }, 119 | "collectCoverageFrom": [ 120 | "**/*.(t|j)s" 121 | ], 122 | "coverageDirectory": "../coverage", 123 | "testEnvironment": "node" 124 | }, 125 | "repository": { 126 | "type": "git", 127 | "url": "git+https://github.com/kuizuo/kz-nest-admin.git" 128 | }, 129 | "bugs": { 130 | "url": "https://github.com/kuizuo/kz-nest-admin/issues" 131 | }, 132 | "homepage": "https://github.com/kuizuo/kz-nest-admin" 133 | } 134 | -------------------------------------------------------------------------------- /public/upload/logo-202205152318102.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuizuo/kz-nest-admin/d19f150a92498abee65bdf2e6dd6057a0909ae98/public/upload/logo-202205152318102.jpg -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import configuration from './config/configuration'; 4 | import { AdminModule } from './modules/admin/admin.module'; 5 | import { LoggerModuleOptions, WinstonLogLevel } from './shared/logger/logger.interface'; 6 | import { LoggerModule } from './shared/logger/logger.module'; 7 | import { SharedModule } from './shared/shared.module'; 8 | import { WSModule } from './modules/ws/ws.module'; 9 | import { TypeOrmModule } from '@nestjs/typeorm'; 10 | import { TypeORMLoggerService } from './shared/logger/typeorm-logger.service'; 11 | import { BullModule } from '@nestjs/bull'; 12 | import { MissionModule } from './mission/mission.module'; 13 | import { LOGGER_MODULE_OPTIONS } from './shared/logger/logger.constants'; 14 | 15 | @Module({ 16 | imports: [ 17 | ConfigModule.forRoot({ 18 | isGlobal: true, 19 | ignoreEnvFile: false, 20 | envFilePath: `.env.${process.env.NODE_ENV}`, 21 | load: [configuration], 22 | }), 23 | TypeOrmModule.forRootAsync({ 24 | imports: [ConfigModule, LoggerModule], 25 | useFactory: (configService: ConfigService, loggerOptions: LoggerModuleOptions) => ({ 26 | autoLoadEntities: true, 27 | type: configService.get('mysql.type'), 28 | host: configService.get('mysql.host'), 29 | port: configService.get('mysql.port'), 30 | username: configService.get('mysql.username'), 31 | password: configService.get('mysql.password'), 32 | database: configService.get('mysql.database'), 33 | synchronize: configService.get('mysql.synchronize'), 34 | logging: configService.get('mysql.logging'), 35 | timezone: configService.get('mysql.timezone'), // 时区 36 | logger: new TypeORMLoggerService(configService.get('mysql.logging'), loggerOptions), 37 | }), 38 | inject: [ConfigService, LOGGER_MODULE_OPTIONS], 39 | }), 40 | LoggerModule.forRootAsync( 41 | { 42 | imports: [ConfigModule], 43 | useFactory: (configService: ConfigService) => { 44 | return { 45 | level: configService.get('logger.level'), 46 | consoleLevel: configService.get('logger.consoleLevel'), 47 | timestamp: configService.get('logger.timestamp'), 48 | maxFiles: configService.get('logger.maxFiles'), 49 | maxFileSize: configService.get('logger.maxFileSize'), 50 | disableConsoleAtProd: configService.get('logger.disableConsoleAtProd'), 51 | dir: configService.get('logger.dir'), 52 | errorLogName: configService.get('logger.errorLogName'), 53 | appLogName: configService.get('logger.appLogName'), 54 | }; 55 | }, 56 | inject: [ConfigService], 57 | }, 58 | true, 59 | ), 60 | MissionModule.forRoot(), 61 | BullModule.forRoot({}), 62 | SharedModule, 63 | AdminModule, 64 | WSModule, 65 | ], 66 | }) 67 | export class AppModule {} 68 | -------------------------------------------------------------------------------- /src/common/class/res.class.ts: -------------------------------------------------------------------------------- 1 | export class ResOp { 2 | readonly data: any; 3 | readonly code: number; 4 | readonly message: string; 5 | 6 | constructor(code: number, data?: any, message = 'success') { 7 | this.code = code; 8 | this.data = data; 9 | this.message = message; 10 | } 11 | 12 | static success(data?: any) { 13 | return new ResOp(200, data); 14 | } 15 | 16 | static error(code: number, message) { 17 | return new ResOp(code, {}, message); 18 | } 19 | } 20 | 21 | export class PageResult { 22 | items?: Array; 23 | total: number; 24 | } 25 | -------------------------------------------------------------------------------- /src/common/contants/decorator.contants.ts: -------------------------------------------------------------------------------- 1 | // @Keep 2 | export const TRANSFORM_KEEP_KEY_METADATA = 'common:transform_keep'; 3 | 4 | // @Mission 5 | export const MISSION_KEY_METADATA = 'common:mission'; 6 | -------------------------------------------------------------------------------- /src/common/contants/error-code.contants.ts: -------------------------------------------------------------------------------- 1 | // /** 2 | // * 统一错误代码定义 3 | // */ 4 | export const ErrorCodeMap = { 5 | // 10000 - 99999 业务操作错误 6 | 10000: '参数校验异常', 7 | 10001: '系统用户已存在', 8 | 10002: '验证码填写有误', 9 | 10003: '用户名密码有误', 10 | 10004: '节点路由已存在', 11 | 10005: '权限必须包含父节点', 12 | 10006: '非法操作:该节点仅支持目录类型父节点', 13 | 10007: '非法操作:节点类型无法直接转换', 14 | 10008: '该角色存在关联用户,请先删除关联用户', 15 | 10009: '该部门存在关联用户,请先删除关联用户', 16 | 10010: '该部门存在关联角色,请先删除关联角色', 17 | 10011: '旧密码与原密码不一致', 18 | 10012: '如想下线自身可右上角退出', 19 | 10013: '不允许下线该用户', 20 | 10014: '父级菜单不存在', 21 | 10015: '该部门存在子部门,请先删除子部门', 22 | 10016: '系统内置功能不允许操作', 23 | 10017: '用户不存在', 24 | 10018: '无法查找当前用户所属部门', 25 | 10019: '部门不存在', 26 | 10020: '任务不存在', 27 | 10021: '参数配置键值对已存在', 28 | 10022: '所分配的默认角色不存在', 29 | 10101: '不安全的任务,确保执行的加入@Mission注解', 30 | 10102: '所执行的任务不存在', 31 | 32 | 10200: '请求频率过快,请一分钟后再试', 33 | 10201: '一天最多发送5条验证码', 34 | 10202: '验证码发送失败', 35 | 36 | // token相关 37 | 11001: '登录无效或无权限访问', 38 | 11002: '登录身份已过期', 39 | 11003: '无权限,请联系管理员申请权限', 40 | 41 | // OSS相关 42 | 20001: '当前创建的文件或目录已存在', 43 | 20002: '无需操作', 44 | 20003: '已超出支持的最大处理数量', 45 | }; 46 | -------------------------------------------------------------------------------- /src/common/contants/param-config.contants.ts: -------------------------------------------------------------------------------- 1 | export const SYS_USER_INITPASSWORD = 'sys_user_initPassword'; 2 | export const SYS_API_TOKEN = 'sys_api_token'; 3 | -------------------------------------------------------------------------------- /src/common/decorators/keep.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { TRANSFORM_KEEP_KEY_METADATA } from '../contants/decorator.contants'; 3 | 4 | /** 5 | * 不转化成JSON结构,保留原有返回 6 | */ 7 | export const Keep = () => SetMetadata(TRANSFORM_KEEP_KEY_METADATA, true); 8 | -------------------------------------------------------------------------------- /src/common/dto/page.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { IsInt, IsOptional, Min } from 'class-validator'; 4 | 5 | export class PaginateDto { 6 | @ApiProperty({ 7 | description: '当前页数', 8 | required: false, 9 | default: 10, 10 | }) 11 | @Type(() => Number) 12 | @IsInt() 13 | @Min(1) 14 | readonly pageSize: number = 10; 15 | 16 | @ApiProperty({ 17 | description: '当前页包含数量', 18 | required: false, 19 | default: 1, 20 | }) 21 | @Type(() => Number) 22 | @IsInt() 23 | @Min(1) 24 | readonly page: number = 1; 25 | 26 | @ApiProperty({ 27 | description: '时间戳', 28 | required: false, 29 | }) 30 | @Type(() => Number) 31 | @IsInt() 32 | @IsOptional() 33 | readonly _t: number; 34 | } 35 | -------------------------------------------------------------------------------- /src/common/exceptions/api.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | import { ErrorCodeMap } from '../contants/error-code.contants'; 3 | 4 | /** 5 | * Api业务异常均抛出该异常 6 | */ 7 | export class ApiException extends HttpException { 8 | /** 9 | * 业务类型错误代码,非Http code 10 | */ 11 | private errorCode: number; 12 | 13 | constructor(errorCode: number) { 14 | super(ErrorCodeMap[errorCode], 200); 15 | this.errorCode = errorCode; 16 | } 17 | 18 | getErrorCode(): number { 19 | return this.errorCode; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/common/exceptions/socket.exception.ts: -------------------------------------------------------------------------------- 1 | import { WsException } from '@nestjs/websockets'; 2 | import { ErrorCodeMap } from '../contants/error-code.contants'; 3 | 4 | export class SocketException extends WsException { 5 | private errorCode: number; 6 | 7 | constructor(errorCode: number) { 8 | super(ErrorCodeMap[errorCode]); 9 | this.errorCode = errorCode; 10 | } 11 | 12 | getErrorCode(): number { 13 | return this.errorCode; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/common/filter/api-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; 2 | import { FastifyReply } from 'fastify'; 3 | import { ApiException } from '../exceptions/api.exception'; 4 | import { ResOp } from '../class/res.class'; 5 | import { LoggerService } from '@/shared/logger/logger.service'; 6 | 7 | /** 8 | * 异常接管,统一异常返回数据 9 | */ 10 | @Catch() 11 | export class ApiExceptionFilter implements ExceptionFilter { 12 | constructor(private logger: LoggerService) {} 13 | 14 | catch(exception: unknown, host: ArgumentsHost) { 15 | const ctx = host.switchToHttp(); 16 | const response = ctx.getResponse(); 17 | 18 | // check api exection 19 | const status = 20 | exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; 21 | // set json response 22 | response.header('Content-Type', 'application/json; charset=utf-8'); 23 | // prod env will not return internal error message 24 | const code = 25 | exception instanceof ApiException ? (exception as ApiException).getErrorCode() : status; 26 | let message = '服务器异常,请稍后再试'; 27 | // 开发模式下提示500类型错误,生产模式下屏蔽500内部错误提示 28 | if (process.env.NODE_ENV === 'development' || status < 500) { 29 | message = exception instanceof HttpException ? exception.message : `${exception}`; 30 | } 31 | // 记录 500 日志 32 | if (status >= 500) { 33 | this.logger.error(exception, ApiExceptionFilter.name); 34 | } 35 | const result = new ResOp(code, null, message); 36 | response.status(status).send(result); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/common/interceptors/api-transform.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { Observable } from 'rxjs'; 4 | import { FastifyReply } from 'fastify'; 5 | import { map } from 'rxjs/operators'; 6 | import { TRANSFORM_KEEP_KEY_METADATA } from '../contants/decorator.contants'; 7 | import { ResOp } from '../class/res.class'; 8 | 9 | /** 10 | * 统一处理返回接口结果,如果不需要则添加@Keep装饰器 11 | */ 12 | export class ApiTransformInterceptor implements NestInterceptor { 13 | constructor(private readonly reflector: Reflector) {} 14 | intercept(context: ExecutionContext, next: CallHandler): Observable { 15 | return next.handle().pipe( 16 | map((data) => { 17 | const keep = this.reflector.get(TRANSFORM_KEEP_KEY_METADATA, context.getHandler()); 18 | if (keep) { 19 | return data; 20 | } else { 21 | const response = context.switchToHttp().getResponse(); 22 | response.header('Content-Type', 'application/json; charset=utf-8'); 23 | return new ResOp(200, data); 24 | } 25 | }), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/common/interceptors/timeout.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | RequestTimeoutException, 7 | } from '@nestjs/common'; 8 | import { Observable, throwError, TimeoutError } from 'rxjs'; 9 | import { catchError, timeout } from 'rxjs/operators'; 10 | 11 | @Injectable() 12 | export class TimeoutInterceptor implements NestInterceptor { 13 | constructor(private readonly time: number = 5000) {} 14 | 15 | intercept(context: ExecutionContext, next: CallHandler): Observable { 16 | return next.handle().pipe( 17 | timeout(this.time), 18 | catchError((err) => { 19 | if (err instanceof TimeoutError) { 20 | return throwError(new RequestTimeoutException('请求超时')); 21 | } 22 | return throwError(err); 23 | }), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/common/interfaces/admin-user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IAdminUser { 2 | uid: number; 3 | username: string; 4 | pv: number; 5 | role: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/config/configuration.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { IConfig } from './defineConfig'; 3 | 4 | export default () => { 5 | let config: IConfig = {}; 6 | 7 | try { 8 | // 读取同目录下的配置 需使用require载入 9 | const configPath = fs.readdirSync(__dirname); 10 | for (const p of configPath) { 11 | if (/\.config\.[t|j]s$/.test(p)) { 12 | const temp = require('./' + p).default; 13 | config = { ...config, ...temp }; 14 | } 15 | } 16 | } catch (e) { 17 | console.log(e); 18 | // 无效配置则自动忽略 19 | } 20 | return config; 21 | }; 22 | -------------------------------------------------------------------------------- /src/config/defineConfig.ts: -------------------------------------------------------------------------------- 1 | import { LoggerModuleOptions as LoggerConfigOptions } from 'src/shared/logger/logger.interface'; 2 | import { LoggerOptions } from 'typeorm'; 3 | 4 | /** 5 | * 用于智能提示 6 | */ 7 | export function defineConfig(config: IConfig): IConfig { 8 | return config; 9 | } 10 | 11 | /** 12 | * kz-admin 配置 13 | */ 14 | export interface IConfig { 15 | /** 16 | * 管理员角色ID,一旦分配,该角色下分配的管理员都为超级管理员 17 | */ 18 | rootRoleId?: number; 19 | /** 20 | * 用户鉴权Token密钥 21 | */ 22 | jwt?: JwtConfigOptions; 23 | /** 24 | * Mysql数据库配置 25 | */ 26 | database?: DataBaseConfigOptions; 27 | /** 28 | * Redis配置 29 | */ 30 | redis?: RedisConfigOptions; 31 | /** 32 | * 应用级别日志配置 33 | */ 34 | logger?: LoggerConfigOptions; 35 | /** 36 | * Swagger文档配置 37 | */ 38 | swagger?: SwaggerConfigOptions; 39 | } 40 | 41 | //--------- config interface ------------ 42 | 43 | export interface JwtConfigOptions { 44 | secret: string; 45 | } 46 | 47 | export interface RedisConfigOptions { 48 | host?: string; 49 | port?: number | string; 50 | password?: string; 51 | db?: number; 52 | } 53 | 54 | export interface DataBaseConfigOptions { 55 | type?: string; 56 | host?: string; 57 | port?: number | string; 58 | username?: string; 59 | password?: string; 60 | database?: string; 61 | synchronize?: boolean; 62 | logging?: LoggerOptions; 63 | } 64 | 65 | export interface SwaggerConfigOptions { 66 | enable?: boolean; 67 | path?: string; 68 | title?: string; 69 | desc?: string; 70 | version?: string; 71 | } 72 | -------------------------------------------------------------------------------- /src/config/dev.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | rootRoleId: 1, 3 | appName: process.env.APP_NAME, 4 | mysql: { 5 | type: 'mysql', 6 | host: process.env.MYSQL_HOST, 7 | port: process.env.MYSQL_PORT, 8 | username: process.env.MYSQL_USERNAME, 9 | password: process.env.MYSQL_PASSWORD, 10 | database: process.env.MYSQL_DATABASE, 11 | synchronize: process.env.MYSQL_SYNC, 12 | entities: ['@/entities/*.entity.ts'], 13 | logging: false, 14 | timezone: '+08:00', // 东八区 15 | }, 16 | mongo: { 17 | url: process.env.MONGO_URL, 18 | }, 19 | es: { 20 | url: process.env.ES_URL, 21 | }, 22 | redis: { 23 | port: process.env.REDIS_PORT, 24 | host: process.env.REDIS_HOST, 25 | password: process.env.REDIS_PASSWORD, 26 | db: process.env.REDIS_DATEBASE, 27 | }, 28 | jwt: { 29 | secret: process.env.JWT_SECRET || '123456', 30 | expiresIn: '24h', 31 | }, 32 | swagger: { 33 | enable: process.env.SWAGGER_ENABLE === 'true', 34 | path: process.env.SWAGGER_PATH, 35 | title: process.env.SWAGGER_TITLE, 36 | }, 37 | email: { 38 | host: process.env.EMAIL_HOST, 39 | port: process.env.EMAIL_PORT, 40 | user: process.env.EMAIL_USER, 41 | pass: process.env.EMAIL_PASS, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/entities/admin/sys-config.entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'sys_config' }) 6 | export default class SysConfig extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ type: 'varchar', length: 50, unique: true }) 12 | @ApiProperty() 13 | key: string; 14 | 15 | @Column({ type: 'varchar', length: 50 }) 16 | @ApiProperty() 17 | name: string; 18 | 19 | @Column({ type: 'varchar', nullable: true }) 20 | @ApiProperty() 21 | value: string; 22 | 23 | @Column({ type: 'varchar', nullable: true }) 24 | @ApiProperty() 25 | remark: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/entities/admin/sys-login-log.entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'sys_login_log' }) 6 | export default class SysLoginLog extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ nullable: true, name: 'user_id' }) 12 | @ApiProperty() 13 | userId: number; 14 | 15 | @Column({ nullable: true }) 16 | @ApiProperty() 17 | ip: string; 18 | 19 | @Column({ nullable: true }) 20 | @ApiProperty() 21 | address: string; 22 | 23 | @Column({ type: 'datetime', nullable: true }) 24 | @ApiProperty() 25 | time: Date; 26 | 27 | @Column({ length: 500, nullable: true }) 28 | @ApiProperty() 29 | ua: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/entities/admin/sys-menu.entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'sys_menu' }) 6 | export default class SysMenu extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ name: 'parent', nullable: true }) 12 | @ApiProperty() 13 | parent: number; 14 | 15 | @Column() 16 | @ApiProperty() 17 | name: string; 18 | 19 | @Column({ nullable: true }) 20 | @ApiProperty() 21 | path: string; 22 | 23 | @Column({ nullable: true }) 24 | @ApiProperty() 25 | permission: string; 26 | 27 | @Column({ type: 'tinyint', default: 0 }) 28 | @ApiProperty() 29 | type: number; 30 | 31 | @Column({ nullable: true, default: '' }) 32 | @ApiProperty() 33 | icon: string; 34 | 35 | @Column({ name: 'order_no', type: 'int', nullable: true, default: 0 }) 36 | @ApiProperty() 37 | orderNo: number; 38 | 39 | @Column({ name: 'component', nullable: true }) 40 | @ApiProperty() 41 | component: string; 42 | 43 | @Column({ type: 'tinyint', default: 0 }) 44 | @ApiProperty() 45 | external: number; 46 | 47 | @Column({ type: 'tinyint', default: 1 }) 48 | @ApiProperty() 49 | keepalive: number; 50 | 51 | @Column({ type: 'tinyint', default: 1 }) 52 | @ApiProperty() 53 | show: number; 54 | 55 | @Column({ type: 'tinyint', default: 1 }) 56 | @ApiProperty() 57 | status: number; 58 | } 59 | -------------------------------------------------------------------------------- /src/entities/admin/sys-role-menu.entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'sys_role_menu' }) 6 | export default class SysRoleMenu extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ name: 'role_id' }) 12 | @ApiProperty() 13 | roleId: number; 14 | 15 | @Column({ name: 'menu_id' }) 16 | @ApiProperty() 17 | menuId: number; 18 | } 19 | -------------------------------------------------------------------------------- /src/entities/admin/sys-role.entity.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'sys_role' }) 6 | export default class SysRole extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ length: 50, unique: true }) 12 | @ApiProperty() 13 | name: string; 14 | 15 | @Column({ unique: true }) 16 | @ApiProperty() 17 | value: string; 18 | 19 | @Column({ nullable: true }) 20 | @ApiProperty() 21 | remark: string; 22 | 23 | @Column({ type: 'tinyint', nullable: true, default: 1 }) 24 | @ApiProperty() 25 | status: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/entities/admin/sys-task-log.entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'sys_task_log' }) 6 | export default class SysTaskLog extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ name: 'task_id' }) 12 | @ApiProperty() 13 | taskId: number; 14 | 15 | @Column({ type: 'tinyint', default: 0 }) 16 | @ApiProperty() 17 | status: number; 18 | 19 | @Column({ type: 'text', nullable: true }) 20 | @ApiProperty() 21 | detail: string; 22 | 23 | @Column({ type: 'int', nullable: true, name: 'consume_time', default: 0 }) 24 | @ApiProperty() 25 | consumeTime: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/entities/admin/sys-task.entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'sys_task' }) 6 | export default class SysTask extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ type: 'varchar', length: 50, unique: true }) 12 | @ApiProperty() 13 | name: string; 14 | 15 | @Column() 16 | @ApiProperty() 17 | service: string; 18 | 19 | @Column({ type: 'tinyint', default: 0 }) 20 | @ApiProperty() 21 | type: number; 22 | 23 | @Column({ type: 'tinyint', default: 1 }) 24 | @ApiProperty() 25 | status: number; 26 | 27 | @Column({ name: 'start_time', type: 'datetime', nullable: true }) 28 | @ApiProperty() 29 | startTime: Date; 30 | 31 | @Column({ name: 'end_time', type: 'datetime', nullable: true }) 32 | @ApiProperty() 33 | endTime: Date; 34 | 35 | @Column({ type: 'int', nullable: true, default: 0 }) 36 | @ApiProperty() 37 | limit: number; 38 | 39 | @Column({ nullable: true }) 40 | @ApiProperty() 41 | cron: string; 42 | 43 | @Column({ type: 'int', nullable: true }) 44 | @ApiProperty() 45 | every: number; 46 | 47 | @Column({ type: 'text', nullable: true }) 48 | @ApiProperty() 49 | data: string; 50 | 51 | @Column({ name: 'job_opts', type: 'text', nullable: true }) 52 | @ApiProperty() 53 | jobOpts: string; 54 | 55 | @Column({ nullable: true }) 56 | @ApiProperty() 57 | remark: string; 58 | } 59 | -------------------------------------------------------------------------------- /src/entities/admin/sys-user-role.entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'sys_user_role' }) 6 | export default class SysUserRole extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ name: 'user_id' }) 12 | @ApiProperty() 13 | userId: number; 14 | 15 | @Column({ name: 'role_id' }) 16 | @ApiProperty() 17 | roleId: number; 18 | } 19 | -------------------------------------------------------------------------------- /src/entities/admin/sys-user.entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'sys_user' }) 6 | export default class SysUser extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ unique: true }) 12 | @ApiProperty() 13 | username: string; 14 | 15 | @Column() 16 | @ApiProperty() 17 | password: string; 18 | 19 | @Column({ length: 32 }) 20 | @ApiProperty() 21 | psalt: string; 22 | 23 | @Column({ name: 'nick_name', nullable: true }) 24 | @ApiProperty() 25 | nickName: string; 26 | 27 | @Column({ name: 'avatar', nullable: true }) 28 | @ApiProperty() 29 | avatar: string; 30 | 31 | @Column({ nullable: true }) 32 | @ApiProperty() 33 | qq: string; 34 | 35 | @Column({ nullable: true }) 36 | @ApiProperty() 37 | email: string; 38 | 39 | @Column({ nullable: true }) 40 | @ApiProperty() 41 | phone: string; 42 | 43 | @Column({ nullable: true }) 44 | @ApiProperty() 45 | remark: string; 46 | 47 | @Column({ type: 'tinyint', nullable: true, default: 1 }) 48 | @ApiProperty() 49 | status: number; 50 | } 51 | -------------------------------------------------------------------------------- /src/entities/admin/tool-storage.entity.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { BaseEntity } from '../base.entity'; 4 | 5 | @Entity({ name: 'tool-storage' }) 6 | export default class ToolStorage extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | @ApiProperty() 9 | id: number; 10 | 11 | @Column({ type: 'varchar', length: 200, comment: '文件名' }) 12 | @ApiProperty() 13 | name: string; 14 | 15 | @Column({ type: 'varchar', length: 200, nullable: true, comment: '真实文件名' }) 16 | @ApiProperty() 17 | fileName: string; 18 | 19 | @Column({ name: 'ext_name', type: 'varchar', nullable: true }) 20 | @ApiProperty() 21 | extName: string; 22 | 23 | @Column({ type: 'varchar' }) 24 | @ApiProperty() 25 | path: string; 26 | 27 | @Column({ type: 'varchar', nullable: true }) 28 | @ApiProperty() 29 | type: string; 30 | 31 | @Column({ type: 'varchar', nullable: true }) 32 | @ApiProperty() 33 | size: string; 34 | 35 | @Column({ nullable: true, name: 'user_id' }) 36 | @ApiProperty() 37 | userId: number; 38 | } 39 | -------------------------------------------------------------------------------- /src/entities/base.entity.ts: -------------------------------------------------------------------------------- 1 | import { CreateDateColumn, UpdateDateColumn } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export abstract class BaseEntity { 5 | @CreateDateColumn({ type: 'timestamp', name: 'created_at' }) 6 | @ApiProperty() 7 | createdAt: Date; 8 | 9 | @UpdateDateColumn({ type: 'timestamp', name: 'updated_at' }) 10 | @ApiProperty() 11 | updatedAt: Date; 12 | } 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import dotenv from 'dotenv'; 3 | dotenv.config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV}`) }); 4 | 5 | import { NestFactory, Reflector } from '@nestjs/core'; 6 | import { 7 | HttpStatus, 8 | Logger, 9 | UnprocessableEntityException, 10 | ValidationError, 11 | ValidationPipe, 12 | } from '@nestjs/common'; 13 | import { AppModule } from './app.module'; 14 | import { NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify'; 15 | import { IoAdapter } from '@nestjs/platform-socket.io'; 16 | import { setupSwagger } from './setup-swagger'; 17 | import { LoggerService } from './shared/logger/logger.service'; 18 | import { ApiExceptionFilter } from './common/filter/api-exception.filter'; 19 | import { ApiTransformInterceptor } from './common/interceptors/api-transform.interceptor'; 20 | import { TimeoutInterceptor } from './common/interceptors/timeout.interceptor'; 21 | 22 | const PORT = process.env.PORT; 23 | const WS_PORT = process.env.WS_PORT; 24 | const PREFIX = process.env.PREFIX; 25 | async function bootstrap() { 26 | const app = await NestFactory.create(AppModule, new FastifyAdapter(), { 27 | // bufferLogs: true, 28 | }); 29 | app.useLogger(app.get(LoggerService)); 30 | app.enableCors({ origin: '*', credentials: true }); 31 | app.useStaticAssets({ root: path.join(__dirname, '..', 'public') }); 32 | // https://github.com/fastify/fastify-multipart/ 33 | await app.register(require('@fastify/multipart'), { 34 | limits: { 35 | fileSize: 1000000, 36 | files: 1, 37 | }, 38 | }); 39 | 40 | // 全局请求添加prefix 41 | app.setGlobalPrefix(PREFIX); 42 | // 处理异常请求 43 | app.useGlobalFilters(new ApiExceptionFilter(app.get(LoggerService))); 44 | // 请求超时处理 45 | app.useGlobalInterceptors(new TimeoutInterceptor(30000)); 46 | // 返回数据处理 47 | app.useGlobalInterceptors(new ApiTransformInterceptor(new Reflector())); 48 | // websocket 49 | app.useWebSocketAdapter(new IoAdapter()); 50 | // 使用全局管道验证数据 51 | app.useGlobalPipes( 52 | new ValidationPipe({ 53 | transform: true, 54 | whitelist: true, 55 | // forbidNonWhitelisted: true, // 禁止 无装饰器验证的数据通过 56 | errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, 57 | exceptionFactory: (errors: ValidationError[]) => { 58 | return new UnprocessableEntityException(Object.values(errors[0].constraints)[0]); 59 | }, 60 | }), 61 | ); 62 | 63 | setupSwagger(app); 64 | 65 | await app.listen(PORT, '0.0.0.0', () => { 66 | Logger.log(process.env.NODE_ENV, 'Server started,current env'); 67 | Logger.log(`api服务已启动,请访问: http://127.0.0.1:${PORT}/${PREFIX}`); 68 | Logger.log(`ws服务已启动,请访问: http://127.0.0.1:${WS_PORT}/${process.env.WS_PATH}`); 69 | Logger.log(`API文档已生成,请访问: http://127.0.0.1:${PORT}/${process.env.SWAGGER_PARH}/`); 70 | }); 71 | } 72 | bootstrap(); 73 | -------------------------------------------------------------------------------- /src/mission/README.md: -------------------------------------------------------------------------------- 1 | ### 任务注册 2 | 3 | 在jobs下定义任务,并在`mission.module.ts`中的providers中注册即可。 4 | 5 | ### 添加任务 6 | 7 | 在后台页面中的定时任务页面中进行添加,主要识别为**服务路径**的定义:`Job类名.方法名`,填写任务参数则会在调用方法时自动传递该参数,参数会被`JSON.parse` -------------------------------------------------------------------------------- /src/mission/jobs/email.job.ts: -------------------------------------------------------------------------------- 1 | import { EmailService } from '@/shared/services/email.service'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { LoggerService } from 'src/shared/logger/logger.service'; 4 | import { Mission } from '../mission.decorator'; 5 | 6 | /** 7 | * Api接口请求类型任务 8 | */ 9 | @Injectable() 10 | @Mission() 11 | export class EmailJob { 12 | constructor( 13 | private readonly emailService: EmailService, 14 | private readonly logger: LoggerService, 15 | ) {} 16 | 17 | async send(config: any): Promise { 18 | if (config) { 19 | const { to, subject, content } = config; 20 | const result = await this.emailService.sendMail(to, subject, content); 21 | this.logger.log(result, EmailJob.name); 22 | } else { 23 | throw new Error('Email send job param is empty'); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/mission/jobs/http-request.job.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { LoggerService } from 'src/shared/logger/logger.service'; 4 | import { Mission } from '../mission.decorator'; 5 | 6 | /** 7 | * Api接口请求类型任务 8 | */ 9 | @Injectable() 10 | @Mission() 11 | export class HttpRequestJob { 12 | constructor(private readonly httpService: HttpService, private readonly logger: LoggerService) {} 13 | 14 | /** 15 | * 发起请求 16 | * @param config {AxiosRequestConfig} 17 | */ 18 | async handle(config: unknown): Promise { 19 | if (config) { 20 | const result = await this.httpService.request(config); 21 | this.logger.log(result, HttpRequestJob.name); 22 | } else { 23 | throw new Error('Http request job param is empty'); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/mission/jobs/sys-log-clear.job.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { SysLogService } from 'src/modules/admin/system/log/log.service'; 3 | import { Mission } from '../mission.decorator'; 4 | 5 | /** 6 | * 管理后台日志清理任务 7 | */ 8 | @Injectable() 9 | @Mission() 10 | export class SysLogClearJob { 11 | constructor(private sysLogService: SysLogService) {} 12 | 13 | async clearLoginLog(): Promise { 14 | await this.sysLogService.clearLoginLog(); 15 | } 16 | 17 | async clearTaskLog(): Promise { 18 | await this.sysLogService.clearTaskLog(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/mission/mission.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { MISSION_KEY_METADATA } from '../common/contants/decorator.contants'; 3 | 4 | /** 5 | * 定时任务标记,没有该任务标记的任务不会被执行,保证全局获取下的模块被安全执行 6 | */ 7 | export const Mission = () => SetMetadata(MISSION_KEY_METADATA, true); 8 | -------------------------------------------------------------------------------- /src/mission/mission.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, ExistingProvider, Module } from '@nestjs/common'; 2 | import { AdminModule } from 'src/modules/admin/admin.module'; 3 | import { SysLogService } from 'src/modules/admin/system/log/log.service'; 4 | import { EmailJob } from './jobs/email.job'; 5 | import { HttpRequestJob } from './jobs/http-request.job'; 6 | import { SysLogClearJob } from './jobs/sys-log-clear.job'; 7 | 8 | const providers = [SysLogClearJob, HttpRequestJob, EmailJob]; 9 | 10 | /** 11 | * auto create alias 12 | * { 13 | * provide: 'SysLogClearMissionService', 14 | * useExisting: SysLogClearMissionService, 15 | * } 16 | */ 17 | function createAliasProviders(): ExistingProvider[] { 18 | const aliasProviders: ExistingProvider[] = []; 19 | for (const p of providers) { 20 | aliasProviders.push({ 21 | provide: p.name, 22 | useExisting: p, 23 | }); 24 | } 25 | return aliasProviders; 26 | } 27 | 28 | /** 29 | * 所有需要执行的定时任务都需要在这里注册 30 | */ 31 | @Module({}) 32 | export class MissionModule { 33 | static forRoot(): DynamicModule { 34 | // 使用Alias定义别名,使得可以通过字符串类型获取定义的Service,否则无法获取 35 | const aliasProviders = createAliasProviders(); 36 | return { 37 | global: true, 38 | module: MissionModule, 39 | imports: [AdminModule], 40 | providers: [...providers, ...aliasProviders, SysLogService], 41 | exports: aliasProviders, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/admin/account/account.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; 3 | import { ADMIN_PREFIX } from '@/modules/admin/admin.constants'; 4 | import { IAdminUser } from '../admin.interface'; 5 | import { AdminUser } from '../core/decorators/admin-user.decorator'; 6 | import { PermissionOptional } from '../core/decorators/permission-optional.decorator'; 7 | import { MenuInfo, PermInfo } from '../login/login.class'; 8 | import { LoginService } from '../login/login.service'; 9 | import { AccountInfo } from '../system/user/user.class'; 10 | import { UpdatePasswordDto, UpdateUserInfoDto, UserExistDto } from '../system/user/user.dto'; 11 | import { SysUserService } from '../system/user/user.service'; 12 | 13 | @ApiTags('账户模块') 14 | @ApiSecurity(ADMIN_PREFIX) 15 | @Controller() 16 | export class AccountController { 17 | constructor(private userService: SysUserService, private loginService: LoginService) {} 18 | 19 | @ApiOperation({ summary: '获取账户资料' }) 20 | @ApiOkResponse({ type: AccountInfo }) 21 | @PermissionOptional() 22 | @Get('info') 23 | async info(@AdminUser() user: IAdminUser): Promise { 24 | return await this.userService.getAccountInfo(user.uid); 25 | } 26 | 27 | @ApiOperation({ summary: '更改账户资料' }) 28 | @PermissionOptional() 29 | @Post('update') 30 | async update(@Body() dto: UpdateUserInfoDto, @AdminUser() user: IAdminUser): Promise { 31 | await this.userService.updateAccountInfo(user.uid, dto); 32 | } 33 | 34 | @ApiOperation({ summary: '更改账户密码' }) 35 | @PermissionOptional() 36 | @Post('password') 37 | async password(@Body() dto: UpdatePasswordDto, @AdminUser() user: IAdminUser): Promise { 38 | await this.userService.updatePassword(user.uid, dto); 39 | } 40 | 41 | @ApiOperation({ summary: '账户登出' }) 42 | @PermissionOptional() 43 | @Get('logout') 44 | async logout(@AdminUser() user: IAdminUser): Promise { 45 | await this.loginService.clearLoginStatus(user.uid); 46 | } 47 | 48 | @ApiOperation({ summary: '获取菜单列表' }) 49 | @ApiOkResponse({ type: MenuInfo }) 50 | @PermissionOptional() 51 | @Get('menu') 52 | async menu(@AdminUser() user: IAdminUser): Promise { 53 | return await this.loginService.getMenu(user.uid); 54 | } 55 | 56 | @ApiOperation({ summary: '获取权限列表' }) 57 | @ApiOkResponse({ type: PermInfo }) 58 | @PermissionOptional() 59 | @Get('perm') 60 | async perm(@AdminUser() user: IAdminUser): Promise { 61 | return await this.loginService.getPerm(user.uid); 62 | } 63 | 64 | @ApiOperation({ summary: '判断用户名是否存在' }) 65 | @PermissionOptional() 66 | @Get('exist') 67 | async exist(@Query() dto: UserExistDto) { 68 | return this.userService.exist(dto.username); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/admin/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LoginModule } from '../login/login.module'; 3 | import { SystemModule } from '../system/system.module'; 4 | import { AccountController } from './account.controller'; 5 | 6 | @Module({ 7 | imports: [SystemModule, LoginModule], 8 | controllers: [AccountController], 9 | }) 10 | export class AccountModule {} 11 | -------------------------------------------------------------------------------- /src/modules/admin/admin.constants.ts: -------------------------------------------------------------------------------- 1 | export const ADMIN_USER = 'adminUser'; 2 | export const AUTHORIZE_KEY_METADATA = 'admin_module:authorize'; 3 | export const API_TOKEN_KEY_METADATA = 'admin_module:api_token'; 4 | export const PERMISSION_OPTIONAL_KEY_METADATA = 'admin_module:permission_optional'; 5 | export const LOG_DISABLED_KEY_METADATA = 'admin_module:log_disabled'; 6 | 7 | export const ROOT_ROLE_ID = 'admin_module:root_role_id'; 8 | export const QINIU_CONFIG = 'admin_module:qiniu_config'; 9 | 10 | export const SYS_TASK_QUEUE_NAME = 'admin_module:sys-task'; 11 | export const SYS_TASK_QUEUE_PREFIX = 'admin:sys:task'; 12 | 13 | export const FORBIDDEN_OP_MENU_ID_INDEX = 43; 14 | 15 | export const ADMIN_PREFIX = ''; // 原为admin 16 | -------------------------------------------------------------------------------- /src/modules/admin/admin.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IAdminUser { 2 | uid: number; 3 | pv: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_GUARD, RouterModule } from '@nestjs/core'; 3 | import { AccountModule } from './account/account.module'; 4 | import { ADMIN_PREFIX } from './admin.constants'; 5 | import { AuthGuard } from './core/guards/auth.guard'; 6 | import { LoginModule } from './login/login.module'; 7 | import { SystemModule } from './system/system.module'; 8 | import { ToolsModule } from './tools/tools.module'; 9 | import { UploadModule } from './upload/upload.module'; 10 | 11 | /** 12 | * Admin模块,所有API都需要加入/admin前缀 13 | */ 14 | @Module({ 15 | imports: [ 16 | // register prefix 17 | RouterModule.register([ 18 | { 19 | path: ADMIN_PREFIX, 20 | children: [ 21 | { path: 'account', module: AccountModule }, 22 | { path: 'sys', module: SystemModule }, 23 | { path: 'tools', module: ToolsModule }, 24 | { path: '', module: UploadModule }, 25 | ], 26 | }, 27 | // like this url /captcha/img /login 28 | { 29 | path: ADMIN_PREFIX, 30 | module: LoginModule, 31 | }, 32 | ]), 33 | // component module 34 | LoginModule, 35 | AccountModule, 36 | SystemModule, 37 | ToolsModule, 38 | UploadModule, 39 | ], 40 | providers: [ 41 | { 42 | provide: APP_GUARD, 43 | useClass: AuthGuard, 44 | }, 45 | ], 46 | exports: [SystemModule], 47 | }) 48 | export class AdminModule {} 49 | -------------------------------------------------------------------------------- /src/modules/admin/core/decorators/admin-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { ADMIN_USER } from '../../admin.constants'; 3 | 4 | export const AdminUser = createParamDecorator((data: string, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | // auth guard will mount this 7 | const user = request[ADMIN_USER]; 8 | 9 | return data ? user?.[data] : user; 10 | }); 11 | -------------------------------------------------------------------------------- /src/modules/admin/core/decorators/authorize.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { AUTHORIZE_KEY_METADATA } from '../../admin.constants'; 3 | 4 | /** 5 | * 开放授权Api,使用该注解则无需校验Token及权限 6 | */ 7 | export const Authorize = () => SetMetadata(AUTHORIZE_KEY_METADATA, true); 8 | -------------------------------------------------------------------------------- /src/modules/admin/core/decorators/log-disabled.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { LOG_DISABLED_KEY_METADATA } from '../../admin.constants'; 3 | 4 | /** 5 | * 日志记录禁用 6 | */ 7 | export const LogDisabled = () => SetMetadata(LOG_DISABLED_KEY_METADATA, true); 8 | -------------------------------------------------------------------------------- /src/modules/admin/core/decorators/permission-optional.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { PERMISSION_OPTIONAL_KEY_METADATA } from '../../admin.constants'; 3 | 4 | /** 5 | * 使用该注解可开放当前Api权限,无需权限访问,但是仍然需要校验身份Token 6 | */ 7 | export const PermissionOptional = () => SetMetadata(PERMISSION_OPTIONAL_KEY_METADATA, true); 8 | -------------------------------------------------------------------------------- /src/modules/admin/core/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { FastifyRequest } from 'fastify'; 4 | import { isEmpty } from 'lodash'; 5 | import { JwtService } from '@nestjs/jwt'; 6 | import { ApiException } from '@/common/exceptions/api.exception'; 7 | import { 8 | ADMIN_PREFIX, 9 | ADMIN_USER, 10 | PERMISSION_OPTIONAL_KEY_METADATA, 11 | AUTHORIZE_KEY_METADATA, 12 | API_TOKEN_KEY_METADATA, 13 | } from '@/modules/admin/admin.constants'; 14 | import { SYS_API_TOKEN } from '@/common/contants/param-config.contants'; 15 | import { LoginService } from '@/modules/admin/login/login.service'; 16 | import { SysParamConfigService } from '@/modules/admin/system/param-config/param-config.service'; 17 | 18 | /** 19 | * admin perm check guard 20 | */ 21 | @Injectable() 22 | export class AuthGuard implements CanActivate { 23 | constructor( 24 | private reflector: Reflector, 25 | private jwtService: JwtService, 26 | private loginService: LoginService, 27 | private paramConfigService: SysParamConfigService, 28 | ) {} 29 | 30 | async canActivate(context: ExecutionContext): Promise { 31 | // 检测是否是开放类型的,例如获取验证码类型的接口不需要校验,可以加入@Authorize可自动放过 32 | const authorize = this.reflector.get(AUTHORIZE_KEY_METADATA, context.getHandler()); 33 | if (authorize) { 34 | return true; 35 | } 36 | const request = context.switchToHttp().getRequest(); 37 | const url = request.url; 38 | const path = url.split('?')[0]; 39 | const token = request.headers['authorization'] as string; 40 | if (isEmpty(token)) { 41 | throw new ApiException(11001); 42 | } 43 | 44 | // 检查是否开启API TOKEN授权,当开启时,只有带API TOKEN可以正常访问 45 | const apiToken = this.reflector.get(API_TOKEN_KEY_METADATA, context.getHandler()); 46 | if (apiToken) { 47 | const result = await this.paramConfigService.findValueByKey(SYS_API_TOKEN); 48 | if (token === result) { 49 | return true; 50 | } else { 51 | throw new ApiException(11003); 52 | } 53 | } 54 | 55 | try { 56 | // 挂载对象到当前请求上 57 | request[ADMIN_USER] = this.jwtService.verify(token); 58 | } catch (e) { 59 | // 无法通过token校验 60 | throw new ApiException(11001); 61 | } 62 | if (isEmpty(request[ADMIN_USER])) { 63 | throw new ApiException(11001); 64 | } 65 | const pv = await this.loginService.getRedisPasswordVersionById(request[ADMIN_USER].uid); 66 | if (pv !== `${request[ADMIN_USER].pv}`) { 67 | // 密码版本不一致,登录期间已更改过密码 68 | throw new ApiException(11002); 69 | } 70 | const redisToken = await this.loginService.getRedisTokenById(request[ADMIN_USER].uid); 71 | if (token !== redisToken) { 72 | // 与redis保存不一致 73 | throw new ApiException(11002); 74 | } 75 | // 注册该注解,Api则放行检测 76 | const notNeedPerm = this.reflector.get( 77 | PERMISSION_OPTIONAL_KEY_METADATA, 78 | context.getHandler(), 79 | ); 80 | // Token校验身份通过,判断是否需要权限的url,不需要权限则pass 81 | if (notNeedPerm) { 82 | return true; 83 | } 84 | const perms: string = await this.loginService.getRedisPermsById(request[ADMIN_USER].uid); 85 | // 安全判空 86 | if (isEmpty(perms)) { 87 | throw new ApiException(11001); 88 | } 89 | // 将sys:admin:user等转换成sys/admin/user 90 | const permArray: string[] = (JSON.parse(perms) as string[]).map((e) => { 91 | return e.replace(/:/g, '/'); 92 | }); 93 | // 遍历权限是否包含该url,不包含则无访问权限 94 | if (!permArray.includes(path.replace(`/${process.env.PREFIX}/`, ''))) { 95 | throw new ApiException(11003); 96 | } 97 | // pass 98 | return true; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/modules/admin/core/permission/index.ts: -------------------------------------------------------------------------------- 1 | import { isExternal } from '@/utils/is'; 2 | 3 | function createRoute(menu, isRoot) { 4 | if (isExternal(menu.path)) { 5 | return { 6 | id: menu.id, 7 | path: menu.path, 8 | component: 'IFrame', 9 | name: menu.name, 10 | meta: { title: menu.name, icon: menu.icon }, 11 | }; 12 | } 13 | 14 | // 目录 15 | if (menu.type === 0) { 16 | return { 17 | id: menu.id, 18 | path: menu.path, 19 | component: menu.component, 20 | show: true, 21 | name: menu.name, 22 | meta: { title: menu.name, icon: menu.icon }, 23 | }; 24 | } 25 | 26 | return { 27 | id: menu.id, 28 | path: menu.path, 29 | name: menu.name, 30 | component: menu.component, 31 | meta: { 32 | title: menu.name, 33 | icon: menu.icon, 34 | ...(!menu.show ? { hideMenu: !menu.show } : null), 35 | ignoreKeepAlive: !menu.keepalive, 36 | }, 37 | }; 38 | } 39 | 40 | function filterAsyncRoutes(menus, parentRoute) { 41 | const res = []; 42 | 43 | menus.forEach((menu) => { 44 | if (menu.type === 2 || !menu.status) { 45 | // 如果是权限或禁用直接跳过 46 | return; 47 | } 48 | // 根级别菜单渲染 49 | let realRoute; 50 | if (!parentRoute && !menu.parent && menu.type === 1) { 51 | // 根菜单 52 | realRoute = createRoute(menu, true); 53 | } else if (!parentRoute && !menu.parent && menu.type === 0) { 54 | // 目录 55 | const childRoutes = filterAsyncRoutes(menus, menu); 56 | realRoute = createRoute(menu, true); 57 | if (childRoutes && childRoutes.length > 0) { 58 | realRoute.redirect = childRoutes[0].path; 59 | realRoute.children = childRoutes; 60 | } 61 | } else if (parentRoute && parentRoute.id === menu.parent && menu.type === 1) { 62 | // 子菜单 63 | realRoute = createRoute(menu, false); 64 | } else if (parentRoute && parentRoute.id === menu.parent && menu.type === 0) { 65 | // 如果还是目录,继续递归 66 | const childRoute = filterAsyncRoutes(menus, menu); 67 | realRoute = createRoute(menu, false); 68 | if (childRoute && childRoute.length > 0) { 69 | realRoute.redirect = childRoute[0].path; 70 | realRoute.children = childRoute; 71 | } 72 | } 73 | // add curent route 74 | if (realRoute) { 75 | res.push(realRoute); 76 | } 77 | }); 78 | return res; 79 | } 80 | 81 | export function generatorRouters(menu) { 82 | return filterAsyncRoutes(menu, null); 83 | } 84 | 85 | // 获取所有菜单以及权限 86 | function filterMenuToTable(menus, parentMenu) { 87 | const res = []; 88 | menus.forEach((menu) => { 89 | // 根级别菜单渲染 90 | let realMenu; 91 | if (!parentMenu && !menu.parent && menu.type === 1) { 92 | // 根菜单,查找该跟菜单下子菜单,因为可能会包含权限 93 | const childMenu = filterMenuToTable(menus, menu); 94 | realMenu = { ...menu }; 95 | realMenu.children = childMenu; 96 | } else if (!parentMenu && !menu.parent && menu.type === 0) { 97 | // 根目录 98 | const childMenu = filterMenuToTable(menus, menu); 99 | realMenu = { ...menu }; 100 | realMenu.children = childMenu; 101 | } else if (parentMenu && parentMenu.id === menu.parent && menu.type === 1) { 102 | // 子菜单下继续找是否有子菜单 103 | const childMenu = filterMenuToTable(menus, menu); 104 | realMenu = { ...menu }; 105 | realMenu.children = childMenu; 106 | } else if (parentMenu && parentMenu.id === menu.parent && menu.type === 0) { 107 | // 如果还是目录,继续递归 108 | const childMenu = filterMenuToTable(menus, menu); 109 | realMenu = { ...menu }; 110 | realMenu.children = childMenu; 111 | } else if (parentMenu && parentMenu.id === menu.parent && menu.type === 2) { 112 | realMenu = { ...menu }; 113 | } 114 | // add curent route 115 | if (realMenu) { 116 | realMenu.pid = menu.id; 117 | res.push(realMenu); 118 | } 119 | }); 120 | return res; 121 | } 122 | 123 | export function generatorMenu(menu) { 124 | return filterMenuToTable(menu, null); 125 | } 126 | 127 | // 仅获取所有菜单不包括权限 128 | function filterMenuToTree(menus, parentMenu) { 129 | const res = []; 130 | menus.forEach((menu) => { 131 | // 根级别菜单渲染 132 | let realMenu; 133 | if (!parentMenu && !menu.parent && menu.type === 1) { 134 | // 根菜单,查找该跟菜单下子菜单,因为可能会包含权限 135 | const childMenu = filterMenuToTree(menus, menu); 136 | realMenu = { ...menu }; 137 | realMenu.children = childMenu; 138 | } else if (!parentMenu && !menu.parent && menu.type === 0) { 139 | // 根目录 140 | const childMenu = filterMenuToTree(menus, menu); 141 | realMenu = { ...menu }; 142 | realMenu.children = childMenu; 143 | } else if (parentMenu && parentMenu.id === menu.parent && menu.type === 1) { 144 | // 子菜单下继续找是否有子菜单 145 | const childMenu = filterMenuToTree(menus, menu); 146 | realMenu = { ...menu }; 147 | realMenu.children = childMenu; 148 | } else if (parentMenu && parentMenu.id === menu.parent && menu.type === 0) { 149 | // 如果还是目录,继续递归 150 | const childMenu = filterMenuToTree(menus, menu); 151 | realMenu = { ...menu }; 152 | realMenu.children = childMenu; 153 | } 154 | // add curent route 155 | if (realMenu) { 156 | realMenu.pid = menu.id; 157 | res.push(realMenu); 158 | } 159 | }); 160 | return res; 161 | } 162 | -------------------------------------------------------------------------------- /src/modules/admin/core/provider/root-role-id.provider.ts: -------------------------------------------------------------------------------- 1 | import { FactoryProvider } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { ROOT_ROLE_ID } from '@/modules/admin/admin.constants'; 4 | 5 | /** 6 | * 提供使用 @Inject(ROOT_ROLE_ID) 直接获取RootRoleId 7 | */ 8 | export function rootRoleIdProvider(): FactoryProvider { 9 | return { 10 | provide: ROOT_ROLE_ID, 11 | useFactory: (configService: ConfigService) => { 12 | return configService.get('rootRoleId', 1); 13 | }, 14 | inject: [ConfigService], 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/admin/login/login.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import SysMenu from '@/entities/admin/sys-menu.entity'; 3 | 4 | export class ImageCaptcha { 5 | @ApiProperty({ 6 | description: 'base64格式的svg图片', 7 | }) 8 | img: string; 9 | 10 | @ApiProperty({ 11 | description: '验证码对应的唯一ID', 12 | }) 13 | id: string; 14 | } 15 | 16 | export class LoginToken { 17 | @ApiProperty({ description: 'JWT身份Token' }) 18 | token: string; 19 | } 20 | 21 | export class MenuInfo { 22 | @ApiProperty({ description: '菜单列表', type: [SysMenu] }) 23 | menus: SysMenu[]; 24 | } 25 | 26 | export class PermInfo { 27 | @ApiProperty({ description: '权限列表', type: [String] }) 28 | perms: string[]; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/admin/login/login.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Headers, Post, Query, Req } from '@nestjs/common'; 2 | import { FastifyRequest } from 'fastify'; 3 | import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | import { Authorize } from '../core/decorators/authorize.decorator'; 5 | import { ImageCaptchaDto, LoginInfoDto, RegisterInfoDto, sendCodeDto } from './login.dto'; 6 | import { ImageCaptcha, LoginToken } from './login.class'; 7 | import { LoginService } from './login.service'; 8 | import { LogDisabled } from '../core/decorators/log-disabled.decorator'; 9 | import { UtilService } from '@/shared/services/util.service'; 10 | import { ResOp } from '@/common/class/res.class'; 11 | import { Keep } from '@/common/decorators/keep.decorator'; 12 | 13 | @ApiTags('登录模块') 14 | @Controller() 15 | export class LoginController { 16 | constructor(private loginService: LoginService, private utils: UtilService) {} 17 | 18 | @ApiOperation({ summary: '获取登录图片验证码' }) 19 | @ApiOkResponse({ type: ImageCaptcha }) 20 | @Get('captcha/img') 21 | @Authorize() 22 | async captchaByImg(@Query() dto: ImageCaptchaDto): Promise { 23 | return await this.loginService.createImageCaptcha(dto); 24 | } 25 | 26 | @ApiOperation({ summary: '发送邮箱验证码' }) 27 | @Post('sendCode') 28 | @LogDisabled() 29 | @Authorize() 30 | @Keep() 31 | async sendCode(@Body() dto: sendCodeDto, @Req() req: FastifyRequest): Promise { 32 | // await this.loginService.checkImgCaptcha(dto.captchaId, dto.verifyCode); 33 | try { 34 | await this.loginService.sendCode(dto.email, this.utils.getReqIP(req)); 35 | return ResOp.success(); 36 | } catch (error) { 37 | console.log(error); 38 | return ResOp.error(500, error?.response); 39 | } 40 | } 41 | 42 | @ApiOperation({ summary: '登录' }) 43 | @ApiOkResponse({ type: LoginToken }) 44 | @Post('login') 45 | @LogDisabled() 46 | @Authorize() 47 | async login( 48 | @Body() dto: LoginInfoDto, 49 | @Req() req: FastifyRequest, 50 | @Headers('user-agent') ua: string, 51 | ): Promise { 52 | // await this.loginService.checkImgCaptcha(dto.captchaId, dto.verifyCode); 53 | const token = await this.loginService.getLoginSign( 54 | dto.username, 55 | dto.password, 56 | this.utils.getReqIP(req), 57 | ua, 58 | ); 59 | return { token }; 60 | } 61 | 62 | @ApiOperation({ summary: '注册' }) 63 | @Post('register') 64 | @LogDisabled() 65 | @Authorize() 66 | async register(@Body() dto: RegisterInfoDto): Promise { 67 | await this.loginService.checkCode(dto.email, dto.code); 68 | await this.loginService.register(dto); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/admin/login/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { 4 | IsEmail, 5 | isEmpty, 6 | IsInt, 7 | IsOptional, 8 | IsString, 9 | Length, 10 | Matches, 11 | MaxLength, 12 | MinLength, 13 | ValidateIf, 14 | } from 'class-validator'; 15 | 16 | export class ImageCaptchaDto { 17 | @ApiProperty({ 18 | required: false, 19 | default: 100, 20 | description: '验证码宽度', 21 | }) 22 | @Type(() => Number) 23 | @IsInt() 24 | @IsOptional() 25 | readonly width: number = 100; 26 | 27 | @ApiProperty({ 28 | required: false, 29 | default: 50, 30 | description: '验证码宽度', 31 | }) 32 | @Type(() => Number) 33 | @IsInt() 34 | @IsOptional() 35 | readonly height: number = 50; 36 | } 37 | 38 | export class LoginInfoDto { 39 | @ApiProperty({ description: '用户名' }) 40 | @IsString() 41 | @MinLength(1) 42 | username: string; 43 | 44 | @ApiProperty({ description: '密码' }) 45 | @IsString() 46 | @MinLength(4) 47 | password: string; 48 | 49 | // @ApiProperty({ description: '验证码标识' }) 50 | // @IsString() 51 | // captchaId: string; 52 | 53 | // @ApiProperty({ description: '用户输入的验证码' }) 54 | // @IsString() 55 | // @MinLength(4) 56 | // @MaxLength(4) 57 | // verifyCode: string; 58 | } 59 | 60 | export class RegisterInfoDto { 61 | @ApiProperty({ description: '账号' }) 62 | @IsString() 63 | @Matches(/^[a-z0-9A-Z]+$/) 64 | @MinLength(4) 65 | @MaxLength(20) 66 | username: string; 67 | 68 | @ApiProperty({ description: '密码' }) 69 | @IsString() 70 | @Matches(/^[a-z0-9A-Z`~!#%^&*=+\\|{};:'\\",<>/?]+$/) 71 | @MinLength(4) 72 | @MaxLength(16) 73 | password: string; 74 | 75 | // @ApiProperty({ required: false, description: '手机号' }) 76 | // @IsString() 77 | // @IsOptional() 78 | // phone: string; 79 | 80 | @ApiProperty({ required: false, description: '邮箱' }) 81 | @IsEmail() 82 | email: string; 83 | 84 | @ApiProperty({ required: false, description: '验证码' }) 85 | @IsString() 86 | @Length(4, 4) 87 | code: string; 88 | 89 | @ApiProperty({ required: false, description: 'QQ' }) 90 | @IsString() 91 | @Matches(/^[0-9]+$/) 92 | @MinLength(5) 93 | @MaxLength(11) 94 | @IsOptional() 95 | qq: string; 96 | } 97 | export class sendCodeDto { 98 | @ApiProperty({ description: '邮箱' }) 99 | @IsEmail({ message: '邮箱格式不正确' }) 100 | email: string; 101 | 102 | // @ApiProperty({ description: '验证码标识' }) 103 | // @IsString({ message: '请输入验证码标识' }) 104 | // captchaId: string; 105 | 106 | // @ApiProperty({ description: '用户输入的验证码' }) 107 | // @IsString({ message: '请输入验证码' }) 108 | // @MinLength(4) 109 | // @MaxLength(4) 110 | // verifyCode: string; 111 | } 112 | -------------------------------------------------------------------------------- /src/modules/admin/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SystemModule } from '../system/system.module'; 3 | import { LoginController } from './login.controller'; 4 | import { LoginService } from './login.service'; 5 | 6 | @Module({ 7 | imports: [SystemModule], 8 | controllers: [LoginController], 9 | providers: [LoginService], 10 | exports: [LoginService], 11 | }) 12 | export class LoginModule {} 13 | -------------------------------------------------------------------------------- /src/modules/admin/login/login.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as svgCaptcha from 'svg-captcha'; 3 | import { ImageCaptcha, MenuInfo, PermInfo } from './login.class'; 4 | import { isEmpty } from 'lodash'; 5 | import { ImageCaptchaDto, RegisterInfoDto } from './login.dto'; 6 | import { JwtService } from '@nestjs/jwt'; 7 | import { UtilService } from '@/shared/services/util.service'; 8 | import { SysMenuService } from '../system/menu/menu.service'; 9 | import { SysUserService } from '../system/user/user.service'; 10 | import { ApiException } from '@/common/exceptions/api.exception'; 11 | import { SysLogService } from '../system/log/log.service'; 12 | import { RedisService } from '@/shared/services/redis.service'; 13 | import { EmailService } from '@/shared/services/email.service'; 14 | import dayjs from 'dayjs'; 15 | 16 | @Injectable() 17 | export class LoginService { 18 | constructor( 19 | private redisService: RedisService, 20 | private menuService: SysMenuService, 21 | private userService: SysUserService, 22 | private logService: SysLogService, 23 | private emailService: EmailService, 24 | private util: UtilService, 25 | private jwtService: JwtService, 26 | ) {} 27 | 28 | /** 29 | * 创建验证码并缓存加入redis缓存 30 | * @param captcha 验证码长宽 31 | * @returns svg & id obj 32 | */ 33 | async createImageCaptcha(captcha: ImageCaptchaDto): Promise { 34 | const svg = svgCaptcha.create({ 35 | size: 4, 36 | color: true, 37 | noise: 4, 38 | width: isEmpty(captcha.width) ? 100 : captcha.width, 39 | height: isEmpty(captcha.height) ? 50 : captcha.height, 40 | charPreset: '1234567890', 41 | }); 42 | const result = { 43 | img: `data:image/svg+xml;base64,${Buffer.from(svg.data).toString('base64')}`, 44 | id: this.util.generateUUID(), 45 | }; 46 | // 5分钟过期时间 47 | await this.redisService 48 | .getRedis() 49 | .set(`admin:captcha:img:${result.id}`, svg.text, 'EX', 60 * 5); 50 | return result; 51 | } 52 | 53 | /** 54 | * 校验图片验证码 55 | */ 56 | async checkImgCaptcha(id: string, code: string): Promise { 57 | const result = await this.redisService.getRedis().get(`admin:captcha:img:${id}`); 58 | if (isEmpty(result) || code.toLowerCase() !== result.toLowerCase()) { 59 | throw new ApiException(10002); 60 | } 61 | // 校验成功后移除验证码 62 | await this.redisService.getRedis().del(`admin:captcha:img:${id}`); 63 | } 64 | 65 | /** 66 | * 获取登录JWT 67 | * 返回null则账号密码有误,不存在该用户 68 | */ 69 | async getLoginSign(username: string, password: string, ip: string, ua: string): Promise { 70 | const user = await this.userService.findUserByUserName(username); 71 | if (isEmpty(user)) { 72 | throw new ApiException(10003); 73 | } 74 | const comparePassword = this.util.md5(`${password}${user.psalt}`); 75 | if (user.password !== comparePassword) { 76 | throw new ApiException(10003); 77 | } 78 | const perms = await this.menuService.getPerms(user.id); 79 | 80 | // 系统管理员允许多点登录 81 | if (user.id === 1) { 82 | const oldToken = await this.getRedisTokenById(user.id); 83 | await this.logService.saveLoginLog(user.id, ip, ua); 84 | if (oldToken) return oldToken; 85 | } 86 | 87 | const jwtSign = this.jwtService.sign( 88 | { 89 | uid: parseInt(user.id.toString()), 90 | pv: 1, 91 | }, 92 | // { 93 | // expiresIn: '24h', 94 | // }, 95 | ); 96 | await this.redisService.getRedis().set(`admin:passwordVersion:${user.id}`, 1); 97 | // Token设置过期时间 24小时 98 | await this.redisService.getRedis().set(`admin:token:${user.id}`, jwtSign, 'EX', 60 * 60 * 24); 99 | await this.redisService.getRedis().set(`admin:perms:${user.id}`, JSON.stringify(perms)); 100 | await this.logService.saveLoginLog(user.id, ip, ua); 101 | return jwtSign; 102 | } 103 | 104 | /** 105 | * 注册 106 | */ 107 | async register(param: RegisterInfoDto): Promise { 108 | await this.userService.register(param); 109 | } 110 | 111 | /** 112 | * 发送验证码 113 | */ 114 | async sendCode(email: string, ip: string): Promise { 115 | const LIMIT_TIME = 5; 116 | const getRemainTime = () => { 117 | const now = dayjs(); 118 | return now.endOf('day').diff(now, 'second'); 119 | }; 120 | 121 | // ip限制 122 | const ipLimit = await this.redisService.getRedis().get(`admin:ip:${ip}:code:limit`); 123 | if (ipLimit) throw new ApiException(10200); 124 | 125 | // 1分钟最多接收1条 126 | const limit = await this.redisService.getRedis().get(`admin:email:${email}:limit`); 127 | if (limit) throw new ApiException(10200); 128 | 129 | // 1天一个邮箱最多接收5条 130 | let limitDayNum: string | number = await this.redisService 131 | .getRedis() 132 | .get(`admin:email:${email}:limit-day`); 133 | limitDayNum = limitDayNum ? parseInt(limitDayNum) : 0; 134 | if (limitDayNum > LIMIT_TIME) throw new ApiException(10201); 135 | 136 | // 1天一个ip最多发送5条 137 | let ipLimitDayNum: string | number = await this.redisService 138 | .getRedis() 139 | .get(`admin:ip:${ip}:code:limit-day`); 140 | ipLimitDayNum = ipLimitDayNum ? parseInt(ipLimitDayNum) : 0; 141 | if (ipLimitDayNum > LIMIT_TIME) throw new ApiException(10201); 142 | if (ipLimitDayNum) throw new ApiException(10200); 143 | 144 | // 发送验证码 145 | const code = Math.random().toString(16).substring(2, 6); 146 | await this.emailService.sendCodeMail(email, code); 147 | 148 | await this.redisService.getRedis().set(`admin:ip:${ip}:code:limit`, 1, 'EX', 60); 149 | await this.redisService.getRedis().set(`admin:email:${email}:limit`, 1, 'EX', 60); 150 | await this.redisService 151 | .getRedis() 152 | .set(`admin:email:${email}:limit-day`, ++limitDayNum, 'EX', getRemainTime()); 153 | await this.redisService 154 | .getRedis() 155 | .set(`admin:ip:${ip}:code:limit-day`, ++ipLimitDayNum, 'EX', getRemainTime()); 156 | 157 | // 验证码5分钟过期时间 158 | await this.redisService.getRedis().set(`admin:email:${email}:code`, code, 'EX', 60 * 5); 159 | } 160 | 161 | /** 162 | * 校验验证码 163 | */ 164 | async checkCode(email: string, code: string): Promise { 165 | const result = await this.redisService.getRedis().get(`admin:email:${email}:code`); 166 | if (isEmpty(result) || code.toLowerCase() !== result.toLowerCase()) { 167 | throw new ApiException(10002); 168 | } 169 | // 校验成功后移除验证码 170 | await this.redisService.getRedis().del(`admin:email:${email}:code`); 171 | } 172 | 173 | /** 174 | * 清除登录状态信息 175 | */ 176 | async clearLoginStatus(uid: number): Promise { 177 | await this.userService.forbidden(uid); 178 | } 179 | 180 | /** 181 | * 获取菜单列表 182 | */ 183 | async getMenu(uid: number): Promise { 184 | return await this.menuService.getMenus(uid); 185 | } 186 | 187 | /** 188 | * 获取权限列表 189 | */ 190 | async getPerm(uid: number): Promise { 191 | return await this.menuService.getPerms(uid); 192 | } 193 | 194 | async getRedisPasswordVersionById(id: number): Promise { 195 | return this.redisService.getRedis().get(`admin:passwordVersion:${id}`); 196 | } 197 | 198 | async getRedisTokenById(id: number): Promise { 199 | return this.redisService.getRedis().get(`admin:token:${id}`); 200 | } 201 | 202 | async getRedisPermsById(id: number): Promise { 203 | return this.redisService.getRedis().get(`admin:perms:${id}`); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/modules/admin/system/log/log.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class LoginLogInfo { 4 | @ApiProperty({ description: '日志编号' }) 5 | id: number; 6 | 7 | @ApiProperty({ description: '登录ip' }) 8 | ip: string; 9 | 10 | @ApiProperty({ description: '系统' }) 11 | os: string; 12 | 13 | @ApiProperty({ description: '浏览器' }) 14 | browser: string; 15 | 16 | @ApiProperty({ description: '时间' }) 17 | time: string; 18 | 19 | @ApiProperty({ description: '登录用户名' }) 20 | username: string; 21 | } 22 | 23 | export class TaskLogInfo { 24 | @ApiProperty({ description: '日志编号' }) 25 | id: number; 26 | 27 | @ApiProperty({ description: '任务编号' }) 28 | taskId: number; 29 | 30 | @ApiProperty({ description: '任务名称' }) 31 | name: string; 32 | 33 | @ApiProperty({ description: '创建时间' }) 34 | createdAt: string; 35 | 36 | @ApiProperty({ description: '耗时' }) 37 | consumeTime: number; 38 | 39 | @ApiProperty({ description: '执行信息' }) 40 | detail: string; 41 | 42 | @ApiProperty({ description: '任务执行状态' }) 43 | status: number; 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/admin/system/log/log.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; 3 | import { PageResult } from '@/common/class/res.class'; 4 | import { ADMIN_PREFIX } from '../../admin.constants'; 5 | import { LogDisabled } from '../../core/decorators/log-disabled.decorator'; 6 | import { LoginLogInfo, TaskLogInfo } from './log.class'; 7 | import { SysLogService } from './log.service'; 8 | import { PageSearchLoginLogDto, PageSearchTaskLogDto } from './log.dto'; 9 | 10 | @ApiSecurity(ADMIN_PREFIX) 11 | @ApiTags('日志模块') 12 | @Controller('log') 13 | export class SysLogController { 14 | constructor(private logService: SysLogService) {} 15 | 16 | @ApiOperation({ summary: '分页查询登录日志' }) 17 | @ApiOkResponse({ type: [LoginLogInfo] }) 18 | @LogDisabled() 19 | @Get('login/page') 20 | async loginLogPage(@Query() dto: PageSearchLoginLogDto): Promise> { 21 | const items = await this.logService.pageGetLoginLog(dto); 22 | const count = await this.logService.countLoginLog(); 23 | return { 24 | items, 25 | total: count, 26 | }; 27 | } 28 | 29 | @ApiOperation({ summary: '分页查询任务日志' }) 30 | @ApiOkResponse({ type: [TaskLogInfo] }) 31 | @LogDisabled() 32 | @Get('task/page') 33 | async taskPage(@Query() dto: PageSearchTaskLogDto): Promise> { 34 | const items = await this.logService.page(dto.page - 1, dto.pageSize); 35 | const count = await this.logService.countTaskLog(); 36 | return { 37 | items, 38 | total: count, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/admin/system/log/log.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsOptional, IsString } from 'class-validator'; 3 | import { PaginateDto } from '@/common/dto/page.dto'; 4 | 5 | export class PageSearchLoginLogDto extends PaginateDto { 6 | @ApiProperty({ description: '用户名' }) 7 | @IsOptional() 8 | @IsString() 9 | username: string; 10 | 11 | @ApiProperty({ description: '登录IP' }) 12 | @IsString() 13 | @IsOptional() 14 | ip: string; 15 | 16 | @ApiProperty({ description: '登录地点' }) 17 | @IsString() 18 | @IsOptional() 19 | address: string; 20 | 21 | @ApiProperty({ description: '登录时间' }) 22 | @IsOptional() 23 | time: string[]; 24 | } 25 | 26 | export class PageSearchTaskLogDto extends PaginateDto { 27 | @ApiProperty({ description: '用户名' }) 28 | @IsOptional() 29 | @IsString() 30 | username: string; 31 | 32 | @ApiProperty({ description: '登录IP' }) 33 | @IsString() 34 | @IsOptional() 35 | ip: string; 36 | 37 | @ApiProperty({ description: '登录时间' }) 38 | @IsOptional() 39 | time: string[]; 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/admin/system/log/log.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import SysLoginLog from '@/entities/admin/sys-login-log.entity'; 4 | import SysTaskLog from '@/entities/admin/sys-task-log.entity'; 5 | import { Between, Like, Repository } from 'typeorm'; 6 | import { UAParser } from 'ua-parser-js'; 7 | import { LoginLogInfo, TaskLogInfo } from './log.class'; 8 | import { PageSearchLoginLogDto } from './log.dto'; 9 | import { SysUserService } from '../user/user.service'; 10 | import { IpService } from '@/shared/services/ip.service'; 11 | 12 | @Injectable() 13 | export class SysLogService { 14 | constructor( 15 | @InjectRepository(SysLoginLog) 16 | private loginLogRepository: Repository, 17 | @InjectRepository(SysTaskLog) 18 | private taskLogRepository: Repository, 19 | private userService: SysUserService, 20 | private ipService: IpService, 21 | ) {} 22 | 23 | /** 24 | * 记录登录日志 25 | */ 26 | async saveLoginLog(uid: number, ip: string, ua: string): Promise { 27 | const address = await this.ipService.getAddress(ip); 28 | 29 | await this.loginLogRepository.save({ 30 | ip, 31 | userId: uid, 32 | ua, 33 | address, 34 | }); 35 | } 36 | 37 | /** 38 | * 计算登录日志日志总数 39 | */ 40 | async countLoginLog(): Promise { 41 | return await this.loginLogRepository.count(); 42 | } 43 | 44 | /** 45 | * 分页加载日志信息 46 | */ 47 | async pageGetLoginLog(dto: PageSearchLoginLogDto): Promise { 48 | const { page, pageSize, username, ip, address, time } = dto; 49 | 50 | const where = { 51 | ...(ip ? { ip: Like(`%${ip}%`) } : null), 52 | ...(address ? { address: Like(`%${address}%`) } : null), 53 | ...(time ? { createdAt: Between(time[0], time[1]) } : null), 54 | }; 55 | 56 | if (username) { 57 | const user = await this.userService.findUserByUserName(username); 58 | where['userId'] = user?.id; 59 | } 60 | 61 | const result = await this.loginLogRepository 62 | .createQueryBuilder('login_log') 63 | .innerJoinAndSelect('sys_user', 'user', 'login_log.user_id = user.id') 64 | .where(where) 65 | .orderBy('login_log.created_at', 'DESC') 66 | .offset((page - 1) * pageSize) 67 | .limit(pageSize) 68 | .getRawMany(); 69 | const parser = new UAParser(); 70 | 71 | return result.map((e) => { 72 | const u = parser.setUA(e.login_log_ua).getResult(); 73 | return { 74 | id: e.login_log_id, 75 | ip: e.login_log_ip, 76 | address: e.login_log_address, 77 | os: `${u.os.name} ${u.os.version}`, 78 | browser: `${u.browser.name} ${u.browser.version}`, 79 | time: e.login_log_created_at, 80 | username: e.user_username, 81 | }; 82 | }); 83 | } 84 | 85 | /** 86 | * 清空表中的所有数据 87 | */ 88 | async clearLoginLog(): Promise { 89 | await this.loginLogRepository.clear(); 90 | } 91 | // ----- task 92 | 93 | /** 94 | * 记录任务日志 95 | */ 96 | async recordTaskLog(tid: number, status: number, time?: number, err?: string): Promise { 97 | const result = await this.taskLogRepository.save({ 98 | taskId: tid, 99 | status, 100 | detail: err, 101 | }); 102 | return result.id; 103 | } 104 | 105 | /** 106 | * 计算日志总数 107 | */ 108 | async countTaskLog(): Promise { 109 | return await this.taskLogRepository.count(); 110 | } 111 | 112 | /** 113 | * 分页加载日志信息 114 | */ 115 | async page(page: number, count: number): Promise { 116 | const result = await this.taskLogRepository 117 | .createQueryBuilder('task_log') 118 | .leftJoinAndSelect('sys_task', 'task', 'task_log.task_id = task.id') 119 | .orderBy('task_log.id', 'DESC') 120 | .offset(page * count) 121 | .limit(count) 122 | .getRawMany(); 123 | return result.map((e) => { 124 | return { 125 | id: e.task_log_id, 126 | taskId: e.task_id, 127 | name: e.task_name, 128 | createdAt: e.task_log_created_at, 129 | consumeTime: e.task_log_consume_time, 130 | detail: e.task_log_detail, 131 | status: e.task_log_status, 132 | }; 133 | }); 134 | } 135 | 136 | /** 137 | * 清空表中的所有数据 138 | */ 139 | async clearTaskLog(): Promise { 140 | await this.taskLogRepository.clear(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/modules/admin/system/menu/menu.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import SysMenu from '@/entities/admin/sys-menu.entity'; 3 | 4 | export class MenuItemAndParentInfoResult { 5 | @ApiProperty({ description: '菜单' }) 6 | menu?: SysMenu; 7 | 8 | @ApiProperty({ description: '父级菜单' }) 9 | parentMenu?: SysMenu; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/admin/system/menu/menu.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; 3 | import { flattenDeep } from 'lodash'; 4 | import { ADMIN_PREFIX, FORBIDDEN_OP_MENU_ID_INDEX } from '@/modules/admin/admin.constants'; 5 | import { ApiException } from '@/common/exceptions/api.exception'; 6 | import SysMenu from '@/entities/admin/sys-menu.entity'; 7 | import { IAdminUser } from '../../admin.interface'; 8 | import { AdminUser } from '../../core/decorators/admin-user.decorator'; 9 | import { MenuItemAndParentInfoResult } from './menu.class'; 10 | import { 11 | CreateMenuDto, 12 | DeleteMenuDto, 13 | InfoMenuDto, 14 | SearchMenuDto, 15 | UpdateMenuDto, 16 | } from './menu.dto'; 17 | import { SysMenuService } from './menu.service'; 18 | 19 | @ApiSecurity(ADMIN_PREFIX) 20 | @ApiTags('菜单权限模块') 21 | @Controller('menu') 22 | export class SysMenuController { 23 | constructor(private menuService: SysMenuService) {} 24 | 25 | @ApiOperation({ summary: '获取所有菜单列表' }) 26 | @ApiOkResponse({ type: [SysMenu] }) 27 | @Get('list') 28 | async list(): Promise { 29 | return await this.menuService.list(); 30 | } 31 | 32 | @ApiOperation({ summary: '新增菜单或权限' }) 33 | @Post('add') 34 | async add(@Body() dto: CreateMenuDto): Promise { 35 | // check 36 | await this.menuService.check(dto); 37 | if (!dto.parent) { 38 | dto.parent = null; 39 | } 40 | if (dto.type === 0) { 41 | dto.component = 'LAYOUT'; 42 | } 43 | 44 | await this.menuService.save(dto); 45 | if (dto.type === 2) { 46 | // 如果是权限发生更改,则刷新所有在线用户的权限 47 | await this.menuService.refreshOnlineUserPerms(); 48 | } 49 | } 50 | 51 | @ApiOperation({ summary: '更新菜单或权限' }) 52 | @Post('update') 53 | async update(@Body() dto: UpdateMenuDto): Promise { 54 | if (dto.id <= FORBIDDEN_OP_MENU_ID_INDEX) { 55 | // 系统内置功能不提供删除 56 | throw new ApiException(10016); 57 | } 58 | // check 59 | await this.menuService.check(dto); 60 | if (dto.parent === -1 || !dto.parent) { 61 | dto.parent = null; 62 | } 63 | const insertData: CreateMenuDto & { id: number } = { 64 | ...dto, 65 | id: dto.id, 66 | }; 67 | await this.menuService.save(insertData); 68 | if (dto.type === 2) { 69 | // 如果是权限发生更改,则刷新所有在线用户的权限 70 | await this.menuService.refreshOnlineUserPerms(); 71 | } 72 | } 73 | 74 | @ApiOperation({ summary: '删除菜单或权限' }) 75 | @Post('delete') 76 | async delete(@Body() dto: DeleteMenuDto): Promise { 77 | // 68为内置init.sql中插入最后的索引编号 78 | if (dto.id <= FORBIDDEN_OP_MENU_ID_INDEX) { 79 | // 系统内置功能不提供删除 80 | throw new ApiException(10016); 81 | } 82 | // 如果有子目录,一并删除 83 | const childMenus = await this.menuService.findChildMenus(dto.id); 84 | await this.menuService.deleteMenuItem(flattenDeep([dto.id, childMenus])); 85 | // 刷新在线用户权限 86 | await this.menuService.refreshOnlineUserPerms(); 87 | } 88 | 89 | @ApiOperation({ summary: '获取菜单或权限信息' }) 90 | @ApiOkResponse({ type: MenuItemAndParentInfoResult }) 91 | @Get('info') 92 | async info(@Query() dto: InfoMenuDto): Promise { 93 | return await this.menuService.getMenuItemAndParentInfo(dto.menuId); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/modules/admin/system/menu/menu.dto.ts: -------------------------------------------------------------------------------- 1 | import { PaginateDto } from '@/common/dto/page.dto'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | import { 5 | IsBoolean, 6 | IsIn, 7 | IsInt, 8 | IsOptional, 9 | IsString, 10 | Matches, 11 | Min, 12 | MinLength, 13 | ValidateIf, 14 | } from 'class-validator'; 15 | 16 | /** 17 | * 增加菜单 18 | */ 19 | export class CreateMenuDto { 20 | @ApiProperty({ description: '菜单类型' }) 21 | @IsIn([0, 1, 2]) 22 | type: number; 23 | 24 | @ApiProperty({ description: '父级菜单' }) 25 | @IsOptional() 26 | parent: number; 27 | 28 | @ApiProperty({ description: '菜单或权限名称' }) 29 | @IsString() 30 | @MinLength(2) 31 | name: string; 32 | 33 | @ApiProperty({ description: '排序' }) 34 | @IsInt() 35 | @Min(0) 36 | orderNo: number; 37 | 38 | @ApiProperty({ description: '前端路由地址' }) 39 | // @Matches(/^[/]$/) 40 | @ValidateIf((o) => o.type !== 2) 41 | path: string; 42 | 43 | @ApiProperty({ description: '是否外链', required: false, default: 1 }) 44 | @IsIn([0, 1]) 45 | @ValidateIf((o) => o.type !== 2) 46 | readonly external: number; 47 | 48 | @ApiProperty({ description: '菜单是否显示', required: false, default: 1 }) 49 | @IsIn([0, 1]) 50 | @ValidateIf((o) => o.type !== 2) 51 | readonly show: number; 52 | 53 | @ApiProperty({ description: '开启页面缓存', required: false, default: 1 }) 54 | @IsIn([0, 1]) 55 | @ValidateIf((o) => o.type === 1) 56 | readonly keepalive: number; 57 | 58 | @ApiProperty({ description: '状态', required: false, default: 1 }) 59 | @IsIn([0, 1]) 60 | readonly status: number; 61 | 62 | @ApiProperty({ description: '菜单图标', required: false }) 63 | @IsString() 64 | @IsOptional() 65 | @ValidateIf((o) => o.type !== 2) 66 | icon: string; 67 | 68 | @ApiProperty({ description: '对应权限' }) 69 | @IsString() 70 | @IsOptional() 71 | @ValidateIf((o) => o.type === 2) 72 | permission: string; 73 | 74 | @ApiProperty({ description: '菜单路由路径或外链' }) 75 | @ValidateIf((o) => o.type !== 2) 76 | @IsString() 77 | @IsOptional() 78 | component: string; 79 | } 80 | 81 | export class UpdateMenuDto extends CreateMenuDto { 82 | @ApiProperty({ description: '更新的菜单ID' }) 83 | @IsInt() 84 | @Min(0) 85 | id: number; 86 | } 87 | 88 | /** 89 | * 删除菜单 90 | */ 91 | export class DeleteMenuDto { 92 | @ApiProperty({ description: '删除的菜单ID' }) 93 | @IsInt() 94 | @Min(0) 95 | id: number; 96 | } 97 | 98 | /** 99 | * 查询菜单 100 | */ 101 | export class InfoMenuDto { 102 | @ApiProperty({ description: '查询的菜单ID' }) 103 | @IsInt() 104 | @Min(0) 105 | @Type(() => Number) 106 | menuId: number; 107 | } 108 | 109 | export class SearchMenuDto { 110 | @ApiProperty({ description: '菜单名称' }) 111 | @IsOptional() 112 | @IsString() 113 | name: string; 114 | 115 | @ApiProperty({ description: '路由' }) 116 | @IsString() 117 | @IsOptional() 118 | path: string; 119 | 120 | @ApiProperty({ description: '权限标识' }) 121 | @IsString() 122 | @IsOptional() 123 | permission: string; 124 | 125 | @ApiProperty({ description: '组件' }) 126 | @IsString() 127 | @IsOptional() 128 | component: string; 129 | 130 | @ApiProperty({ description: '状态' }) 131 | @IsOptional() 132 | status: number; 133 | } 134 | -------------------------------------------------------------------------------- /src/modules/admin/system/menu/menu.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { concat, includes, isEmpty, uniq } from 'lodash'; 4 | import { ROOT_ROLE_ID } from '@/modules/admin/admin.constants'; 5 | import { ApiException } from '@/common/exceptions/api.exception'; 6 | import SysMenu from '@/entities/admin/sys-menu.entity'; 7 | import { In, IsNull, Like, Not, Repository } from 'typeorm'; 8 | import { SysRoleService } from '../role/role.service'; 9 | import { MenuItemAndParentInfoResult } from './menu.class'; 10 | import { CreateMenuDto, SearchMenuDto } from './menu.dto'; 11 | import { RedisService } from '@/shared/services/redis.service'; 12 | import { generatorMenu, generatorRouters } from '../../core/permission'; 13 | 14 | @Injectable() 15 | export class SysMenuService { 16 | constructor( 17 | @InjectRepository(SysMenu) private menuRepository: Repository, 18 | private redisService: RedisService, 19 | @Inject(ROOT_ROLE_ID) private rootRoleId: number, 20 | private roleService: SysRoleService, 21 | ) {} 22 | 23 | /** 24 | * 获取所有菜单以及权限 25 | */ 26 | async list(): Promise { 27 | // const { name, path, permission, component, status } = dto; 28 | // const where = { 29 | // ...(name ? { name: Like(`%${name}%`) } : null), 30 | // ...(path ? { path: Like(`%${path}%`) } : null), 31 | // ...(permission ? { permission: Like(`%${permission}%`) } : null), 32 | // ...(component ? { component: Like(`%${component}%`) } : null), 33 | // ...(status ? { status: status } : null), 34 | // }; 35 | const menus = await this.menuRepository.find({ 36 | order: { orderNo: 'ASC' }, 37 | }); 38 | const menuList = generatorMenu(menus); 39 | return menuList; 40 | } 41 | 42 | /** 43 | * 保存或新增菜单 44 | */ 45 | async save(menu: CreateMenuDto & { id?: number }): Promise { 46 | await this.menuRepository.save(menu); 47 | } 48 | 49 | /** 50 | * 根据角色获取所有菜单 51 | */ 52 | async getMenus(uid: number): Promise { 53 | const roleIds = await this.roleService.getRoleIdByUser(uid); 54 | let menus: SysMenu[] = []; 55 | if (includes(roleIds, this.rootRoleId)) { 56 | menus = await this.menuRepository.find({ order: { orderNo: 'ASC' } }); 57 | } else { 58 | menus = await this.menuRepository 59 | .createQueryBuilder('menu') 60 | .innerJoinAndSelect('sys_role_menu', 'role_menu', 'menu.id = role_menu.menu_id') 61 | .andWhere('role_menu.role_id IN (:...roldIds)', { roldIds: roleIds }) 62 | .orderBy('menu.order_no', 'ASC') 63 | .getMany(); 64 | } 65 | 66 | const menuList = generatorRouters(menus); 67 | return menuList; 68 | } 69 | 70 | /** 71 | * 检查菜单创建规则是否符合 72 | */ 73 | async check(dto: CreateMenuDto): Promise { 74 | if (dto.type === 2 && !dto.parent) { 75 | // 无法直接创建权限,必须有parent 76 | throw new ApiException(10005); 77 | } 78 | if (dto.type === 1 && dto.parent) { 79 | const parent = await this.getMenuItemInfo(dto.parent); 80 | if (isEmpty(parent)) { 81 | throw new ApiException(10014); 82 | } 83 | if (parent && parent.type === 1) { 84 | // 当前新增为菜单但父节点也为菜单时为非法操作 85 | throw new ApiException(10006); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * 查找当前菜单下的子菜单,目录以及菜单 92 | */ 93 | async findChildMenus(mid: number): Promise { 94 | const allMenus: any = []; 95 | const menus = await this.menuRepository.findBy({ parent: mid }); 96 | // if (_.isEmpty(menus)) { 97 | // return allMenus; 98 | // } 99 | // const childMenus: any = []; 100 | for (let i = 0; i < menus.length; i++) { 101 | if (menus[i].type !== 2) { 102 | // 子目录下是菜单或目录,继续往下级查找 103 | const c = await this.findChildMenus(menus[i].id); 104 | allMenus.push(c); 105 | } 106 | allMenus.push(menus[i].id); 107 | } 108 | return allMenus; 109 | } 110 | 111 | /** 112 | * 获取某个菜单的信息 113 | * @param mid menu id 114 | */ 115 | async getMenuItemInfo(mid: number): Promise { 116 | const menu = await this.menuRepository.findOneBy({ id: mid }); 117 | return menu; 118 | } 119 | 120 | /** 121 | * 获取某个菜单以及关联的父菜单的信息 122 | */ 123 | async getMenuItemAndParentInfo(mid: number): Promise { 124 | const menu = await this.menuRepository.findOneBy({ id: mid }); 125 | let parentMenu: SysMenu | undefined = undefined; 126 | if (menu && menu.parent) { 127 | parentMenu = await this.menuRepository.findOneBy({ id: menu.parent }); 128 | } 129 | return { menu, parentMenu }; 130 | } 131 | 132 | /** 133 | * 查找节点路由是否存在 134 | */ 135 | async findRouterExist(path: string): Promise { 136 | const menus = await this.menuRepository.findOneBy({ path }); 137 | return !isEmpty(menus); 138 | } 139 | 140 | /** 141 | * 获取当前用户的所有权限 142 | */ 143 | async getPerms(uid: number): Promise { 144 | const roleIds = await this.roleService.getRoleIdByUser(uid); 145 | let permission: any[] = []; 146 | let result: any = null; 147 | if (includes(roleIds, this.rootRoleId)) { 148 | result = await this.menuRepository.findBy({ 149 | permission: Not(IsNull()), 150 | type: In([1, 2]), 151 | }); 152 | } else { 153 | if (isEmpty(roleIds)) { 154 | return permission; 155 | } 156 | result = await this.menuRepository 157 | .createQueryBuilder('menu') 158 | .innerJoinAndSelect('sys_role_menu', 'role_menu', 'menu.id = role_menu.menu_id') 159 | .andWhere('role_menu.role_id IN (:...roldIds)', { roldIds: roleIds }) 160 | .andWhere('menu.type IN (1,2)') 161 | .andWhere('menu.permission IS NOT NULL') 162 | .getMany(); 163 | } 164 | if (!isEmpty(result)) { 165 | result.forEach((e) => { 166 | if (e.permission) { 167 | permission = concat(permission, e.permission.split(',')); 168 | } 169 | }); 170 | permission = uniq(permission); 171 | } 172 | return permission; 173 | } 174 | 175 | /** 176 | * 删除多项菜单 177 | */ 178 | async deleteMenuItem(mids: number[]): Promise { 179 | await this.menuRepository.delete(mids); 180 | } 181 | 182 | /** 183 | * 刷新指定用户ID的权限 184 | */ 185 | async refreshPerms(uid: number): Promise { 186 | const perms = await this.getPerms(uid); 187 | const online = await this.redisService.getRedis().get(`admin:token:${uid}`); 188 | if (online) { 189 | // 判断是否在线 190 | await this.redisService.getRedis().set(`admin:perms:${uid}`, JSON.stringify(perms)); 191 | } 192 | } 193 | 194 | /** 195 | * 刷新所有在线用户的权限 196 | */ 197 | async refreshOnlineUserPerms(): Promise { 198 | const onlineUserIds: string[] = await this.redisService.getRedis().keys('admin:token:*'); 199 | if (onlineUserIds && onlineUserIds.length > 0) { 200 | for (let i = 0; i < onlineUserIds.length; i++) { 201 | const uid = onlineUserIds[i].split('admin:token:')[1]; 202 | if (!uid) continue; 203 | const perms = await this.getPerms(parseInt(uid)); 204 | await this.redisService.getRedis().set(`admin:perms:${uid}`, JSON.stringify(perms)); 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/modules/admin/system/online/online.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class OnlineUserInfo { 4 | @ApiProperty({ description: '最近的一条登录日志ID' }) 5 | id: number; 6 | 7 | @ApiProperty({ description: '登录IP' }) 8 | ip: string; 9 | 10 | @ApiProperty({ description: '登录地点' }) 11 | address: string; 12 | 13 | @ApiProperty({ description: '用户名' }) 14 | username: string; 15 | 16 | @ApiProperty({ description: '是否当前' }) 17 | isCurrent: boolean; 18 | 19 | @ApiProperty({ description: '登陆时间' }) 20 | time: string; 21 | 22 | @ApiProperty({ description: '系统' }) 23 | os: string; 24 | 25 | @ApiProperty({ description: '浏览器' }) 26 | browser: string; 27 | 28 | @ApiProperty({ description: '是否禁用' }) 29 | disable: boolean; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/admin/system/online/online.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; 3 | import { ApiException } from '@/common/exceptions/api.exception'; 4 | import { ADMIN_PREFIX } from '../../admin.constants'; 5 | import { IAdminUser } from '../../admin.interface'; 6 | import { AdminUser } from '../../core/decorators/admin-user.decorator'; 7 | import { LogDisabled } from '../../core/decorators/log-disabled.decorator'; 8 | import { OnlineUserInfo } from './online.class'; 9 | import { KickDto } from './online.dto'; 10 | import { SysOnlineService } from './online.service'; 11 | 12 | @ApiSecurity(ADMIN_PREFIX) 13 | @ApiTags('在线用户模块') 14 | @Controller('online') 15 | export class SysOnlineController { 16 | constructor(private onlineService: SysOnlineService) {} 17 | 18 | @ApiOperation({ summary: '查询当前在线用户' }) 19 | @ApiOkResponse({ type: [OnlineUserInfo] }) 20 | @LogDisabled() 21 | @Get('list') 22 | async list(@AdminUser() user: IAdminUser): Promise { 23 | return await this.onlineService.listOnlineUser(user.uid); 24 | } 25 | 26 | @ApiOperation({ summary: '下线指定在线用户' }) 27 | @Post('kick') 28 | async kick(@Body() dto: KickDto, @AdminUser() user: IAdminUser): Promise { 29 | if (dto.id === user.uid) { 30 | throw new ApiException(10012); 31 | } 32 | await this.onlineService.kickUser(dto.id, user.uid); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/admin/system/online/online.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsInt } from 'class-validator'; 3 | 4 | export class KickDto { 5 | @ApiProperty({ description: '需要下线的角色ID' }) 6 | @IsInt() 7 | id: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/admin/system/online/online.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { InjectEntityManager } from '@nestjs/typeorm'; 4 | import { ApiException } from '@/common/exceptions/api.exception'; 5 | import { AdminWSService } from '@/modules/ws/admin-ws.service'; 6 | import { AdminWSGateway } from '@/modules/ws/admin-ws.gateway'; 7 | import { EVENT_KICK } from '@/modules/ws/ws.event'; 8 | import { EntityManager } from 'typeorm'; 9 | import { UAParser } from 'ua-parser-js'; 10 | import { SysUserService } from '../user/user.service'; 11 | import { OnlineUserInfo } from './online.class'; 12 | 13 | @Injectable() 14 | export class SysOnlineService { 15 | constructor( 16 | @InjectEntityManager() private entityManager: EntityManager, 17 | private userService: SysUserService, 18 | private adminWsGateWay: AdminWSGateway, 19 | private adminWSService: AdminWSService, 20 | private jwtService: JwtService, 21 | ) {} 22 | 23 | /** 24 | * 罗列在线用户列表 25 | */ 26 | async listOnlineUser(currentUid: number): Promise { 27 | const onlineSockets = await this.adminWSService.getOnlineSockets(); 28 | if (!onlineSockets || onlineSockets.length <= 0) { 29 | return []; 30 | } 31 | const onlineIds = onlineSockets.map((socket) => { 32 | const token = socket.handshake.query?.token as string; 33 | return this.jwtService.verify(token).uid; 34 | }); 35 | return await this.findLastLoginInfoList(onlineIds, currentUid); 36 | } 37 | 38 | /** 39 | * 下线当前用户 40 | */ 41 | async kickUser(uid: number, currentUid: number): Promise { 42 | const rootUserId = await this.userService.findRootUserId(); 43 | const currentUserInfo = await this.userService.getAccountInfo(currentUid); 44 | if (uid === rootUserId) { 45 | throw new ApiException(10013); 46 | } 47 | // reset redis keys 48 | await this.userService.forbidden(uid); 49 | // socket emit 50 | const socket = await this.adminWSService.findSocketIdByUid(uid); 51 | if (socket) { 52 | // socket emit event 53 | this.adminWsGateWay.socketServer 54 | .to(socket.id) 55 | .emit(EVENT_KICK, { operater: currentUserInfo.username }); 56 | // close socket 57 | socket.disconnect(); 58 | } 59 | } 60 | 61 | /** 62 | * 根据用户id列表查找最近登录信息和用户信息 63 | */ 64 | async findLastLoginInfoList(ids: number[], currentUid: number): Promise { 65 | const rootUserId = await this.userService.findRootUserId(); 66 | const result = await this.entityManager.query( 67 | ` 68 | SELECT sys_login_log.created_at, sys_login_log.ip, sys_login_log.address, sys_login_log.ua, sys_user.id, sys_user.username, sys_user.nick_name 69 | FROM sys_login_log 70 | INNER JOIN sys_user ON sys_login_log.user_id = sys_user.id 71 | WHERE sys_login_log.created_at IN (SELECT MAX(created_at) as createdAt FROM sys_login_log GROUP BY user_id) 72 | AND sys_user.id IN (?) 73 | `, 74 | [ids], 75 | ); 76 | if (result) { 77 | const parser = new UAParser(); 78 | return result.map((e) => { 79 | const u = parser.setUA(e.ua).getResult(); 80 | return { 81 | id: e.id, 82 | ip: e.ip, 83 | address: e.address, 84 | username: `${e.nick_name}(${e.username})`, 85 | isCurrent: currentUid === e.id, 86 | time: e.created_at, 87 | os: `${u.os.name} ${u.os.version}`, 88 | browser: `${u.browser.name} ${u.browser.version}`, 89 | disable: currentUid === e.id || e.id === rootUserId, 90 | }; 91 | }); 92 | } 93 | return []; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/modules/admin/system/param-config/param-config.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; 3 | import { PageResult } from '@/common/class/res.class'; 4 | import { PaginateDto } from '@/common/dto/page.dto'; 5 | import SysConfig from '@/entities/admin/sys-config.entity'; 6 | import { ADMIN_PREFIX } from '../../admin.constants'; 7 | import { 8 | CreateParamConfigDto, 9 | DeleteParamConfigDto, 10 | InfoParamConfigDto, 11 | UpdateParamConfigDto, 12 | } from './param-config.dto'; 13 | import { SysParamConfigService } from './param-config.service'; 14 | 15 | @ApiSecurity(ADMIN_PREFIX) 16 | @ApiTags('参数配置模块') 17 | @Controller('param-config') 18 | export class SysParamConfigController { 19 | constructor(private paramConfigService: SysParamConfigService) {} 20 | 21 | @ApiOperation({ summary: '分页获取参数配置列表' }) 22 | @ApiOkResponse({ type: [SysConfig] }) 23 | @Get('page') 24 | async page(@Query() dto: PaginateDto): Promise> { 25 | const items = await this.paramConfigService.getConfigListByPage(dto.page - 1, dto.pageSize); 26 | const count = await this.paramConfigService.countConfigList(); 27 | return { 28 | items, 29 | total: count, 30 | }; 31 | } 32 | 33 | @ApiOperation({ summary: '新增参数配置' }) 34 | @Post('add') 35 | async add(@Body() dto: CreateParamConfigDto): Promise { 36 | await this.paramConfigService.isExistKey(dto.key); 37 | await this.paramConfigService.add(dto); 38 | } 39 | 40 | @ApiOperation({ summary: '查询单个参数配置信息' }) 41 | @ApiOkResponse({ type: SysConfig }) 42 | @Get('info') 43 | async info(@Query() dto: InfoParamConfigDto): Promise { 44 | return this.paramConfigService.findOne(dto.id); 45 | } 46 | 47 | @ApiOperation({ summary: '更新单个参数配置' }) 48 | @Post('update') 49 | async update(@Body() dto: UpdateParamConfigDto): Promise { 50 | await this.paramConfigService.update(dto); 51 | } 52 | 53 | @ApiOperation({ summary: '删除指定的参数配置' }) 54 | @Post('delete') 55 | async delete(@Body() dto: DeleteParamConfigDto): Promise { 56 | await this.paramConfigService.delete(dto.ids); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/admin/system/param-config/param-config.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { 4 | ArrayNotEmpty, 5 | IsArray, 6 | IsInt, 7 | IsOptional, 8 | IsString, 9 | Min, 10 | MinLength, 11 | } from 'class-validator'; 12 | 13 | export class CreateParamConfigDto { 14 | @ApiProperty({ description: '参数名称' }) 15 | @IsString() 16 | name: string; 17 | 18 | @ApiProperty({ description: '参数键名' }) 19 | @IsString() 20 | @MinLength(3) 21 | key: string; 22 | 23 | @ApiProperty({ description: '参数值' }) 24 | @IsString() 25 | value: string; 26 | 27 | @ApiProperty({ required: false, description: '备注' }) 28 | @IsString() 29 | @IsOptional() 30 | remark: string; 31 | } 32 | 33 | export class UpdateParamConfigDto { 34 | @ApiProperty({ description: '配置编号' }) 35 | @IsInt() 36 | @Min(1) 37 | id: number; 38 | 39 | @ApiProperty({ description: '参数名称' }) 40 | @IsString() 41 | name: string; 42 | 43 | @ApiProperty({ description: '参数值' }) 44 | @IsString() 45 | value: string; 46 | 47 | @ApiProperty({ required: false, description: '备注' }) 48 | @IsString() 49 | @IsOptional() 50 | remark: string; 51 | } 52 | 53 | export class DeleteParamConfigDto { 54 | @ApiProperty({ description: '需要删除的配置id列表', type: [Number] }) 55 | @IsArray() 56 | @ArrayNotEmpty() 57 | ids: number[]; 58 | } 59 | 60 | export class InfoParamConfigDto { 61 | @ApiProperty({ description: '需要查询的配置编号' }) 62 | @IsInt() 63 | @Min(0) 64 | @Type(() => Number) 65 | id: number; 66 | } 67 | -------------------------------------------------------------------------------- /src/modules/admin/system/param-config/param-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { ApiException } from '@/common/exceptions/api.exception'; 4 | import SysConfig from '@/entities/admin/sys-config.entity'; 5 | import { Repository } from 'typeorm'; 6 | import { CreateParamConfigDto, UpdateParamConfigDto } from './param-config.dto'; 7 | 8 | @Injectable() 9 | export class SysParamConfigService { 10 | constructor( 11 | @InjectRepository(SysConfig) 12 | private configRepository: Repository, 13 | ) {} 14 | 15 | /** 16 | * 罗列所有配置 17 | */ 18 | async getConfigListByPage(page: number, count: number): Promise { 19 | return this.configRepository.find({ 20 | order: { 21 | id: 'ASC', 22 | }, 23 | take: count, 24 | skip: page * count, 25 | }); 26 | } 27 | 28 | /** 29 | * 获取参数总数 30 | */ 31 | async countConfigList(): Promise { 32 | return this.configRepository.count(); 33 | } 34 | 35 | /** 36 | * 新增 37 | */ 38 | async add(dto: CreateParamConfigDto): Promise { 39 | await this.configRepository.insert(dto); 40 | } 41 | 42 | /** 43 | * 更新 44 | */ 45 | async update(dto: UpdateParamConfigDto): Promise { 46 | await this.configRepository.update( 47 | { id: dto.id }, 48 | { name: dto.name, value: dto.value, remark: dto.remark }, 49 | ); 50 | } 51 | 52 | /** 53 | * 删除 54 | */ 55 | async delete(ids: number[]): Promise { 56 | await this.configRepository.delete(ids); 57 | } 58 | 59 | /** 60 | * 查询单个 61 | */ 62 | async findOne(id: number): Promise { 63 | return await this.configRepository.findOneBy({ id }); 64 | } 65 | 66 | async isExistKey(key: string): Promise { 67 | const result = await this.configRepository.findOneBy({ key }); 68 | if (result) { 69 | throw new ApiException(10021); 70 | } 71 | } 72 | 73 | async findValueByKey(key: string): Promise { 74 | const result = await this.configRepository.findOne({ where: { key }, select: ['value'] }); 75 | if (result) { 76 | return result.value; 77 | } 78 | return null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/modules/admin/system/role/role.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import SysRoleMenu from '@/entities/admin/sys-role-menu.entity'; 3 | import SysRole from '@/entities/admin/sys-role.entity'; 4 | 5 | export class RoleInfo { 6 | @ApiProperty({ 7 | type: SysRole, 8 | }) 9 | info: SysRole; 10 | 11 | @ApiProperty({ 12 | type: [SysRoleMenu], 13 | }) 14 | menus: any[]; 15 | } 16 | 17 | export class CreatedRoleId { 18 | roleId: number; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/admin/system/role/role.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | import { ApiOperation, ApiOkResponse, ApiSecurity, ApiTags } from '@nestjs/swagger'; 3 | import { ADMIN_PREFIX } from '@/modules/admin/admin.constants'; 4 | import { PageSearchRoleDto } from './role.dto'; 5 | import { PageResult } from '@/common/class/res.class'; 6 | import SysRole from '@/entities/admin/sys-role.entity'; 7 | import { SysRoleService } from './role.service'; 8 | import { CreateRoleDto, DeleteRoleDto, InfoRoleDto, UpdateRoleDto } from './role.dto'; 9 | import { ApiException } from '@/common/exceptions/api.exception'; 10 | import { AdminUser } from '../../core/decorators/admin-user.decorator'; 11 | import { IAdminUser } from '../../admin.interface'; 12 | import { RoleInfo } from './role.class'; 13 | import { SysMenuService } from '../menu/menu.service'; 14 | 15 | @ApiSecurity(ADMIN_PREFIX) 16 | @ApiTags('角色模块') 17 | @Controller('role') 18 | export class SysRoleController { 19 | constructor(private roleService: SysRoleService, private menuService: SysMenuService) {} 20 | 21 | @ApiOperation({ summary: '获取角色列表' }) 22 | @ApiOkResponse({ type: [SysRole] }) 23 | @Get('list') 24 | async list(): Promise { 25 | return await this.roleService.list(); 26 | } 27 | 28 | @ApiOperation({ summary: '分页查询角色信息' }) 29 | @ApiOkResponse({ type: [SysRole] }) 30 | @Get('page') 31 | async page(@Query() dto: PageSearchRoleDto): Promise> { 32 | return await this.roleService.page(dto); 33 | } 34 | 35 | @ApiOperation({ summary: '删除角色' }) 36 | @Post('delete') 37 | async delete(@Body() dto: DeleteRoleDto): Promise { 38 | const count = await this.roleService.countUserIdByRole(dto.ids); 39 | if (count > 0) { 40 | throw new ApiException(10008); 41 | } 42 | await this.roleService.delete(dto.ids); 43 | await this.menuService.refreshOnlineUserPerms(); 44 | } 45 | 46 | @ApiOperation({ summary: '新增角色' }) 47 | @Post('add') 48 | async add(@Body() dto: CreateRoleDto): Promise { 49 | await this.roleService.add(dto); 50 | } 51 | 52 | @ApiOperation({ summary: '更新角色' }) 53 | @Post('update') 54 | async update(@Body() dto: UpdateRoleDto): Promise { 55 | await this.roleService.update(dto); 56 | await this.menuService.refreshOnlineUserPerms(); 57 | } 58 | 59 | @ApiOperation({ summary: '获取角色信息' }) 60 | @ApiOkResponse({ type: RoleInfo }) 61 | @Get('info') 62 | async info(@Query() dto: InfoRoleDto): Promise { 63 | return await this.roleService.info(dto.id); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/modules/admin/system/role/role.dto.ts: -------------------------------------------------------------------------------- 1 | import { PaginateDto } from '@/common/dto/page.dto'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | import { 5 | ArrayNotEmpty, 6 | IsArray, 7 | IsIn, 8 | IsInt, 9 | IsOptional, 10 | IsString, 11 | Matches, 12 | Min, 13 | MinLength, 14 | } from 'class-validator'; 15 | 16 | export class DeleteRoleDto { 17 | @ApiProperty({ description: '需要删除的角色ID列表', type: [Number] }) 18 | @IsArray() 19 | @ArrayNotEmpty() 20 | ids: number[]; 21 | } 22 | 23 | export class CreateRoleDto { 24 | @ApiProperty({ description: '角色名称' }) 25 | @IsString() 26 | @MinLength(2) 27 | name: string; 28 | 29 | @ApiProperty({ description: '角色值' }) 30 | @Matches(/^[a-z0-9A-Z]+$/) 31 | @MinLength(2) 32 | @IsString() 33 | value: string; 34 | 35 | @ApiProperty({ description: '角色备注', required: false }) 36 | @IsString() 37 | @IsOptional() 38 | remark: string; 39 | 40 | @ApiProperty({ description: '状态' }) 41 | @IsIn([0, 1]) 42 | @IsOptional() 43 | status: number; 44 | 45 | @ApiProperty({ description: '关联菜单、权限编号', required: false }) 46 | @IsOptional() 47 | @IsArray() 48 | menus: number[]; 49 | } 50 | 51 | export class UpdateRoleDto extends CreateRoleDto { 52 | @ApiProperty({ description: '关联部门编号' }) 53 | @IsInt() 54 | @Min(0) 55 | id: number; 56 | } 57 | 58 | export class InfoRoleDto { 59 | @ApiProperty({ description: '需要查找的角色ID' }) 60 | @IsInt() 61 | @Min(0) 62 | @Type(() => Number) 63 | id: number; 64 | } 65 | 66 | export class PageSearchRoleDto extends PaginateDto { 67 | @ApiProperty({ description: '角色名称' }) 68 | @IsOptional() 69 | @IsString() 70 | name: string; 71 | 72 | @ApiProperty({ description: '角色值' }) 73 | @IsString() 74 | @IsOptional() 75 | value: string; 76 | 77 | @ApiProperty({ description: '状态' }) 78 | @IsOptional() 79 | status: number; 80 | } 81 | -------------------------------------------------------------------------------- /src/modules/admin/system/role/role.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; 3 | import { difference, filter, includes, isEmpty, map } from 'lodash'; 4 | import { EntityManager, In, Like, Not, Repository } from 'typeorm'; 5 | import SysRole from '@/entities/admin/sys-role.entity'; 6 | import SysMenu from '@/entities/admin/sys-menu.entity'; 7 | import SysRoleMenu from '@/entities/admin/sys-role-menu.entity'; 8 | import SysUserRole from '@/entities/admin/sys-user-role.entity'; 9 | import { CreateRoleDto, UpdateRoleDto } from './role.dto'; 10 | import { CreatedRoleId } from './role.class'; 11 | import { ROOT_ROLE_ID } from '@/modules/admin/admin.constants'; 12 | import { PageSearchRoleDto } from './role.dto'; 13 | import { PageResult } from '@/common/class/res.class'; 14 | 15 | @Injectable() 16 | export class SysRoleService { 17 | constructor( 18 | @InjectRepository(SysRole) private roleRepository: Repository, 19 | @InjectRepository(SysMenu) private menuRepository: Repository, 20 | @InjectRepository(SysRoleMenu) private roleMenuRepository: Repository, 21 | @InjectRepository(SysUserRole) private userRoleRepository: Repository, 22 | @InjectEntityManager() private entityManager: EntityManager, 23 | @Inject(ROOT_ROLE_ID) private rootRoleId: number, 24 | ) {} 25 | 26 | /** 27 | * 列举所有角色:除去超级管理员 28 | */ 29 | async list(): Promise { 30 | const result = await this.roleRepository.findBy({ 31 | // id: Not(this.rootRoleId), 32 | }); 33 | 34 | return result; 35 | } 36 | 37 | /** 38 | * 列举所有角色条数:除去超级管理员 39 | */ 40 | async count(): Promise { 41 | const count = await this.roleRepository.countBy({ 42 | id: Not(this.rootRoleId), 43 | }); 44 | return count; 45 | } 46 | 47 | /** 48 | * 根据角色获取角色信息 49 | */ 50 | async info(rid: number): Promise { 51 | const info = await this.roleRepository.findOneBy({ id: rid }); 52 | const roleMenus = await this.roleMenuRepository.findBy({ roleId: rid }); 53 | const menus = roleMenus.map((m) => m.menuId); 54 | 55 | return { ...info, menus }; 56 | } 57 | 58 | /** 59 | * 根据角色Id数组删除 60 | */ 61 | async delete(roleIds: number[]): Promise { 62 | if (includes(roleIds, this.rootRoleId)) { 63 | throw new Error('不能删除超级管理员'); 64 | } 65 | await this.entityManager.transaction(async (manager) => { 66 | await manager.delete(SysRole, roleIds); 67 | await manager.delete(SysRoleMenu, { roleId: In(roleIds) }); 68 | }); 69 | } 70 | 71 | /** 72 | * 增加角色 73 | */ 74 | async add(param: CreateRoleDto): Promise { 75 | const { name, value, remark, menus } = param; 76 | const role = await this.roleRepository.insert({ 77 | name, 78 | value, 79 | remark, 80 | }); 81 | const { identifiers } = role; 82 | const roleId = parseInt(identifiers[0].id); 83 | if (menus && menus.length > 0) { 84 | // 关联菜单 85 | const insertRows = menus.map((m) => { 86 | return { 87 | roleId, 88 | menuId: m, 89 | }; 90 | }); 91 | await this.roleMenuRepository.insert(insertRows); 92 | } 93 | return { roleId }; 94 | } 95 | 96 | /** 97 | * 更新角色信息 98 | */ 99 | async update(param: UpdateRoleDto): Promise { 100 | const { id: roleId, name, value, remark, status, menus } = param; 101 | const role = await this.roleRepository.save({ 102 | id: roleId, 103 | name, 104 | value, 105 | remark, 106 | status, 107 | }); 108 | const originMenuRows = await this.roleMenuRepository.findBy({ roleId }); 109 | const originMenuIds = originMenuRows.map((e) => { 110 | return e.menuId; 111 | }); 112 | 113 | // 开始对比差异 114 | const insertMenusRowIds = difference(menus, originMenuIds); 115 | const deleteMenusRowIds = difference(originMenuIds, menus); 116 | // using transaction 117 | await this.entityManager.transaction(async (manager) => { 118 | // 菜单 119 | if (insertMenusRowIds.length > 0) { 120 | // 有条目更新 121 | const insertRows = insertMenusRowIds.map((e) => { 122 | return { 123 | roleId, 124 | menuId: e, 125 | }; 126 | }); 127 | await manager.insert(SysRoleMenu, insertRows); 128 | } 129 | if (deleteMenusRowIds.length > 0) { 130 | // 有条目需要删除 131 | const realDeleteRowIds = filter(originMenuRows, (e) => { 132 | return includes(deleteMenusRowIds, e.menuId); 133 | }).map((e) => { 134 | return e.id; 135 | }); 136 | await manager.delete(SysRoleMenu, realDeleteRowIds); 137 | } 138 | }); 139 | return role; 140 | } 141 | 142 | /** 143 | * 分页加载角色信息 144 | */ 145 | async page(dto: PageSearchRoleDto): Promise> { 146 | const { page, pageSize, name, value, status } = dto; 147 | const where = { 148 | ...(value ? { value: Like(`%${value}%`) } : null), 149 | ...(name ? { name: Like(`%${name}%`) } : null), 150 | ...(status ? { status: status } : null), 151 | }; 152 | const [items, total] = await this.roleRepository 153 | .createQueryBuilder('role') 154 | .where({ 155 | // id: Not(this.rootRoleId), 156 | ...where, 157 | }) 158 | .orderBy('role.id', 'ASC') 159 | .skip(pageSize * (page - 1)) 160 | .take(pageSize) 161 | .getManyAndCount(); 162 | 163 | return { items, total }; 164 | } 165 | 166 | /** 167 | * 根据用户id查找角色信息 168 | */ 169 | async getRoleIdByUser(id: number): Promise { 170 | const result = await this.userRoleRepository.findBy({ 171 | userId: id, 172 | }); 173 | if (!isEmpty(result)) { 174 | return map(result, (v) => { 175 | return v.roleId; 176 | }); 177 | } 178 | return []; 179 | } 180 | 181 | /** 182 | * 根据角色ID列表查找关联用户ID 183 | */ 184 | async countUserIdByRole(ids: number[]): Promise { 185 | if (includes(ids, this.rootRoleId)) { 186 | throw new Error('Not Support Delete Root'); 187 | } 188 | return await this.userRoleRepository.countBy({ roleId: In(ids) }); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/modules/admin/system/serve/serve.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class Runtime { 4 | @ApiProperty({ description: '系统' }) 5 | os?: string; 6 | 7 | @ApiProperty({ description: '服务器架构' }) 8 | arch?: string; 9 | 10 | @ApiProperty({ description: 'Node版本' }) 11 | nodeVersion?: string; 12 | 13 | @ApiProperty({ description: 'Npm版本' }) 14 | npmVersion?: string; 15 | } 16 | 17 | export class CoreLoad { 18 | @ApiProperty({ description: '当前CPU资源消耗' }) 19 | rawLoad?: number; 20 | 21 | @ApiProperty({ description: '当前空闲CPU资源' }) 22 | rawLoadIdle?: number; 23 | } 24 | 25 | // Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz 26 | export class Cpu { 27 | @ApiProperty({ description: '制造商 e.g. Intel(R)' }) 28 | manufacturer?: string; 29 | 30 | @ApiProperty({ description: '品牌 e.g. Core(TM)2 Duo' }) 31 | brand?: string; 32 | 33 | @ApiProperty({ description: '物理核心数' }) 34 | physicalCores?: number; 35 | 36 | @ApiProperty({ description: '型号' }) 37 | model?: string; 38 | 39 | @ApiProperty({ description: '速度 in GHz e.g. 3.4' }) 40 | speed?: number; 41 | 42 | @ApiProperty({ description: 'CPU资源消耗 原始滴答' }) 43 | rawCurrentLoad?: number; 44 | 45 | @ApiProperty({ description: '空闲CPU资源 原始滴答' }) 46 | rawCurrentLoadIdle?: number; 47 | 48 | @ApiProperty({ description: 'cpu资源消耗', type: [CoreLoad] }) 49 | coresLoad?: CoreLoad[]; 50 | } 51 | 52 | export class Disk { 53 | @ApiProperty({ description: '磁盘空间大小 (bytes)' }) 54 | size?: number; 55 | 56 | @ApiProperty({ description: '已使用磁盘空间 (bytes)' }) 57 | used?: number; 58 | 59 | @ApiProperty({ description: '可用磁盘空间 (bytes)' }) 60 | available?: number; 61 | } 62 | 63 | export class Memory { 64 | @ApiProperty({ description: 'total memory in bytes' }) 65 | total?: number; 66 | 67 | @ApiProperty({ description: '可用内存' }) 68 | available?: number; 69 | } 70 | 71 | /** 72 | * 系统信息 73 | */ 74 | export class ServeStatInfo { 75 | @ApiProperty({ description: '运行环境', type: Runtime }) 76 | runtime?: Runtime; 77 | 78 | @ApiProperty({ description: 'CPU信息', type: Cpu }) 79 | cpu?: Cpu; 80 | 81 | @ApiProperty({ description: '磁盘信息', type: Disk }) 82 | disk?: Disk; 83 | 84 | @ApiProperty({ description: '内存信息', type: Memory }) 85 | memory?: Memory; 86 | } 87 | -------------------------------------------------------------------------------- /src/modules/admin/system/serve/serve.controller.ts: -------------------------------------------------------------------------------- 1 | import { CacheInterceptor, Controller, Get, UseInterceptors } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; 3 | import { ADMIN_PREFIX } from '../../admin.constants'; 4 | import { PermissionOptional } from '../../core/decorators/permission-optional.decorator'; 5 | import { ServeStatInfo } from './serve.class'; 6 | import { SysServeService } from './serve.service'; 7 | 8 | @ApiSecurity(ADMIN_PREFIX) 9 | @ApiTags('服务监控') 10 | @Controller('serve') 11 | @UseInterceptors(CacheInterceptor) 12 | export class SysServeController { 13 | constructor(private serveService: SysServeService) {} 14 | 15 | @ApiOperation({ summary: '获取服务器运行信息' }) 16 | @ApiOkResponse({ type: ServeStatInfo }) 17 | @PermissionOptional() 18 | @Get('stat') 19 | async stat(): Promise { 20 | return await this.serveService.getServeStat(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/admin/system/serve/serve.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as si from 'systeminformation'; 3 | import { Disk, ServeStatInfo } from './serve.class'; 4 | 5 | @Injectable() 6 | export class SysServeService { 7 | /** 8 | * 获取服务器信息 9 | */ 10 | async getServeStat(): Promise { 11 | const [versions, osinfo, cpuinfo, currentLoadinfo, meminfo] = ( 12 | await Promise.allSettled([ 13 | si.versions('node, npm'), 14 | si.osInfo(), 15 | si.cpu(), 16 | si.currentLoad(), 17 | si.mem(), 18 | ]) 19 | ).map((p: any) => p.value); 20 | 21 | // 计算总空间 22 | const diskListInfo = await si.fsSize(); 23 | const diskinfo = new Disk(); 24 | diskinfo.size = 0; 25 | diskinfo.available = 0; 26 | diskinfo.used = 0; 27 | diskListInfo.forEach((d) => { 28 | diskinfo.size += d.size; 29 | diskinfo.available += d.available; 30 | diskinfo.used += d.used; 31 | }); 32 | 33 | return { 34 | runtime: { 35 | npmVersion: versions.npm, 36 | nodeVersion: versions.node, 37 | os: osinfo.platform, 38 | arch: osinfo.arch, 39 | }, 40 | cpu: { 41 | manufacturer: cpuinfo.manufacturer, 42 | brand: cpuinfo.brand, 43 | physicalCores: cpuinfo.physicalCores, 44 | model: cpuinfo.model, 45 | speed: cpuinfo.speed, 46 | rawCurrentLoad: currentLoadinfo.rawCurrentLoad, 47 | rawCurrentLoadIdle: currentLoadinfo.rawCurrentLoadIdle, 48 | coresLoad: currentLoadinfo.cpus.map((e) => { 49 | return { 50 | rawLoad: e.rawLoad, 51 | rawLoadIdle: e.rawLoadIdle, 52 | }; 53 | }), 54 | }, 55 | disk: diskinfo, 56 | memory: { 57 | total: meminfo.total, 58 | available: meminfo.available, 59 | }, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/admin/system/system.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { 4 | ROOT_ROLE_ID, 5 | SYS_TASK_QUEUE_NAME, 6 | SYS_TASK_QUEUE_PREFIX, 7 | } from '@/modules/admin/admin.constants'; 8 | import SysLoginLog from '@/entities/admin/sys-login-log.entity'; 9 | import SysMenu from '@/entities/admin/sys-menu.entity'; 10 | import SysRoleMenu from '@/entities/admin/sys-role-menu.entity'; 11 | import SysRole from '@/entities/admin/sys-role.entity'; 12 | import SysTaskLog from '@/entities/admin/sys-task-log.entity'; 13 | import SysTask from '@/entities/admin/sys-task.entity'; 14 | import SysUserRole from '@/entities/admin/sys-user-role.entity'; 15 | import SysUser from '@/entities/admin/sys-user.entity'; 16 | import { rootRoleIdProvider } from '../core/provider/root-role-id.provider'; 17 | import { SysLogController } from './log/log.controller'; 18 | import { SysLogService } from './log/log.service'; 19 | import { SysMenuController } from './menu/menu.controller'; 20 | import { SysMenuService } from './menu/menu.service'; 21 | import { SysRoleController } from './role/role.controller'; 22 | import { SysRoleService } from './role/role.service'; 23 | import { SysUserController } from './user/user.controller'; 24 | import { SysUserService } from './user/user.service'; 25 | import { BullModule } from '@nestjs/bull'; 26 | import { SysTaskController } from './task/task.controller'; 27 | import { SysTaskService } from './task/task.service'; 28 | import { ConfigModule, ConfigService } from '@nestjs/config'; 29 | import { SysTaskConsumer } from './task/task.processor'; 30 | import { SysOnlineController } from './online/online.controller'; 31 | import { SysOnlineService } from './online/online.service'; 32 | import { WSModule } from '@/modules/ws/ws.module'; 33 | import SysConfig from '@/entities/admin/sys-config.entity'; 34 | import { SysParamConfigController } from './param-config/param-config.controller'; 35 | import { SysParamConfigService } from './param-config/param-config.service'; 36 | import { SysServeController } from './serve/serve.controller'; 37 | import { SysServeService } from './serve/serve.service'; 38 | 39 | @Module({ 40 | imports: [ 41 | TypeOrmModule.forFeature([ 42 | SysUser, 43 | SysUserRole, 44 | SysMenu, 45 | SysRoleMenu, 46 | SysRole, 47 | SysLoginLog, 48 | SysTask, 49 | SysTaskLog, 50 | SysConfig, 51 | ]), 52 | BullModule.registerQueueAsync({ 53 | name: SYS_TASK_QUEUE_NAME, 54 | imports: [ConfigModule], 55 | useFactory: (configService: ConfigService) => ({ 56 | redis: { 57 | host: configService.get('redis.host'), 58 | port: configService.get('redis.port'), 59 | password: configService.get('redis.password'), 60 | db: configService.get('redis.db'), 61 | }, 62 | prefix: SYS_TASK_QUEUE_PREFIX, 63 | }), 64 | inject: [ConfigService], 65 | }), 66 | WSModule, 67 | ], 68 | controllers: [ 69 | SysUserController, 70 | SysRoleController, 71 | SysMenuController, 72 | SysLogController, 73 | SysTaskController, 74 | SysOnlineController, 75 | SysParamConfigController, 76 | SysServeController, 77 | ], 78 | providers: [ 79 | rootRoleIdProvider(), 80 | SysUserService, 81 | SysRoleService, 82 | SysMenuService, 83 | SysLogService, 84 | SysTaskService, 85 | SysTaskConsumer, 86 | SysOnlineService, 87 | SysParamConfigService, 88 | SysServeService, 89 | ], 90 | exports: [ 91 | ROOT_ROLE_ID, 92 | TypeOrmModule, 93 | SysUserService, 94 | SysMenuService, 95 | SysLogService, 96 | SysOnlineService, 97 | SysParamConfigService, 98 | ], 99 | }) 100 | export class SystemModule {} 101 | -------------------------------------------------------------------------------- /src/modules/admin/system/task/task.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; 3 | import { isEmpty } from 'lodash'; 4 | import { PageResult } from '@/common/class/res.class'; 5 | import { ApiException } from '@/common/exceptions/api.exception'; 6 | import SysTask from '@/entities/admin/sys-task.entity'; 7 | import { ADMIN_PREFIX } from '../../admin.constants'; 8 | import { CheckIdTaskDto, CreateTaskDto, UpdateTaskDto, PageSearchTaskDto } from './task.dto'; 9 | import { SysTaskService } from './task.service'; 10 | 11 | @ApiSecurity(ADMIN_PREFIX) 12 | @ApiTags('任务调度模块') 13 | @Controller('task') 14 | export class SysTaskController { 15 | constructor(private taskService: SysTaskService) {} 16 | 17 | @ApiOperation({ summary: '获取任务列表' }) 18 | @ApiOkResponse({ type: [SysTask] }) 19 | @Get('page') 20 | async page(@Query() dto: PageSearchTaskDto): Promise> { 21 | return await this.taskService.page(dto); 22 | } 23 | 24 | @ApiOperation({ summary: '添加任务' }) 25 | @Post('add') 26 | async add(@Body() dto: CreateTaskDto): Promise { 27 | const serviceCall = dto.service.split('.'); 28 | await this.taskService.checkHasMissionMeta(serviceCall[0], serviceCall[1]); 29 | await this.taskService.addOrUpdate(dto); 30 | } 31 | 32 | @ApiOperation({ summary: '更新任务' }) 33 | @Post('update') 34 | async update(@Body() dto: UpdateTaskDto): Promise { 35 | const serviceCall = dto.service.split('.'); 36 | await this.taskService.checkHasMissionMeta(serviceCall[0], serviceCall[1]); 37 | await this.taskService.addOrUpdate(dto); 38 | } 39 | 40 | @ApiOperation({ summary: '查询任务详细信息' }) 41 | @ApiOkResponse({ type: SysTask }) 42 | @Get('info') 43 | async info(@Query() dto: CheckIdTaskDto): Promise { 44 | return await this.taskService.info(dto.id); 45 | } 46 | 47 | @ApiOperation({ summary: '手动执行一次任务' }) 48 | @Post('once') 49 | async once(@Body() dto: CheckIdTaskDto): Promise { 50 | const task = await this.taskService.info(dto.id); 51 | if (!isEmpty(task)) { 52 | await this.taskService.once(task); 53 | } else { 54 | throw new ApiException(10020); 55 | } 56 | } 57 | 58 | @ApiOperation({ summary: '停止任务' }) 59 | @Post('stop') 60 | async stop(@Body() dto: CheckIdTaskDto): Promise { 61 | const task = await this.taskService.info(dto.id); 62 | if (!isEmpty(task)) { 63 | await this.taskService.stop(task); 64 | } else { 65 | throw new ApiException(10020); 66 | } 67 | } 68 | 69 | @ApiOperation({ summary: '启动任务' }) 70 | @Post('start') 71 | async start(@Body() dto: CheckIdTaskDto): Promise { 72 | const task = await this.taskService.info(dto.id); 73 | if (!isEmpty(task)) { 74 | await this.taskService.start(task); 75 | } else { 76 | throw new ApiException(10020); 77 | } 78 | } 79 | 80 | @ApiOperation({ summary: '删除任务' }) 81 | @Post('delete') 82 | async delete(@Body() dto: CheckIdTaskDto): Promise { 83 | const task = await this.taskService.info(dto.id); 84 | if (!isEmpty(task)) { 85 | await this.taskService.delete(task); 86 | } else { 87 | throw new ApiException(10020); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/modules/admin/system/task/task.dto.ts: -------------------------------------------------------------------------------- 1 | import * as parser from 'cron-parser'; 2 | import { isEmpty } from 'lodash'; 3 | import { 4 | IsDateString, 5 | IsIn, 6 | IsInt, 7 | IsOptional, 8 | IsString, 9 | MaxLength, 10 | Min, 11 | MinLength, 12 | Validate, 13 | ValidateIf, 14 | ValidationArguments, 15 | ValidatorConstraint, 16 | ValidatorConstraintInterface, 17 | } from 'class-validator'; 18 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 19 | import { Type } from 'class-transformer'; 20 | import { PaginateDto } from '@/common/dto/page.dto'; 21 | 22 | // cron 表达式验证,bull lib下引用了cron-parser 23 | @ValidatorConstraint({ name: 'isCronExpression', async: false }) 24 | export class IsCronExpression implements ValidatorConstraintInterface { 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 26 | validate(value: string, args: ValidationArguments) { 27 | try { 28 | if (isEmpty(value)) { 29 | throw new Error('cron expression is empty'); 30 | } 31 | parser.parseExpression(value); 32 | return true; 33 | } catch (e) { 34 | return false; 35 | } 36 | } 37 | 38 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 39 | defaultMessage(_args: ValidationArguments) { 40 | // here you can provide default error message if validation failed 41 | return 'this cron expression ($value) invalid'; 42 | } 43 | } 44 | 45 | export class CreateTaskDto { 46 | @ApiProperty({ description: '任务名称' }) 47 | @IsString() 48 | @MinLength(2) 49 | @MaxLength(50) 50 | name: string; 51 | 52 | @ApiProperty({ description: '调用的服务' }) 53 | @IsString() 54 | @MinLength(1) 55 | service: string; 56 | 57 | @ApiProperty({ description: '任务类别:cron | interval' }) 58 | @IsIn([0, 1]) 59 | type: number; 60 | 61 | @ApiProperty({ description: '任务状态' }) 62 | @IsIn([0, 1]) 63 | status: number; 64 | 65 | @ApiPropertyOptional({ description: '开始时间', type: Date }) 66 | @IsDateString() 67 | @ValidateIf((o) => !isEmpty(o.startTime)) 68 | startTime: string; 69 | 70 | @ApiPropertyOptional({ description: '结束时间', type: Date }) 71 | @IsDateString() 72 | @ValidateIf((o) => !isEmpty(o.endTime)) 73 | endTime: string; 74 | 75 | @ApiPropertyOptional({ description: '限制执行次数,负数则无限制' }) 76 | @IsInt() 77 | @IsOptional() 78 | readonly limit: number = -1; 79 | 80 | @ApiProperty({ description: 'cron表达式' }) 81 | @Validate(IsCronExpression) 82 | @ValidateIf((o) => o.type === 0) 83 | cron: string; 84 | 85 | @ApiProperty({ description: '执行间隔,毫秒单位' }) 86 | @IsInt() 87 | @Min(100) 88 | @ValidateIf((o) => o.type === 1) 89 | every: number; 90 | 91 | @ApiPropertyOptional({ description: '执行参数' }) 92 | @IsString() 93 | @IsOptional() 94 | data: string; 95 | 96 | @ApiPropertyOptional({ description: '任务备注' }) 97 | @IsOptional() 98 | @IsString() 99 | remark: string; 100 | } 101 | 102 | export class UpdateTaskDto extends CreateTaskDto { 103 | @ApiProperty({ description: '需要更新的任务ID' }) 104 | @IsInt() 105 | @Min(0) 106 | id: number; 107 | } 108 | 109 | export class CheckIdTaskDto { 110 | @ApiProperty({ description: '任务ID' }) 111 | @IsInt() 112 | @Min(0) 113 | @Type(() => Number) 114 | id: number; 115 | } 116 | 117 | export class PageSearchTaskDto extends PaginateDto { 118 | @ApiProperty({ description: '任务名称' }) 119 | @IsOptional() 120 | @IsString() 121 | name: string; 122 | 123 | @ApiProperty({ description: '调用的服务' }) 124 | @IsString() 125 | @IsOptional() 126 | @MinLength(1) 127 | service: string; 128 | 129 | @ApiProperty({ description: '任务类别:cron | interval' }) 130 | @IsOptional() 131 | @IsIn([0, 1]) 132 | type: number; 133 | 134 | @ApiProperty({ description: '任务状态' }) 135 | @IsOptional() 136 | @IsIn([0, 1]) 137 | status: number; 138 | } 139 | -------------------------------------------------------------------------------- /src/modules/admin/system/task/task.processor.ts: -------------------------------------------------------------------------------- 1 | import { OnQueueCompleted, Process, Processor } from '@nestjs/bull'; 2 | import { Job } from 'bull'; 3 | import { SYS_TASK_QUEUE_NAME } from '../../admin.constants'; 4 | import { SysLogService } from '../log/log.service'; 5 | import { SysTaskService } from './task.service'; 6 | 7 | export interface ExecuteData { 8 | id: number; 9 | args?: string | null; 10 | service: string; 11 | } 12 | 13 | @Processor(SYS_TASK_QUEUE_NAME) 14 | export class SysTaskConsumer { 15 | constructor(private taskService: SysTaskService, private taskLogService: SysLogService) {} 16 | 17 | @Process() 18 | async handle(job: Job): Promise { 19 | const startTime = Date.now(); 20 | const { data } = job; 21 | try { 22 | await this.taskService.callService(data.service, data.args); 23 | const timing = Date.now() - startTime; 24 | // 任务执行成功 25 | await this.taskLogService.recordTaskLog(data.id, 1, timing); 26 | } catch (e) { 27 | const timing = Date.now() - startTime; 28 | // 执行失败 29 | await this.taskLogService.recordTaskLog(data.id, 0, timing, `${e}`); 30 | } 31 | } 32 | 33 | @OnQueueCompleted() 34 | onCompleted(job: Job) { 35 | this.taskService.updateTaskCompleteStatus(job.data.id); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/admin/system/task/task.service.ts: -------------------------------------------------------------------------------- 1 | import { InjectQueue } from '@nestjs/bull'; 2 | import { Injectable, OnModuleInit } from '@nestjs/common'; 3 | import { ModuleRef, Reflector } from '@nestjs/core'; 4 | import { UnknownElementException } from '@nestjs/core/errors/exceptions/unknown-element.exception'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { Queue } from 'bull'; 7 | import { isEmpty } from 'lodash'; 8 | import { MISSION_KEY_METADATA } from '@/common/contants/decorator.contants'; 9 | import { ApiException } from '@/common/exceptions/api.exception'; 10 | import SysTask from '@/entities/admin/sys-task.entity'; 11 | import { LoggerService } from '@/shared/logger/logger.service'; 12 | import { RedisService } from '@/shared/services/redis.service'; 13 | import { Like, Repository } from 'typeorm'; 14 | import { SYS_TASK_QUEUE_NAME, SYS_TASK_QUEUE_PREFIX } from '../../admin.constants'; 15 | import { CreateTaskDto, PageSearchTaskDto, UpdateTaskDto } from './task.dto'; 16 | import { PageResult } from '@/common/class/res.class'; 17 | 18 | @Injectable() 19 | export class SysTaskService implements OnModuleInit { 20 | constructor( 21 | @InjectRepository(SysTask) private taskRepository: Repository, 22 | @InjectQueue(SYS_TASK_QUEUE_NAME) private taskQueue: Queue, 23 | private moduleRef: ModuleRef, 24 | private reflector: Reflector, 25 | private redisService: RedisService, 26 | private logger: LoggerService, 27 | ) {} 28 | 29 | /** 30 | * module init 31 | */ 32 | async onModuleInit() { 33 | await this.initTask(); 34 | } 35 | 36 | /** 37 | * 初始化任务,系统启动前调用 38 | */ 39 | async initTask(): Promise { 40 | const initKey = `${SYS_TASK_QUEUE_PREFIX}:init`; 41 | // 防止重复初始化 42 | const result = await this.redisService 43 | .getRedis() 44 | .multi() 45 | .setnx(initKey, new Date().getTime()) 46 | .expire(initKey, 60 * 30) 47 | .exec(); 48 | if (result[0][1] === 0) { 49 | // 存在锁则直接跳过防止重复初始化 50 | this.logger.log('Init task is lock', SysTaskService.name); 51 | return; 52 | } 53 | const jobs = await this.taskQueue.getJobs([ 54 | 'active', 55 | 'delayed', 56 | 'failed', 57 | 'paused', 58 | 'waiting', 59 | 'completed', 60 | ]); 61 | for (let i = 0; i < jobs.length; i++) { 62 | // 先移除所有已存在的任务 63 | await jobs[i].remove(); 64 | } 65 | // 查找所有需要运行的任务 66 | const tasks = await this.taskRepository.findBy({ status: 1 }); 67 | if (tasks && tasks.length > 0) { 68 | for (const t of tasks) { 69 | await this.start(t); 70 | } 71 | } 72 | // 启动后释放锁 73 | await this.redisService.getRedis().del(initKey); 74 | } 75 | 76 | /** 77 | * 分页查询 78 | */ 79 | async page(dto: PageSearchTaskDto): Promise> { 80 | const { page, pageSize, name, service, type, status } = dto; 81 | const where = { 82 | ...(name ? { name: Like(`%${name}%`) } : null), 83 | ...(service ? { service: Like(`%${service}%`) } : null), 84 | ...(type ? { type: type } : null), 85 | ...(status ? { status: status } : null), 86 | }; 87 | 88 | const [items, total] = await this.taskRepository 89 | .createQueryBuilder('task') 90 | .where(where) 91 | .orderBy('task.id', 'ASC') 92 | .skip(pageSize * (page - 1)) 93 | .take(pageSize) 94 | .getManyAndCount(); 95 | 96 | return { items, total }; 97 | } 98 | 99 | /** 100 | * count task 101 | */ 102 | async count(): Promise { 103 | return await this.taskRepository.count(); 104 | } 105 | 106 | /** 107 | * task info 108 | */ 109 | async info(id: number): Promise { 110 | return await this.taskRepository.findOneBy({ id }); 111 | } 112 | 113 | /** 114 | * delete task 115 | */ 116 | async delete(task: SysTask): Promise { 117 | if (!task) { 118 | throw new Error('Task is Empty'); 119 | } 120 | await this.stop(task); 121 | await this.taskRepository.delete(task.id); 122 | } 123 | 124 | /** 125 | * 手动执行一次 126 | */ 127 | async once(task: SysTask): Promise { 128 | if (task) { 129 | await this.taskQueue.add( 130 | { id: task.id, service: task.service, args: task.data }, 131 | { jobId: task.id, removeOnComplete: true, removeOnFail: true }, 132 | ); 133 | } else { 134 | throw new Error('Task is Empty'); 135 | } 136 | } 137 | 138 | /** 139 | * 添加任务 140 | */ 141 | async addOrUpdate(param: CreateTaskDto | UpdateTaskDto): Promise { 142 | const result = await this.taskRepository.save(param); 143 | const task = await this.info(result.id); 144 | if (result.status === 0) { 145 | await this.stop(task); 146 | } else if (result.status === 1) { 147 | await this.start(task); 148 | } 149 | } 150 | 151 | /** 152 | * 启动任务 153 | */ 154 | async start(task: SysTask): Promise { 155 | if (!task) { 156 | throw new Error('Task is Empty'); 157 | } 158 | // 先停掉之前存在的任务 159 | await this.stop(task); 160 | let repeat: any; 161 | if (task.type === 1) { 162 | // 间隔 Repeat every millis (cron setting cannot be used together with this setting.) 163 | repeat = { 164 | every: task.every, 165 | }; 166 | } else { 167 | // cron 168 | repeat = { 169 | cron: task.cron, 170 | }; 171 | // Start date when the repeat job should start repeating (only with cron). 172 | if (task.startTime) { 173 | repeat.startDate = task.startTime; 174 | } 175 | if (task.endTime) { 176 | repeat.endDate = task.endTime; 177 | } 178 | } 179 | if (task.limit > 0) { 180 | repeat.limit = task.limit; 181 | } 182 | const job = await this.taskQueue.add( 183 | { id: task.id, service: task.service, args: task.data }, 184 | { jobId: task.id, removeOnComplete: true, removeOnFail: true, repeat }, 185 | ); 186 | if (job && job.opts) { 187 | await this.taskRepository.update(task.id, { 188 | jobOpts: JSON.stringify(job.opts.repeat), 189 | status: 1, 190 | }); 191 | } else { 192 | // update status to 0,标识暂停任务,因为启动失败 193 | job && (await job.remove()); 194 | await this.taskRepository.update(task.id, { status: 0 }); 195 | throw new Error('Task Start failed'); 196 | } 197 | } 198 | 199 | /** 200 | * 停止任务 201 | */ 202 | async stop(task: SysTask): Promise { 203 | if (!task) { 204 | throw new Error('Task is Empty'); 205 | } 206 | const exist = await this.existJob(task.id.toString()); 207 | if (!exist) { 208 | await this.taskRepository.update(task.id, { status: 0 }); 209 | return; 210 | } 211 | const jobs = await this.taskQueue.getJobs([ 212 | 'active', 213 | 'delayed', 214 | 'failed', 215 | 'paused', 216 | 'waiting', 217 | 'completed', 218 | ]); 219 | for (let i = 0; i < jobs.length; i++) { 220 | if (jobs[i].data.id === task.id) { 221 | await jobs[i].remove(); 222 | } 223 | } 224 | await this.taskRepository.update(task.id, { status: 0 }); 225 | // if (task.jobOpts) { 226 | // await this.app.queue.sys.removeRepeatable(JSON.parse(task.jobOpts)); 227 | // // update status 228 | // await this.getRepo().admin.sys.Task.update(task.id, { status: 0 }); 229 | // } 230 | } 231 | 232 | /** 233 | * 查看队列中任务是否存在 234 | */ 235 | async existJob(jobId: string): Promise { 236 | // https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md#queueremoverepeatablebykey 237 | const jobs = await this.taskQueue.getRepeatableJobs(); 238 | const ids = jobs.map((e) => { 239 | return e.id; 240 | }); 241 | return ids.includes(jobId); 242 | } 243 | 244 | /** 245 | * 更新是否已经完成,完成则移除该任务并修改状态 246 | */ 247 | async updateTaskCompleteStatus(tid: number): Promise { 248 | const jobs = await this.taskQueue.getRepeatableJobs(); 249 | const task = await this.taskRepository.findOneBy({ id: tid }); 250 | // 如果下次执行时间小于当前时间,则表示已经执行完成。 251 | for (const job of jobs) { 252 | const currentTime = new Date().getTime(); 253 | if (job.id === tid.toString() && job.next < currentTime) { 254 | // 如果下次执行时间小于当前时间,则表示已经执行完成。 255 | await this.stop(task); 256 | break; 257 | } 258 | } 259 | } 260 | 261 | /** 262 | * 检测service是否有注解定义 263 | * @param serviceName service 264 | */ 265 | async checkHasMissionMeta(nameOrInstance: string | unknown, exec: string): Promise { 266 | try { 267 | let service: any; 268 | if (typeof nameOrInstance === 'string') { 269 | service = await this.moduleRef.get(nameOrInstance, { strict: false }); 270 | } else { 271 | service = nameOrInstance; 272 | } 273 | // 所执行的任务不存在 274 | if (!service || !(exec in service)) { 275 | throw new ApiException(10102); 276 | } 277 | // 检测是否有Mission注解 278 | const hasMission = this.reflector.get( 279 | MISSION_KEY_METADATA, 280 | // https://github.com/nestjs/nest/blob/e5f0815da52ce22e5077c461fe881e89c4b5d640/packages/core/helpers/context-utils.ts#L90 281 | service.constructor, 282 | ); 283 | // 如果没有,则抛出错误 284 | if (!hasMission) { 285 | throw new ApiException(10101); 286 | } 287 | } catch (e) { 288 | if (e instanceof UnknownElementException) { 289 | // 任务不存在 290 | throw new ApiException(10102); 291 | } else { 292 | // 其余错误则不处理,继续抛出 293 | throw e; 294 | } 295 | } 296 | } 297 | 298 | /** 299 | * 根据serviceName调用service,例如 SysLogService.clearReqLog 300 | */ 301 | async callService(serviceName: string, args: string): Promise { 302 | if (serviceName) { 303 | const arr = serviceName.split('.'); 304 | if (arr.length < 1) { 305 | throw new Error('serviceName define error'); 306 | } 307 | const methodName = arr[1]; 308 | const service = await this.moduleRef.get(arr[0], { strict: false }); 309 | // 安全注解检查 310 | await this.checkHasMissionMeta(service, methodName); 311 | if (isEmpty(args)) { 312 | await service[methodName](); 313 | } else { 314 | // 参数安全判断 315 | const parseArgs = this.safeParse(args); 316 | 317 | if (Array.isArray(parseArgs)) { 318 | // 数组形式则自动扩展成方法参数回掉 319 | await service[methodName](...parseArgs); 320 | } else { 321 | await service[methodName](parseArgs); 322 | } 323 | } 324 | } 325 | } 326 | 327 | safeParse(args: string): unknown | string { 328 | try { 329 | return JSON.parse(args); 330 | } catch (e) { 331 | return args; 332 | } 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/modules/admin/system/user/user.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import SysUser from '@/entities/admin/sys-user.entity'; 3 | 4 | export class AccountInfo { 5 | @ApiProperty() 6 | username: string; 7 | 8 | @ApiProperty() 9 | nickName: string; 10 | 11 | @ApiProperty() 12 | email: string; 13 | 14 | @ApiProperty() 15 | phone: string; 16 | 17 | @ApiProperty() 18 | remark: string; 19 | 20 | @ApiProperty() 21 | avatar: string; 22 | 23 | @ApiProperty() 24 | qq: string; 25 | } 26 | 27 | export class PageSearchUserInfo { 28 | @ApiProperty() 29 | createdAt: string; 30 | 31 | @ApiProperty() 32 | email: string; 33 | 34 | @ApiProperty() 35 | qq: string; 36 | 37 | @ApiProperty() 38 | avatar: string; 39 | 40 | @ApiProperty() 41 | id: number; 42 | 43 | @ApiProperty() 44 | name: string; 45 | 46 | @ApiProperty() 47 | nickName: string; 48 | 49 | @ApiProperty() 50 | phone: string; 51 | 52 | @ApiProperty() 53 | remark: string; 54 | 55 | @ApiProperty() 56 | status: number; 57 | 58 | @ApiProperty() 59 | updatedAt: string; 60 | 61 | @ApiProperty() 62 | username: string; 63 | 64 | @ApiProperty() 65 | departmentName: string; 66 | 67 | @ApiProperty({ 68 | type: [String], 69 | }) 70 | roleNames: string[]; 71 | } 72 | 73 | export class UserDetailInfo extends SysUser { 74 | @ApiProperty({ 75 | description: '关联角色', 76 | }) 77 | roles: number[]; 78 | } 79 | -------------------------------------------------------------------------------- /src/modules/admin/system/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | import { ApiOkResponse, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger'; 3 | import { PageResult } from '@/common/class/res.class'; 4 | import { ADMIN_PREFIX } from '../../admin.constants'; 5 | import { IAdminUser } from '../../admin.interface'; 6 | import { AdminUser } from '../../core/decorators/admin-user.decorator'; 7 | import { 8 | CreateUserDto, 9 | DeleteUserDto, 10 | InfoUserDto, 11 | PageSearchUserDto, 12 | PasswordUserDto, 13 | UpdateUserDto, 14 | UserExistDto, 15 | } from './user.dto'; 16 | import { PageSearchUserInfo, UserDetailInfo } from './user.class'; 17 | import { SysUserService } from './user.service'; 18 | import { SysMenuService } from '../menu/menu.service'; 19 | 20 | @ApiSecurity(ADMIN_PREFIX) 21 | @ApiTags('用户模块') 22 | @Controller('user') 23 | export class SysUserController { 24 | constructor(private userService: SysUserService, private menuService: SysMenuService) {} 25 | 26 | @ApiOperation({ summary: '新增用户' }) 27 | @Post('add') 28 | async add(@Body() dto: CreateUserDto): Promise { 29 | await this.userService.add(dto); 30 | } 31 | 32 | @ApiOperation({ summary: '查询用户信息' }) 33 | @ApiOkResponse({ type: UserDetailInfo }) 34 | @Get('info') 35 | async info(@Query() dto: InfoUserDto): Promise { 36 | return await this.userService.info(dto.id); 37 | } 38 | 39 | @ApiOperation({ summary: '根据ID删除用户' }) 40 | @Post('delete') 41 | async delete(@Body() dto: DeleteUserDto): Promise { 42 | await this.userService.delete(dto.ids); 43 | await this.userService.multiForbidden(dto.ids); 44 | } 45 | 46 | @ApiOperation({ summary: '获取用户列表' }) 47 | @ApiOkResponse({ type: [PageSearchUserInfo] }) 48 | @Get('list') 49 | async list( 50 | @Query() dto: PageSearchUserDto, 51 | @AdminUser() user: IAdminUser, 52 | ): Promise> { 53 | return await this.userService.page(dto); 54 | } 55 | 56 | @ApiOperation({ summary: '更新用户信息' }) 57 | @Post('update') 58 | async update(@Body() dto: UpdateUserDto): Promise { 59 | await this.userService.update(dto); 60 | await this.menuService.refreshPerms(dto.id); 61 | } 62 | 63 | @ApiOperation({ summary: '更改指定用户密码' }) 64 | @Post('password') 65 | async password(@Body() dto: PasswordUserDto): Promise { 66 | await this.userService.forceUpdatePassword(dto.id, dto.password); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/modules/admin/system/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { Type } from 'class-transformer'; 3 | import { 4 | ArrayMaxSize, 5 | ArrayMinSize, 6 | ArrayNotEmpty, 7 | IsArray, 8 | IsEmail, 9 | IsIn, 10 | IsInt, 11 | IsOptional, 12 | IsString, 13 | Matches, 14 | MaxLength, 15 | Min, 16 | MinLength, 17 | ValidateIf, 18 | } from 'class-validator'; 19 | import { isEmpty } from 'lodash'; 20 | import { PaginateDto } from '@/common/dto/page.dto'; 21 | 22 | export class UpdateUserInfoDto { 23 | @ApiProperty({ description: '用户呢称' }) 24 | @IsString() 25 | @IsOptional() 26 | nickName: string; 27 | 28 | @ApiProperty({ description: '用户邮箱' }) 29 | @IsEmail() 30 | @ValidateIf((o) => !isEmpty(o.email)) 31 | email: string; 32 | 33 | @ApiProperty({ description: '用户QQ' }) 34 | @IsString() 35 | @Matches(/^[0-9]+$/) 36 | @MinLength(5) 37 | @MaxLength(11) 38 | @IsOptional() 39 | qq: string; 40 | 41 | @ApiProperty({ description: '用户手机号' }) 42 | @IsString() 43 | @IsOptional() 44 | phone: string; 45 | 46 | @ApiProperty({ description: '用户头像' }) 47 | @IsString() 48 | @IsOptional() 49 | avatar: string; 50 | 51 | @ApiProperty({ description: '用户备注' }) 52 | @IsString() 53 | @IsOptional() 54 | remark: string; 55 | } 56 | 57 | export class UpdatePasswordDto { 58 | @ApiProperty({ description: '旧密码' }) 59 | @IsString() 60 | @Matches(/^[a-z0-9A-Z\W_]+$/) 61 | @MinLength(6) 62 | @MaxLength(20) 63 | oldPassword: string; 64 | 65 | @ApiProperty({ description: '新密码' }) 66 | @Matches(/(?!^\d+$)(?!^[A-Za-z]+$)(?!^[^A-Za-z0-9]+$)(?!^.*[\u4E00-\u9FA5].*$)^\S{6,16}$/, { 67 | message: '密码必须包含数字、字母、特殊字符,长度为6-16', 68 | }) 69 | newPassword: string; 70 | } 71 | 72 | export class CreateUserDto { 73 | @ApiProperty({ description: '登录账号' }) 74 | @IsString() 75 | @Matches(/^[a-z0-9A-Z\W_]+$/) 76 | @MinLength(4) 77 | @MaxLength(20) 78 | username: string; 79 | 80 | @ApiProperty({ description: '登录密码' }) 81 | @IsOptional() 82 | @Matches(/(?!^\d+$)(?!^[A-Za-z]+$)(?!^[^A-Za-z0-9]+$)(?!^.*[\u4E00-\u9FA5].*$)^\S{6,16}$/, { 83 | message: '密码必须包含数字、字母、特殊字符,长度为6-16', 84 | }) 85 | password: string; 86 | 87 | @ApiProperty({ description: '归属角色', type: [Number] }) 88 | @ArrayNotEmpty() 89 | @ArrayMinSize(1) 90 | @ArrayMaxSize(3) 91 | roles: number[]; 92 | 93 | @ApiProperty({ description: '呢称' }) 94 | @IsString() 95 | @IsOptional() 96 | nickName: string; 97 | 98 | @ApiProperty({ description: '邮箱' }) 99 | @IsEmail() 100 | @ValidateIf((o) => !isEmpty(o.email)) 101 | email: string; 102 | 103 | @ApiProperty({ description: '手机号' }) 104 | @IsString() 105 | @IsOptional() 106 | phone: string; 107 | 108 | @ApiProperty({ description: 'QQ' }) 109 | @IsString() 110 | @Matches(/^[0-9]+$/) 111 | @MinLength(5) 112 | @MaxLength(11) 113 | @IsOptional() 114 | qq: string; 115 | 116 | @ApiProperty({ description: '备注' }) 117 | @IsString() 118 | @IsOptional() 119 | remark: string; 120 | 121 | @ApiProperty({ description: '状态' }) 122 | @IsIn([0, 1]) 123 | status: number; 124 | } 125 | 126 | export class UpdateUserDto extends CreateUserDto { 127 | @ApiProperty({ description: '用户ID' }) 128 | @IsInt() 129 | @Min(0) 130 | id: number; 131 | } 132 | 133 | export class InfoUserDto { 134 | @ApiProperty({ description: '用户ID' }) 135 | @IsInt() 136 | @Min(0) 137 | @Type(() => Number) 138 | id: number; 139 | } 140 | 141 | export class DeleteUserDto { 142 | @ApiProperty({ description: '需要删除的用户ID列表', type: [Number] }) 143 | @IsArray() 144 | @ArrayNotEmpty() 145 | ids: number[]; 146 | } 147 | 148 | export class PageSearchUserDto extends PaginateDto { 149 | @ApiProperty({ description: '用户名' }) 150 | @IsOptional() 151 | @IsString() 152 | username: string; 153 | 154 | @ApiProperty({ description: '昵称' }) 155 | @IsString() 156 | @IsOptional() 157 | nickName: string; 158 | 159 | @ApiProperty({ description: 'qq' }) 160 | @IsString() 161 | @Matches(/^[0-9]+$/) 162 | @MinLength(5) 163 | @MaxLength(11) 164 | @IsOptional() 165 | qq: string; 166 | 167 | @ApiProperty({ description: '邮箱' }) 168 | @IsString() 169 | @IsOptional() 170 | email: string; 171 | 172 | @ApiProperty({ description: '状态' }) 173 | @IsOptional() 174 | status: number; 175 | } 176 | 177 | export class PasswordUserDto { 178 | @ApiProperty({ description: '管理员ID' }) 179 | @IsInt() 180 | @Min(0) 181 | id: number; 182 | 183 | @ApiProperty({ description: '更改后的密码' }) 184 | @Matches(/^[a-z0-9A-Z`~!#%^&*=+\\|{};:'\\",<>/?]{6,16}$/, { message: '密码格式不正确' }) 185 | password: string; 186 | } 187 | 188 | export class UserExistDto { 189 | @ApiProperty({ description: '登录账号' }) 190 | @IsString() 191 | @Matches(/^[a-z0-9A-Z]+$/) 192 | @MinLength(6) 193 | @MaxLength(20) 194 | username: string; 195 | } 196 | -------------------------------------------------------------------------------- /src/modules/admin/tools/email/email.controller.ts: -------------------------------------------------------------------------------- 1 | import { EmailService } from '@/shared/services/email.service'; 2 | import { Body, Controller, Post } from '@nestjs/common'; 3 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | 5 | import { SendEmailDto } from './email.dto'; 6 | 7 | @ApiTags('邮箱模块') 8 | @Controller('email') 9 | export class EmailController { 10 | constructor(private emailService: EmailService) {} 11 | 12 | @ApiOperation({ summary: '发送邮件' }) 13 | @Post('send') 14 | async send(@Body() dto: SendEmailDto): Promise { 15 | const { to, subject, content } = dto; 16 | await this.emailService.sendMailHtml(to, subject, content); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/admin/tools/email/email.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsOptional, IsString } from 'class-validator'; 3 | 4 | /** 5 | * 发送邮件 6 | */ 7 | export class SendEmailDto { 8 | @ApiProperty({ description: '收件人邮箱' }) 9 | @IsString() 10 | to: string; 11 | 12 | @ApiProperty({ description: '标题' }) 13 | @IsString() 14 | subject: number; 15 | 16 | @ApiProperty({ description: '正文' }) 17 | @IsString() 18 | content: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/admin/tools/storage/storage.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class StorageInfo { 4 | @ApiProperty({ description: '文件ID' }) 5 | id: number; 6 | 7 | @ApiProperty({ description: '文件名' }) 8 | name: string; 9 | 10 | @ApiProperty({ description: '文件扩展名' }) 11 | extName: string; 12 | 13 | @ApiProperty({ description: '文件路径' }) 14 | path: string; 15 | 16 | @ApiProperty({ description: '文件类型' }) 17 | type: string; 18 | 19 | @ApiProperty({ description: '大小' }) 20 | size: string; 21 | 22 | @ApiProperty({ description: '上传时间' }) 23 | createdAt: string; 24 | 25 | @ApiProperty({ description: '上传者' }) 26 | username: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/admin/tools/storage/storage.controller.ts: -------------------------------------------------------------------------------- 1 | import { PageResult } from '@/common/class/res.class'; 2 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 3 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | import { StorageInfo } from './storage.class'; 5 | 6 | import { DeleteStorageDto, PageSearchStorageDto } from './storage.dto'; 7 | import { StorageService } from './storage.service'; 8 | 9 | @ApiTags('存储模块') 10 | @Controller('storage') 11 | export class StorageController { 12 | constructor(private storageService: StorageService) {} 13 | 14 | @ApiOperation({ summary: '获取本地存储列表' }) 15 | @Get('list') 16 | async list(@Query() dto: PageSearchStorageDto): Promise> { 17 | const items = await this.storageService.page(dto); 18 | const count = await this.storageService.count(); 19 | return { 20 | items, 21 | total: count, 22 | }; 23 | } 24 | 25 | @ApiOperation({ summary: '删除文件' }) 26 | @Post('delete') 27 | async delete(@Body() dto: DeleteStorageDto): Promise { 28 | return await this.storageService.delete(dto.ids); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/admin/tools/storage/storage.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { ArrayNotEmpty, IsArray, IsOptional, IsString } from 'class-validator'; 3 | import { PaginateDto } from '@/common/dto/page.dto'; 4 | 5 | export class PageSearchStorageDto extends PaginateDto { 6 | @ApiProperty({ description: '文件名' }) 7 | @IsOptional() 8 | @IsString() 9 | name: string; 10 | 11 | @ApiProperty({ description: '文件后缀' }) 12 | @IsString() 13 | @IsOptional() 14 | extName: string; 15 | 16 | @ApiProperty({ description: '文件类型' }) 17 | @IsString() 18 | @IsOptional() 19 | type: string; 20 | 21 | @ApiProperty({ description: '大小' }) 22 | @IsString() 23 | @IsOptional() 24 | size: string; 25 | 26 | @ApiProperty({ description: '上传时间' }) 27 | @IsOptional() 28 | time: string[]; 29 | 30 | @ApiProperty({ description: '上传者' }) 31 | @IsString() 32 | @IsOptional() 33 | username: string; 34 | } 35 | 36 | export class CreateStorageDto { 37 | @ApiProperty({ description: '文件名' }) 38 | @IsString() 39 | name: string; 40 | 41 | @ApiProperty({ description: '真实文件名' }) 42 | @IsString() 43 | fileName: string; 44 | 45 | @ApiProperty({ description: '文件扩展名' }) 46 | @IsString() 47 | extName: string; 48 | 49 | @ApiProperty({ description: '文件路径' }) 50 | @IsString() 51 | path: string; 52 | 53 | @ApiProperty({ description: '文件路径' }) 54 | @IsString() 55 | type: string; 56 | 57 | @ApiProperty({ description: '文件大小' }) 58 | @IsString() 59 | size: string; 60 | } 61 | 62 | export class DeleteStorageDto { 63 | @ApiProperty({ description: '需要删除的文件ID列表', type: [Number] }) 64 | @IsArray() 65 | @ArrayNotEmpty() 66 | ids: number[]; 67 | } 68 | -------------------------------------------------------------------------------- /src/modules/admin/tools/storage/storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Between, Like, Repository } from 'typeorm'; 4 | import ToolStorage from '@/entities/admin/tool-storage.entity'; 5 | import { CreateStorageDto, PageSearchStorageDto } from './storage.dto'; 6 | import { deleteFile } from '@/utils/file.util'; 7 | import { StorageInfo } from './storage.class'; 8 | import SysUser from '@/entities/admin/sys-user.entity'; 9 | 10 | @Injectable() 11 | export class StorageService { 12 | constructor( 13 | @InjectRepository(ToolStorage) 14 | private storageRepository: Repository, 15 | @InjectRepository(SysUser) 16 | private userRepository: Repository, 17 | ) {} 18 | 19 | /** 20 | * 保存文件上传记录 21 | */ 22 | async save(file: CreateStorageDto & { userId: number }): Promise { 23 | await this.storageRepository.save({ 24 | name: file.name, 25 | fileName: file.fileName, 26 | extName: file.extName, 27 | path: file.path, 28 | type: file.type, 29 | size: file.size, 30 | userId: file.userId, 31 | }); 32 | } 33 | 34 | /** 35 | * 删除文件 36 | */ 37 | async delete(fileIds: number[]): Promise { 38 | const items = await this.storageRepository.findByIds(fileIds); 39 | await this.storageRepository.delete(fileIds); 40 | 41 | items.forEach((el) => { 42 | deleteFile(el.path); 43 | }); 44 | } 45 | 46 | async page(dto: PageSearchStorageDto): Promise { 47 | const { page, pageSize, name, type, size, extName, time, username } = dto; 48 | 49 | const where = { 50 | ...(name ? { name: Like(`%${name}%`) } : null), 51 | ...(type ? { type: type } : null), 52 | ...(extName ? { extName: extName } : null), 53 | ...(size ? { size: Between(size[0], size[1]) } : null), 54 | ...(time ? { createdAt: Between(time[0], time[1]) } : null), 55 | }; 56 | 57 | if (username) { 58 | const user = await this.userRepository.findOneBy({ username: username }); 59 | where['userId'] = user?.id; 60 | } 61 | 62 | const result = await this.storageRepository 63 | .createQueryBuilder('storage') 64 | .leftJoinAndSelect('sys_user', 'user', 'storage.user_id = user.id') 65 | .where(where) 66 | .orderBy('storage.created_at', 'DESC') 67 | .offset((page - 1) * pageSize) 68 | .limit(pageSize) 69 | .getRawMany(); 70 | 71 | return result.map((e) => { 72 | return { 73 | id: e.storage_id, 74 | name: e.storage_name, 75 | extName: e.storage_ext_name, 76 | path: e.storage_path, 77 | type: e.storage_type, 78 | size: e.storage_size, 79 | createdAt: e.storage_created_at, 80 | username: e.user_username, 81 | }; 82 | }); 83 | } 84 | 85 | async count(): Promise { 86 | return await this.storageRepository.count(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/modules/admin/tools/tools.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EmailController } from './email/email.controller'; 3 | import { EmailService } from '@/shared/services/email.service'; 4 | import { StorageService } from './storage/storage.service'; 5 | import { StorageController } from './storage/storage.controller'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import ToolStorage from '@/entities/admin/tool-storage.entity'; 8 | import SysUser from '@/entities/admin/sys-user.entity'; 9 | 10 | @Module({ 11 | imports: [TypeOrmModule.forFeature([ToolStorage, SysUser])], 12 | controllers: [EmailController, StorageController], 13 | providers: [EmailService, StorageService], 14 | exports: [EmailService, StorageService], 15 | }) 16 | export class ToolsModule {} 17 | -------------------------------------------------------------------------------- /src/modules/admin/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Controller, Post, Req } from '@nestjs/common'; 2 | import { FastifyRequest } from 'fastify'; 3 | import { MultipartFile } from '@fastify/multipart'; 4 | import { ApiTags } from '@nestjs/swagger'; 5 | import { 6 | fileRename, 7 | getExtname, 8 | getFilePath, 9 | getFileType, 10 | getName, 11 | getSize, 12 | saveFile, 13 | } from '@/utils/file.util'; 14 | import { StorageService } from '../tools/storage/storage.service'; 15 | import { AdminUser } from '../core/decorators/admin-user.decorator'; 16 | import { IAdminUser } from '../admin.interface'; 17 | 18 | @ApiTags('上传模块') 19 | @Controller('upload') 20 | export class UploadController { 21 | constructor(private storageService: StorageService) {} 22 | 23 | @Post() 24 | async upload(@Req() req: FastifyRequest, @AdminUser() user: IAdminUser) { 25 | const file: MultipartFile = await req.file(); 26 | const fileName = file.filename; 27 | const size = getSize(file.file.bytesRead); 28 | const extName = getExtname(fileName); 29 | const type = getFileType(extName); 30 | 31 | const name = fileRename(fileName); 32 | const path = getFilePath(name); 33 | 34 | try { 35 | await saveFile(file, name); 36 | await this.storageService.save({ 37 | name: getName(fileName), 38 | fileName, 39 | extName, 40 | path, 41 | type, 42 | size, 43 | userId: user?.uid, 44 | }); 45 | 46 | return { 47 | filename: path, 48 | }; 49 | } catch (error) { 50 | console.log(error); 51 | throw new BadRequestException('上传失败'); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/modules/admin/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ToolsModule } from '../tools/tools.module'; 3 | import { UploadController } from './upload.controller'; 4 | 5 | @Module({ 6 | imports: [ToolsModule], 7 | controllers: [UploadController], 8 | providers: [], 9 | }) 10 | export class UploadModule {} 11 | -------------------------------------------------------------------------------- /src/modules/ws/admin-ws.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OnGatewayConnection, 3 | OnGatewayDisconnect, 4 | OnGatewayInit, 5 | WebSocketGateway, 6 | WebSocketServer, 7 | } from '@nestjs/websockets'; 8 | import { Server, Socket } from 'socket.io'; 9 | import { AuthService } from './auth.service'; 10 | import { EVENT_OFFLINE, EVENT_ONLINE } from './ws.event'; 11 | 12 | /** 13 | * Admin WebSokcet网关,不含权限校验,Socket端只做通知相关操作 14 | */ 15 | @WebSocketGateway(parseInt(process.env.WS_PORT), { 16 | path: '/' + process.env.WS_PATH, 17 | namespace: '/admin', 18 | }) 19 | export class AdminWSGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit { 20 | @WebSocketServer() 21 | private wss: Server; 22 | 23 | get socketServer(): Server { 24 | return this.wss; 25 | } 26 | 27 | constructor(private authService: AuthService) {} 28 | 29 | /** 30 | * OnGatewayInit 31 | * @param server Server 32 | */ 33 | afterInit() { 34 | // TODO 35 | } 36 | 37 | /** 38 | * OnGatewayConnection 39 | */ 40 | async handleConnection(client: Socket): Promise { 41 | try { 42 | this.authService.checkAdminAuthToken(client.handshake?.query?.token); 43 | } catch (e) { 44 | // no auth 45 | client.disconnect(); 46 | return; 47 | } 48 | 49 | // broadcast online 50 | client.broadcast.emit(EVENT_ONLINE); 51 | } 52 | 53 | /** 54 | * OnGatewayDisconnect 55 | */ 56 | async handleDisconnect(client: Socket): Promise { 57 | // TODO 58 | client.broadcast.emit(EVENT_OFFLINE); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/modules/ws/admin-ws.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { Socket } from 'socket.io'; 4 | import { SocketException } from 'src/common/exceptions/socket.exception'; 5 | import { AuthService } from './auth.service'; 6 | 7 | @Injectable() 8 | export class AdminWsGuard implements CanActivate { 9 | constructor(private authService: AuthService) {} 10 | 11 | canActivate(context: ExecutionContext): boolean | Promise | Observable { 12 | const client = context.switchToWs().getClient(); 13 | const token = client?.handshake?.query?.token; 14 | try { 15 | // 挂载对象到当前请求上 16 | this.authService.checkAdminAuthToken(token); 17 | return true; 18 | } catch (e) { 19 | // close 20 | client.disconnect(); 21 | // 无法通过token校验 22 | throw new SocketException(11001); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/ws/admin-ws.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { In, Repository } from 'typeorm'; 4 | import SysRoleMenu from '@/entities/admin/sys-role-menu.entity'; 5 | import SysUserRole from '@/entities/admin/sys-user-role.entity'; 6 | import { AdminWSGateway } from '@/modules/ws/admin-ws.gateway'; 7 | import { RemoteSocket } from 'socket.io'; 8 | import { EVENT_UPDATE_MENU } from './ws.event'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | 11 | @Injectable() 12 | export class AdminWSService { 13 | constructor( 14 | private jwtService: JwtService, 15 | @InjectRepository(SysRoleMenu) 16 | private roleMenuRepository: Repository, 17 | @InjectRepository(SysUserRole) 18 | private userRoleRepository: Repository, 19 | private adminWsGateWay: AdminWSGateway, 20 | ) {} 21 | 22 | /** 23 | * 获取当前在线用户 24 | */ 25 | async getOnlineSockets() { 26 | const onlineSockets = await this.adminWsGateWay.socketServer.fetchSockets(); 27 | return onlineSockets; 28 | } 29 | 30 | /** 31 | * 根据uid查找socketid 32 | */ 33 | async findSocketIdByUid(uid: number): Promise> { 34 | const onlineSockets = await this.getOnlineSockets(); 35 | const socket = onlineSockets.find((socket) => { 36 | const token = socket.handshake.query?.token as string; 37 | const tokenUid = this.jwtService.verify(token).uid; 38 | return tokenUid === uid; 39 | }); 40 | return socket; 41 | } 42 | 43 | /** 44 | * 根据uid数组过滤出socketid 45 | */ 46 | async filterSocketIdByUidArr(uids: number[]): Promise[]> { 47 | const onlineSockets = await this.getOnlineSockets(); 48 | const sockets = onlineSockets.filter((socket) => { 49 | const token = socket.handshake.query?.token as string; 50 | const tokenUid = this.jwtService.verify(token).uid; 51 | return uids.includes(tokenUid); 52 | }); 53 | return sockets; 54 | } 55 | 56 | /** 57 | * 通知前端重新获取权限菜单 58 | * @param userIds 59 | * @constructor 60 | */ 61 | async noticeUserToUpdateMenusByUserIds(uid: number | number[]) { 62 | const userIds = Array.isArray(uid) ? uid : [uid]; 63 | const sockets = await this.filterSocketIdByUidArr(userIds); 64 | if (sockets) { 65 | // socket emit event 66 | this.adminWsGateWay.socketServer.to(sockets.map((n) => n.id)).emit(EVENT_UPDATE_MENU); 67 | } 68 | } 69 | 70 | /** 71 | * 通过menuIds通知用户更新权限菜单 72 | */ 73 | async noticeUserToUpdateMenusByMenuIds(menuIds: number[]): Promise { 74 | const roleMenus = await this.roleMenuRepository.findBy({ 75 | menuId: In(menuIds), 76 | }); 77 | const roleIds = roleMenus.map((n) => n.roleId); 78 | await this.noticeUserToUpdateMenusByRoleIds(roleIds); 79 | } 80 | 81 | /** 82 | * 通过roleIds通知用户更新权限菜单 83 | */ 84 | async noticeUserToUpdateMenusByRoleIds(roleIds: number[]): Promise { 85 | const users = await this.userRoleRepository.findBy({ 86 | roleId: In(roleIds), 87 | }); 88 | if (users) { 89 | const userIds = users.map((n) => n.userId); 90 | await this.noticeUserToUpdateMenusByUserIds(userIds); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/modules/ws/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { isEmpty } from 'lodash'; 4 | import { SocketException } from 'src/common/exceptions/socket.exception'; 5 | import { IAdminUser } from '../admin/admin.interface'; 6 | 7 | @Injectable() 8 | export class AuthService { 9 | constructor(private jwtService: JwtService) {} 10 | 11 | checkAdminAuthToken(token: string | string[] | undefined): IAdminUser | never { 12 | if (isEmpty(token)) { 13 | throw new SocketException(11001); 14 | } 15 | try { 16 | // 挂载对象到当前请求上 17 | return this.jwtService.verify(Array.isArray(token) ? token[0] : token); 18 | } catch (e) { 19 | // 无法通过token校验 20 | throw new SocketException(11001); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/ws/ws.event.ts: -------------------------------------------------------------------------------- 1 | // 用户上线事件 2 | export const EVENT_ONLINE = 'online'; 3 | export const EVENT_OFFLINE = 'offline'; 4 | // 踢下线 5 | export const EVENT_KICK = 'kick'; 6 | // 当角色权限或菜单被修改时,通知用户获取最新的权限菜单 7 | export const EVENT_UPDATE_MENU = 'update_menu'; 8 | -------------------------------------------------------------------------------- /src/modules/ws/ws.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AdminWSGateway } from './admin-ws.gateway'; 3 | import { AuthService } from './auth.service'; 4 | import { AdminWSService } from './admin-ws.service'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import SysUserRole from '@/entities/admin/sys-user-role.entity'; 7 | import SysRoleMenu from '@/entities/admin/sys-role-menu.entity'; 8 | 9 | const providers = [AdminWSGateway, AuthService, AdminWSService]; 10 | 11 | /** 12 | * WebSocket Module 13 | */ 14 | @Module({ 15 | imports: [TypeOrmModule.forFeature([SysUserRole, SysRoleMenu])], 16 | providers, 17 | exports: providers, 18 | }) 19 | export class WSModule {} 20 | -------------------------------------------------------------------------------- /src/setup-swagger.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | // import fs from 'fs'; 5 | 6 | export function setupSwagger(app: INestApplication): void { 7 | const configService: ConfigService = app.get(ConfigService); 8 | const path = configService.get('swagger.path', 'swagger-ui'); 9 | // 默认为启用 10 | const enable = configService.get('swagger.enable', true); 11 | 12 | // 判断是否需要启用 13 | if (!enable) { 14 | return; 15 | } 16 | 17 | // 配置Swagger文档 18 | const options = new DocumentBuilder() 19 | // .addBearerAuth() // 开启 BearerAuth 授权认证 20 | .setTitle(configService.get('swagger.title')) 21 | .setDescription('相关 API 接口文档') 22 | .setVersion('1.0') 23 | .setLicense('MIT', 'https://github.com/kuizuo/kz-nest-admin') 24 | .build(); 25 | const document = SwaggerModule.createDocument(app, options, { 26 | // include: [ApiModule], 27 | ignoreGlobalPrefix: false, 28 | }); 29 | 30 | // 导出数据JSON格式,方便导入第三方API接口工具 31 | // fs.writeFileSync('./swagger-spec.json', JSON.stringify(document)); 32 | 33 | SwaggerModule.setup(path, app, document); 34 | } 35 | -------------------------------------------------------------------------------- /src/shared/logger/logger.constants.ts: -------------------------------------------------------------------------------- 1 | export const LOGGER_MODULE_OPTIONS = Symbol('LOGGER_MODULE_OPTIONS'); 2 | export const PROJECT_LOG_DIR_NAME = 'logs'; 3 | export const DEFAULT_WEB_LOG_NAME = 'web.log'; 4 | export const DEFAULT_ERROR_LOG_NAME = 'common-error.log'; 5 | export const DEFAULT_ACCESS_LOG_NAME = 'access.log'; 6 | export const DEFAULT_SQL_SLOW_LOG_NAME = 'sql-slow.log'; 7 | export const DEFAULT_SQL_ERROR_LOG_NAME = 'sql-error.log'; 8 | export const DEFAULT_TASK_LOG_NAME = 'task.log'; 9 | // 默认日志存储天数 10 | export const DEFAULT_MAX_SIZE = '2m'; 11 | -------------------------------------------------------------------------------- /src/shared/logger/logger.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from '@nestjs/common'; 2 | import { LoggerOptions } from 'typeorm'; 3 | 4 | /** 5 | * 日志等级 6 | */ 7 | export type WinstonLogLevel = 'info' | 'error' | 'warn' | 'debug' | 'verbose'; 8 | 9 | export interface TypeORMLoggerOptions { 10 | options?: LoggerOptions; 11 | } 12 | 13 | /** 14 | * 日志配置,默认按天数进行切割 15 | */ 16 | export interface LoggerModuleOptions { 17 | /** 18 | * 日志文件输出 19 | * 默认只会输出 log 及以上(warn 和 error)的日志到文件中,等级级别如下 20 | */ 21 | level?: WinstonLogLevel | 'none'; 22 | 23 | /** 24 | * 控制台输出等级 25 | */ 26 | consoleLevel?: WinstonLogLevel | 'none'; 27 | 28 | /** 29 | * 如果启用,将打印当前和上一个日志消息之间的时间戳(时差) 30 | */ 31 | timestamp?: boolean; 32 | 33 | /** 34 | * 生产环境下,默认会关闭终端日志输出,如有需要,可以设置为 false 35 | */ 36 | disableConsoleAtProd?: boolean; 37 | 38 | /** 39 | * Maximum size of the file after which it will rotate. This can be a number of bytes, or units of kb, mb, and gb. 40 | * If using the units, add 'k', 'm', or 'g' as the suffix. The units need to directly follow the number. 41 | * default: 2m 42 | */ 43 | maxFileSize?: string; 44 | 45 | /** 46 | * Maximum number of logs to keep. If not set, 47 | * no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. 48 | * default: 15d 49 | */ 50 | maxFiles?: string; 51 | 52 | /** 53 | * 开发环境下日志产出的目录,绝对路径 54 | * 开发环境下为了避免冲突以及集中管理,日志会打印在项目目录下的 logs 目录 55 | */ 56 | dir?: string; 57 | 58 | /** 59 | * 任何 logger 的 .error() 调用输出的日志都会重定向到这里,重点通过查看此日志定位异常,默认文件名为 common-error.%DATE%.log 60 | * 注意:此文件名可以包含%DATE%占位符 61 | */ 62 | errorLogName?: string; 63 | 64 | /** 65 | * 应用相关日志,供应用开发者使用的日志。我们在绝大数情况下都在使用它,默认文件名为 web.%DATE%.log 66 | * 注意:此文件名可以包含%DATE%占位符 67 | */ 68 | appLogName?: string; 69 | } 70 | 71 | export interface LoggerModuleAsyncOptions extends Pick { 72 | useFactory?: (...args: any[]) => LoggerModuleOptions; 73 | inject?: any[]; 74 | } 75 | -------------------------------------------------------------------------------- /src/shared/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { LOGGER_MODULE_OPTIONS } from './logger.constants'; 3 | import { LoggerModuleAsyncOptions, LoggerModuleOptions } from './logger.interface'; 4 | import { LoggerService } from './logger.service'; 5 | 6 | @Module({}) 7 | export class LoggerModule { 8 | static forRoot(options: LoggerModuleOptions, isGlobal = false): DynamicModule { 9 | return { 10 | global: isGlobal, 11 | module: LoggerModule, 12 | providers: [ 13 | LoggerService, 14 | { 15 | provide: LOGGER_MODULE_OPTIONS, 16 | useValue: options, 17 | }, 18 | ], 19 | exports: [LoggerService, LOGGER_MODULE_OPTIONS], 20 | }; 21 | } 22 | 23 | static forRootAsync(options: LoggerModuleAsyncOptions, isGlobal = false): DynamicModule { 24 | return { 25 | global: isGlobal, 26 | module: LoggerModule, 27 | imports: options.imports, 28 | providers: [ 29 | LoggerService, 30 | { 31 | provide: LOGGER_MODULE_OPTIONS, 32 | useFactory: options.useFactory, 33 | inject: options.inject, 34 | }, 35 | ], 36 | exports: [LoggerService, LOGGER_MODULE_OPTIONS], 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/shared/logger/typeorm-logger.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Logger, LoggerOptions } from 'typeorm'; 3 | import { DEFAULT_SQL_ERROR_LOG_NAME, DEFAULT_SQL_SLOW_LOG_NAME } from './logger.constants'; 4 | import { LoggerModuleOptions } from './logger.interface'; 5 | import { LoggerService } from './logger.service'; 6 | 7 | /** 8 | * 自定义TypeORM日志,sqlSlow日志及error日志会自动记录至日志文件 9 | */ 10 | @Injectable() 11 | export class TypeORMLoggerService implements Logger { 12 | /** 13 | * sql logger 14 | */ 15 | private logger: LoggerService; 16 | 17 | constructor(private options: LoggerOptions, private config: LoggerModuleOptions) { 18 | this.logger = new LoggerService(TypeORMLoggerService.name, { 19 | level: 'warn', 20 | consoleLevel: 'verbose', 21 | appLogName: DEFAULT_SQL_SLOW_LOG_NAME, 22 | errorLogName: DEFAULT_SQL_ERROR_LOG_NAME, 23 | timestamp: this.config.timestamp, 24 | dir: this.config.dir, 25 | maxFileSize: this.config.maxFileSize, 26 | maxFiles: this.config.maxFiles, 27 | }); 28 | } 29 | 30 | /** 31 | * Logs query and parameters used in it. 32 | */ 33 | logQuery(query: string, parameters?: any[]) { 34 | if ( 35 | this.options === 'all' || 36 | this.options === true || 37 | (Array.isArray(this.options) && this.options.indexOf('query') !== -1) 38 | ) { 39 | const sql = 40 | query + 41 | (parameters && parameters.length 42 | ? ' -- PARAMETERS: ' + this.stringifyParams(parameters) 43 | : ''); 44 | this.logger.verbose('[QUERY]: ' + sql); 45 | } 46 | } 47 | 48 | /** 49 | * Logs query that is failed. 50 | */ 51 | logQueryError(error: string | Error, query: string, parameters?: any[]) { 52 | if ( 53 | this.options === 'all' || 54 | this.options === true || 55 | (Array.isArray(this.options) && this.options.indexOf('error') !== -1) 56 | ) { 57 | const sql = 58 | query + 59 | (parameters && parameters.length 60 | ? ' -- PARAMETERS: ' + this.stringifyParams(parameters) 61 | : ''); 62 | this.logger.error([`[FAILED QUERY]: ${sql}`, `[QUERY ERROR]: ${error}`]); 63 | } 64 | } 65 | 66 | /** 67 | * Logs query that is slow. 68 | */ 69 | logQuerySlow(time: number, query: string, parameters?: any[]) { 70 | const sql = 71 | query + 72 | (parameters && parameters.length 73 | ? ' -- PARAMETERS: ' + this.stringifyParams(parameters) 74 | : ''); 75 | this.logger.warn(`[SLOW QUERY: ${time} ms]: ` + sql); 76 | } 77 | 78 | /** 79 | * Logs events from the schema build process. 80 | */ 81 | logSchemaBuild(message: string) { 82 | if ( 83 | this.options === 'all' || 84 | (Array.isArray(this.options) && this.options.indexOf('schema') !== -1) 85 | ) { 86 | this.logger.verbose(message); 87 | } 88 | } 89 | 90 | /** 91 | * Logs events from the migrations run process. 92 | */ 93 | logMigration(message: string) { 94 | this.logger.verbose(message); 95 | } 96 | 97 | /** 98 | * Perform logging using given logger, or by default to the console. 99 | * Log has its own level and message. 100 | */ 101 | log(level: 'log' | 'info' | 'warn', message: any) { 102 | switch (level) { 103 | case 'log': 104 | if ( 105 | this.options === 'all' || 106 | (Array.isArray(this.options) && this.options.indexOf('log') !== -1) 107 | ) 108 | this.logger.verbose('[LOG]: ' + message); 109 | break; 110 | case 'info': 111 | if ( 112 | this.options === 'all' || 113 | (Array.isArray(this.options) && this.options.indexOf('info') !== -1) 114 | ) 115 | this.logger.log('[INFO]: ' + message); 116 | break; 117 | case 'warn': 118 | if ( 119 | this.options === 'all' || 120 | (Array.isArray(this.options) && this.options.indexOf('warn') !== -1) 121 | ) 122 | this.logger.warn('[WARN]: ' + message); 123 | break; 124 | } 125 | } 126 | 127 | /** 128 | * Converts parameters to a string. 129 | * Sometimes parameters can have circular objects and therefor we are handle this case too. 130 | */ 131 | protected stringifyParams(parameters: any[]) { 132 | try { 133 | return JSON.stringify(parameters); 134 | } catch (error) { 135 | // most probably circular objects in parameters 136 | return parameters; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/shared/logger/utils/app-root-path.util.ts: -------------------------------------------------------------------------------- 1 | import { parse, resolve, join } from 'path'; 2 | import { existsSync } from 'fs'; 3 | 4 | /** 5 | * 获取应用根目录 6 | * @returns 应用根目录 7 | */ 8 | export function getAppRootPath(): string { 9 | // Check for environmental variable 10 | if (process.env.APP_ROOT_PATH) { 11 | return resolve(process.env.APP_ROOT_PATH); 12 | } 13 | // 逐级查找 node_modules 目录 14 | let cur = __dirname; 15 | const root = parse(cur).root; 16 | 17 | let appRootPath = ''; 18 | while (true) { 19 | if (existsSync(join(cur, 'node_modules')) && existsSync(join(cur, 'package.json'))) { 20 | // 如果存在node_modules、package.json 21 | appRootPath = cur; 22 | } 23 | // 已经为根路径,无需向上查找 24 | if (root === cur) { 25 | break; 26 | } 27 | 28 | // 继续向上查找 29 | cur = resolve(cur, '..'); 30 | } 31 | 32 | if (appRootPath) { 33 | process.env.APP_ROOT_PATH = appRootPath; 34 | } 35 | return appRootPath; 36 | } 37 | -------------------------------------------------------------------------------- /src/shared/logger/utils/home-dir.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | 3 | export function getHomedir(): string { 4 | if (process.env.MOCK_HOME_DIR) return process.env.MOCK_HOME_DIR; 5 | 6 | if (typeof os.userInfo === 'function') { 7 | try { 8 | const homedir = os.userInfo().homedir; 9 | if (homedir) return homedir; 10 | } catch (err) { 11 | if (err.code !== 'ENOENT') throw err; 12 | } 13 | } 14 | 15 | if (typeof os.homedir === 'function') { 16 | return os.homedir(); 17 | } 18 | 19 | return process.env.HOME; 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/redis/redis.constants.ts: -------------------------------------------------------------------------------- 1 | export const REDIS_CLIENT = Symbol('REDIS_CLIENT'); 2 | export const REDIS_MODULE_OPTIONS = Symbol('REDIS_MODULE_OPTIONS'); 3 | export const REDIS_DEFAULT_CLIENT_KEY = 'default'; 4 | -------------------------------------------------------------------------------- /src/shared/redis/redis.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from '@nestjs/common'; 2 | import { Redis, RedisOptions, ClusterNode, ClusterOptions } from 'ioredis'; 3 | 4 | export interface RedisModuleOptions extends RedisOptions { 5 | /** 6 | * muitl client connection, default 7 | */ 8 | name?: string; 9 | 10 | /** 11 | * support url 12 | */ 13 | url?: string; 14 | 15 | /** 16 | * is cluster 17 | */ 18 | cluster?: boolean; 19 | 20 | /** 21 | * cluster node, using cluster is true 22 | */ 23 | nodes?: ClusterNode[]; 24 | 25 | /** 26 | * cluster options, using cluster is true 27 | */ 28 | clusterOptions?: ClusterOptions; 29 | 30 | /** 31 | * callback 32 | */ 33 | onClientReady?(client: Redis): void; 34 | } 35 | 36 | export interface RedisModuleAsyncOptions extends Pick { 37 | useFactory?: ( 38 | ...args: any[] 39 | ) => 40 | | RedisModuleOptions 41 | | RedisModuleOptions[] 42 | | Promise 43 | | Promise; 44 | inject?: any[]; 45 | } 46 | -------------------------------------------------------------------------------- /src/shared/redis/redis.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module, OnModuleDestroy, Provider } from '@nestjs/common'; 2 | import IORedis, { Redis, Cluster } from 'ioredis'; 3 | import { isEmpty } from 'lodash'; 4 | import { REDIS_CLIENT, REDIS_DEFAULT_CLIENT_KEY, REDIS_MODULE_OPTIONS } from './redis.constants'; 5 | import { RedisModuleAsyncOptions, RedisModuleOptions } from './redis.interface'; 6 | 7 | @Module({}) 8 | export class RedisModule implements OnModuleDestroy { 9 | static register(options: RedisModuleOptions | RedisModuleOptions[]): DynamicModule { 10 | const clientProvider = this.createAysncProvider(); 11 | return { 12 | module: RedisModule, 13 | providers: [ 14 | clientProvider, 15 | { 16 | provide: REDIS_MODULE_OPTIONS, 17 | useValue: options, 18 | }, 19 | ], 20 | exports: [clientProvider], 21 | }; 22 | } 23 | 24 | static registerAsync(options: RedisModuleAsyncOptions): DynamicModule { 25 | const clientProvider = this.createAysncProvider(); 26 | return { 27 | module: RedisModule, 28 | imports: options.imports ?? [], 29 | providers: [clientProvider, this.createAsyncClientOptions(options)], 30 | exports: [clientProvider], 31 | }; 32 | } 33 | 34 | /** 35 | * create provider 36 | */ 37 | private static createAysncProvider(): Provider { 38 | // create client 39 | return { 40 | provide: REDIS_CLIENT, 41 | useFactory: ( 42 | options: RedisModuleOptions | RedisModuleOptions[], 43 | ): Map => { 44 | const clients = new Map(); 45 | if (Array.isArray(options)) { 46 | options.forEach((op) => { 47 | const name = op.name ?? REDIS_DEFAULT_CLIENT_KEY; 48 | if (clients.has(name)) { 49 | throw new Error('Redis Init Error: name must unique'); 50 | } 51 | clients.set(name, this.createClient(op)); 52 | }); 53 | } else { 54 | // not array 55 | clients.set(REDIS_DEFAULT_CLIENT_KEY, this.createClient(options)); 56 | } 57 | return clients; 58 | }, 59 | inject: [REDIS_MODULE_OPTIONS], 60 | }; 61 | } 62 | 63 | /** 64 | * 创建IORedis实例 65 | */ 66 | private static createClient(options: RedisModuleOptions): Redis | Cluster { 67 | const { onClientReady, url, cluster, clusterOptions, nodes, ...opts } = options; 68 | let client = null; 69 | // check url 70 | if (!isEmpty(url)) { 71 | client = new IORedis(url); 72 | } else if (cluster) { 73 | // check cluster 74 | client = new IORedis.Cluster(nodes, clusterOptions); 75 | } else { 76 | client = new IORedis(opts); 77 | } 78 | if (onClientReady) { 79 | onClientReady(client); 80 | } 81 | return client; 82 | } 83 | 84 | private static createAsyncClientOptions(options: RedisModuleAsyncOptions) { 85 | return { 86 | provide: REDIS_MODULE_OPTIONS, 87 | useFactory: options.useFactory, 88 | inject: options.inject, 89 | }; 90 | } 91 | 92 | onModuleDestroy() { 93 | // on destroy 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/shared/services/email.service.ts: -------------------------------------------------------------------------------- 1 | import { ISendMailOptions, MailerService, MailerTransportFactory } from '@nestjs-modules/mailer'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | @Injectable() 6 | export class EmailService { 7 | constructor( 8 | private readonly mailerService: MailerService, 9 | private configService: ConfigService, 10 | ) {} 11 | 12 | async sendMail(to, subject, content): Promise { 13 | return await this.mailerService.sendMail({ 14 | to: to, 15 | from: { 16 | name: this.configService.get('appName'), 17 | address: this.configService.get('email.user'), 18 | }, 19 | subject: subject, 20 | text: content, 21 | }); 22 | } 23 | 24 | async sendMailHtml(to, subject, html): Promise { 25 | return await this.mailerService.sendMail({ 26 | to: to, 27 | from: { 28 | name: this.configService.get('appName'), 29 | address: this.configService.get('email.user'), 30 | }, 31 | subject: subject, 32 | html: html, 33 | }); 34 | } 35 | 36 | async sendCodeMail(to, code) { 37 | const content = `尊敬的用户您好,您的验证码是${code},请于5分钟内输入。`; 38 | const ret = await this.sendMail( 39 | to, 40 | `[${this.configService.get('appName')}] ` + '验证码通知', 41 | content, 42 | ); 43 | 44 | if (!ret) return '发送验证码失败'; 45 | return '发送邮箱验证码成功,请在5分钟内输入'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/shared/services/ip.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class IpService { 6 | constructor(private readonly http: HttpService) {} 7 | 8 | async getAddress(ip: string) { 9 | const { data } = await this.http.axiosRef.get( 10 | `https://api.kuizuo.cn/api/ip-location?ip=${ip}&type=json`, 11 | ); 12 | return data.addr; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/shared/services/qq.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class QQService { 6 | constructor(private readonly http: HttpService) {} 7 | 8 | async getNickname(qq: string | number) { 9 | const { data } = await this.http.axiosRef.get('https://api.kuizuo.cn/api/qqnick?qq=' + qq); 10 | return data; 11 | } 12 | 13 | async getAvater(qq: string | number) { 14 | return `https://q1.qlogo.cn/g?b=qq&s=100&nk=${qq}`; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/services/redis.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { Cluster } from 'cluster'; 3 | import { Redis } from 'ioredis'; 4 | import { REDIS_CLIENT, REDIS_DEFAULT_CLIENT_KEY } from '../redis/redis.constants'; 5 | 6 | @Injectable() 7 | export class RedisService { 8 | constructor( 9 | @Inject(REDIS_CLIENT) 10 | private readonly clients: Map, 11 | ) {} 12 | 13 | /** 14 | * get redis client by name 15 | * @param name client name 16 | * @returns Redis Instance 17 | */ 18 | public getRedis(name = REDIS_DEFAULT_CLIENT_KEY): Redis { 19 | if (!this.clients.has(name)) { 20 | throw new Error(`redis client ${name} does not exist`); 21 | } 22 | return this.clients.get(name) as Redis; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/shared/services/util.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { FastifyRequest } from 'fastify'; 3 | // import { customAlphabet, nanoid } from 'nanoid'; 4 | import * as CryptoJS from 'crypto-js'; 5 | 6 | @Injectable() 7 | export class UtilService { 8 | /** 9 | * 获取请求IP 10 | */ 11 | getReqIP(req: FastifyRequest): string { 12 | return ( 13 | // 判断是否有反向代理 IP 14 | ( 15 | (req.headers['x-forwarded-for'] as string) || 16 | // 判断后端的 socket 的 IP 17 | req.socket.remoteAddress 18 | ).replace('::ffff:', '') 19 | ); 20 | } 21 | 22 | /** 23 | * AES加密 24 | */ 25 | public aesEncrypt(msg: string, secret: string): string { 26 | return CryptoJS.AES.encrypt(msg, secret).toString(); 27 | } 28 | 29 | /** 30 | * AES解密 31 | */ 32 | public aesDecrypt(encrypted: string, secret: string): string { 33 | return CryptoJS.AES.decrypt(encrypted, secret).toString(CryptoJS.enc.Utf8); 34 | } 35 | 36 | /** 37 | * md5加密 38 | */ 39 | public md5(msg: string): string { 40 | return CryptoJS.MD5(msg).toString(); 41 | } 42 | 43 | /** 44 | * 生成一个UUID 45 | */ 46 | public generateUUID(): string { 47 | return ''; 48 | } 49 | 50 | /** 51 | * 生成一个随机的值 52 | */ 53 | public generateRandomValue( 54 | length: number, 55 | placeholder = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM', 56 | ): string { 57 | return ''; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { MailerModule } from '@nestjs-modules/mailer'; 2 | import { HttpModule } from '@nestjs/axios'; 3 | import { Global, CacheModule, Module } from '@nestjs/common'; 4 | import { ConfigModule, ConfigService } from '@nestjs/config'; 5 | import { JwtModule } from '@nestjs/jwt'; 6 | import { EmailService } from './services/email.service'; 7 | import { QQService } from './services/qq.service'; 8 | import { RedisModule } from './redis/redis.module'; 9 | import { RedisService } from './services/redis.service'; 10 | import { UtilService } from './services/util.service'; 11 | import { IpService } from './services/ip.service'; 12 | 13 | // common provider list 14 | const providers = [UtilService, RedisService, EmailService, QQService, IpService]; 15 | 16 | /** 17 | * 全局共享模块 18 | */ 19 | @Global() 20 | @Module({ 21 | imports: [ 22 | HttpModule.register({ 23 | timeout: 5000, 24 | maxRedirects: 5, 25 | }), 26 | // redis cache 27 | CacheModule.register(), 28 | // jwt 29 | JwtModule.registerAsync({ 30 | imports: [ConfigModule], 31 | useFactory: (configService: ConfigService) => ({ 32 | secret: configService.get('jwt.secret'), 33 | }), 34 | inject: [ConfigService], 35 | }), 36 | // redis 37 | RedisModule.registerAsync({ 38 | imports: [ConfigModule], 39 | useFactory: (configService: ConfigService) => ({ 40 | host: configService.get('redis.host'), 41 | port: configService.get('redis.port'), 42 | password: configService.get('redis.password'), 43 | db: configService.get('redis.db'), 44 | }), 45 | inject: [ConfigService], 46 | }), 47 | // email 48 | MailerModule.forRootAsync({ 49 | imports: [ConfigModule], 50 | useFactory: (configService: ConfigService) => ({ 51 | transport: { 52 | host: configService.get('email.host'), 53 | port: configService.get('email.port'), 54 | ignoreTLS: true, 55 | secure: true, 56 | auth: { 57 | user: configService.get('email.user'), 58 | pass: configService.get('email.pass'), 59 | }, 60 | }, 61 | }), 62 | inject: [ConfigService], 63 | }), 64 | ], 65 | providers: [...providers], 66 | exports: [HttpModule, CacheModule, JwtModule, ...providers], 67 | }) 68 | export class SharedModule {} 69 | -------------------------------------------------------------------------------- /src/utils/captcha.ts: -------------------------------------------------------------------------------- 1 | import svgCaptcha from 'svg-captcha'; 2 | 3 | export function createCaptcha() { 4 | return svgCaptcha.createMathExpr({ 5 | size: 4, 6 | ignoreChars: '0o1iIl', 7 | noise: 2, 8 | color: true, 9 | background: '#eee', 10 | fontSize: 50, 11 | width: 110, 12 | height: 38, 13 | }); 14 | } 15 | 16 | export function createMathExpr() { 17 | const options = {}; 18 | return svgCaptcha.createMathExpr(options); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/crypto.util.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js'; 2 | 3 | const key = CryptoJS.enc.Utf8.parse('kuizuoabcdefe9bc'); 4 | const iv = CryptoJS.enc.Utf8.parse('0123456789kuizuo'); 5 | 6 | export function encPass(str: string) { 7 | return this.MD5(CryptoJS.SHA1('kz' + str + '!@#123').toString()); 8 | } 9 | 10 | export function AES_enc(data) { 11 | if (!data) return data; 12 | const enc = CryptoJS.AES.encrypt(data, key, { 13 | iv: iv, 14 | mode: CryptoJS.mode.CBC, 15 | padding: CryptoJS.pad.Pkcs7, 16 | }); 17 | return enc.toString(); 18 | } 19 | 20 | export function AES_dec(data) { 21 | if (!data) return data; 22 | const dec = CryptoJS.AES.decrypt(data, key, { 23 | iv: iv, 24 | mode: CryptoJS.mode.CBC, 25 | padding: CryptoJS.pad.Pkcs7, 26 | }); 27 | return dec.toString(CryptoJS.enc.Utf8); 28 | } 29 | 30 | export function MD5(str: string) { 31 | return CryptoJS.MD5(str).toString(); 32 | } 33 | 34 | export function Base64_decode(str: string) { 35 | return CryptoJS.enc.Base64.parse(str).toString(CryptoJS.enc.Utf8); 36 | } 37 | 38 | export function Base64_encode(str: string) { 39 | return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(str)); 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/file.util.ts: -------------------------------------------------------------------------------- 1 | import { MultipartFile } from '@fastify/multipart'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import dayjs from 'dayjs'; 5 | 6 | enum Type { 7 | IMAGE = '图片', 8 | TXT = '文档', 9 | MUSIC = '音乐', 10 | VIDEO = '视频', 11 | OTHER = '其他', 12 | } 13 | 14 | export function getFileType(extName: string) { 15 | const documents = 'txt doc pdf ppt pps xlsx xls docx'; 16 | const music = 'mp3 wav wma mpa ram ra aac aif m4a'; 17 | const video = 'avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg'; 18 | const image = 'bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg'; 19 | if (image.includes(extName)) { 20 | return Type.IMAGE; 21 | } else if (documents.includes(extName)) { 22 | return Type.TXT; 23 | } else if (music.includes(extName)) { 24 | return Type.MUSIC; 25 | } else if (video.includes(extName)) { 26 | return Type.VIDEO; 27 | } else { 28 | return Type.OTHER; 29 | } 30 | } 31 | 32 | export function getName(fileName: string) { 33 | if (fileName.includes('.')) { 34 | return fileName.split('.')[0]; 35 | } else { 36 | return fileName; 37 | } 38 | } 39 | 40 | export function getExtname(fileName: string) { 41 | return path.extname(fileName).replace('.', ''); 42 | } 43 | 44 | export function getSize(bytes: number, decimals = 2) { 45 | if (bytes === 0) return '0 Bytes'; 46 | 47 | const k = 1024; 48 | const dm = decimals < 0 ? 0 : decimals; 49 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 50 | 51 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 52 | 53 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; 54 | } 55 | 56 | export function fileRename(fileName: string) { 57 | const name = fileName.split('.')[0]; 58 | const extName = path.extname(fileName); 59 | const time = dayjs().format('YYYYMMDDHHmmSSS'); 60 | return `${name}-${time}${extName}`; 61 | } 62 | 63 | export function getFilePath(name: string) { 64 | return '/upload/' + name; 65 | } 66 | 67 | export async function saveFile(file: MultipartFile, name: string) { 68 | const filePath = path.join(__dirname, '../../', 'public/upload', name); 69 | const writeStream = fs.createWriteStream(filePath); 70 | const buffer = await file.toBuffer(); 71 | writeStream.write(buffer); 72 | } 73 | 74 | export async function deleteFile(name: string) { 75 | fs.unlink(path.join(__dirname, '../../', 'public', name), (error) => { 76 | // console.log(error); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/index.util.ts: -------------------------------------------------------------------------------- 1 | export function getRanNum() { 2 | return Math.ceil(Math.random() * 10) + ''; 3 | } 4 | 5 | export function getRanChar() { 6 | return String.fromCharCode(65 + Math.ceil(Math.random() * 25)); 7 | } 8 | 9 | export function genCard(rule = '@@@###@@##@@###@@@') { 10 | let card = 'XX_'; 11 | rule.split('').forEach((item) => { 12 | if (item == '@') { 13 | card += getRanChar(); 14 | } else if (item == '#') { 15 | card += getRanNum(); 16 | } 17 | }); 18 | return card; 19 | } 20 | 21 | export function getRanMobile() { 22 | const prefixArray = ['130', '131', '132', '133', '135', '137', '138', '170', '187', '189']; 23 | const i = parseInt((10 * Math.random()).toString()); 24 | let prefix = prefixArray[i]; 25 | for (let j = 0; j < 8; j++) { 26 | prefix = prefix + Math.floor(Math.random() * 10); 27 | } 28 | 29 | return parseInt(prefix); 30 | } 31 | 32 | export function genNo() { 33 | return ( 34 | Date.now() + 35 | Math.floor(Math.random() * 999) 36 | .toString() 37 | .padStart(3, '0') 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function isExternal(path: string): boolean { 2 | const reg = 3 | /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; 4 | return reg.test(path); 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2017", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | "paths": { 22 | "@/*": ["src/*"] 23 | } 24 | }, 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------