├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── config ├── env.prod.ts ├── env.ts ├── index.ts └── log4jsConfig.ts ├── nest-cli.json ├── package.json ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── auth │ ├── auth.controller.spec.ts │ ├── auth.controller.ts │ ├── auth.module.ts │ ├── auth.service.spec.ts │ ├── auth.service.ts │ ├── dto │ │ └── login-user.dto.ts │ ├── jwt.strategy.ts │ └── local.strategy.ts ├── cache │ └── redis.ts ├── core │ ├── filter │ │ ├── any-exception │ │ │ ├── any-exception.filter.spec.ts │ │ │ └── any-exception.filter.ts │ │ └── http-exception │ │ │ ├── http-exception.filter.spec.ts │ │ │ └── http-exception.filter.ts │ └── interceptor │ │ └── transform │ │ ├── transform.interceptor.spec.ts │ │ └── transform.interceptor.ts ├── main.ts ├── menu │ ├── dto │ │ └── menu.dto.ts │ ├── entities │ │ └── menu.entity.ts │ ├── menu.controller.spec.ts │ ├── menu.controller.ts │ ├── menu.module.ts │ ├── menu.service.spec.ts │ └── menu.service.ts ├── middleware │ └── logger │ │ ├── logger.middleware.spec.ts │ │ └── logger.middleware.ts ├── organization │ ├── dto │ │ └── organization.dto.ts │ ├── entities │ │ └── organization.entity.ts │ ├── organization.controller.spec.ts │ ├── organization.controller.ts │ ├── organization.module.ts │ ├── organization.service.spec.ts │ └── organization.service.ts ├── role │ ├── dto │ │ ├── query-role.dto.ts │ │ └── role.dto.ts │ ├── entities │ │ └── role.entity.ts │ ├── role.controller.spec.ts │ ├── role.controller.ts │ ├── role.module.ts │ ├── role.service.spec.ts │ └── role.service.ts ├── user │ ├── dto │ │ ├── create-user.dto.ts │ │ ├── login-user.dto.ts │ │ ├── query-user.dto.ts │ │ ├── update-user.dto.ts │ │ └── updatePass-user.dto.ts │ ├── entities │ │ └── user.entity.ts │ ├── user.controller.spec.ts │ ├── user.controller.ts │ ├── user.module.ts │ ├── user.service.spec.ts │ └── user.service.ts └── utils │ ├── common.ts │ ├── log4js.ts │ ├── pagination.ts │ ├── sql.ts │ └── utils.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | .env.prod 5 | .vscode 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | pnpm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # OS 17 | .DS_Store 18 | 19 | # Tests 20 | /coverage 21 | /.nyc_output 22 | 23 | # IDEs and editors 24 | /.idea 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | > **技术千千万,学习永不断;虽然是咸鱼,也想努把力!!!** 3 | > 这段时间一直很迷茫,做前端这行也有6年了,眼看马上奔三的人了,但依旧是个菜逼,整天代码写了不少,但总感觉缺少些什么,一直都没什么进步;每下定决心准备沉淀下自己,过不了几天人又变懒了;这不新一轮的沉淀开始了,本次研究基于NodeJS搭建一套实用的后台管理系统。 4 | **项目地址:** 5 | [github](https://github.com/sunshine824/Nestjs-Cli-Serve)、 6 | [gitee](https://gitee.com/sunshine824/Nestjs-Cli-Serve) 7 | 8 | ## 为什么不选择学习新后端语言 9 | 1. 如果重新学习一门后端语言,学习成本过高,再加上就算初步学会了,没有实战的机会,过不了多久就会忘掉。 10 | 2. 数据库知识还没掌握好,这时再学习新语言,精力不够。 11 | 3. 这一点才是最重要的,前面的都是借口,人太笨,理解能力太差了,担心自己学废。 12 | 13 | ## 为什么选择NestJS 14 | - 基于JavaSript,不需要重新学习新语言。 15 | - Nest (NestJS) 是一个用于构建高效、可扩展的 [`Node.js`](https://link.juejin.cn/?target=https%3A%2F%2Fnodejs.org%2F "https://nodejs.org/") 服务器端应用程序的开发框架。它利用`JavaScript` 的渐进增强的能力,使用并完全支持 [`TypeScript`](https://link.juejin.cn/?target=http%3A%2F%2Fwww.typescriptlang.org%2F "http://www.typescriptlang.org/") 16 | - 用的人多,遇到问题好查询。 17 | 18 | ## 开始前的准备 19 | 1. 一定的JavaScript、TypeScript基础 20 | 2. Redis安装(在本项目中,只用来了单点登录) 21 | 3. [MySQL](https://dev.mysql.com/downloads/mysql/)的安装,网上的教程有很多,这里就不多赘述了(我本地是安装在docker,方便管理) 22 | 4. [Nodejs & npm](https://nodejs.org/zh-cn/download/) :配置本地开发环境,安装 Node 后你会发现 npm 也会一起安装下来 (V12+) 23 | 24 | >ps:mysql安装好后,手动新建一个名为"nest_admin"的数据库,后面的表创建就交给代码 25 | 26 | ## 实现功能 27 | - [X] JWT登录注册 28 | - [X] 单点登录拦截 29 | - [X] 权限接口拦截 30 | - [X] 分页逻辑封装 31 | - [X] 日志监控系统 32 | - [X] Swagger API接入、文档生成 33 | - [ ] 文件本地上传、云端上传 34 | - [ ] Nest微服务搭建 35 | 36 | 37 | ## 项目结构 38 | 39 | ``` 40 | ├── config # 项目配置信息(数据库,redis,全局变量) 41 | ├── src 42 | ├── auth # 权限管理模块(登录认证,接口权限拦截) 43 | │   └── dto # swagger文档 44 | ├── cache # Redis缓存工具包 45 | ├── core 46 | │   ├── filter # 请求错误拦截 47 | │   │   ├── any-exception 48 | │   │   └── http-exception 49 | │   └── interceptor # 请求成功拦截 50 | │       └── transform 51 | ├── menu # 菜单管理模块 52 | │   ├── dto 53 | │   └── entities # 数据库表实体 54 | │   └── menu.controller.ts # 控制器(接口定义) 55 | │   └── menu.service.ts # 接口业务逻辑实现 56 | ├── middleware # 访问日志 57 | │   └── logger 58 | ├── organization # 组织管理模块 59 | ├── role # 权限管理模块 60 | ├── user # 用户管理模块 61 | └── utils # 工具包 62 | ``` 63 | 64 | ## 启动项目 65 | >ps:启动项目前请确保mysql,redis已启动 66 | ```bash 67 | # clone 68 | $ git clone https://github.com/sunshine824/Nestjs-Cli-Serve.git 69 | 70 | # install 71 | $ npm install 72 | 73 | # development 74 | $ npm run start 75 | 76 | # watch mode 77 | $ npm run start:dev 78 | 79 | # production mode 80 | $ npm run start:prod 81 | ``` 82 | ## swagger 83 | 启动项目之后,swagger访问地址:http://localhost:9080/docs 84 | 85 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/47865f5d6ae84d42bc225eef6ce8bc60~tplv-k3u1fbpfcp-watermark.image?) 86 | 87 | ## 最后 88 | 文章暂时就写到这,后续会单独将每个模块拆出来讨论,如果本文对您有些许帮助,麻烦动动您的金手指搓个赞❤️。 89 | 本文如果有错误和不足之处,欢迎大家在评论区指出,多多提出您宝贵的意见! 90 | 91 | 最后分享项目地址:[github](https://github.com/sunshine824/Nestjs-Cli-Serve)、 92 | [gitee](https://gitee.com/sunshine824/Nestjs-Cli-Serve) -------------------------------------------------------------------------------- /config/env.prod.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // token密钥 3 | SECRET: 'test123456', 4 | // 数据库配置 5 | DATABASE_CONFIG: { 6 | type: 'mysql', 7 | host: 'localhost', // 主机,默认为localhost 8 | port: 3306, // 端口号 9 | username: 'root', // 用户名 10 | password: '123456', // 密码 11 | database: 'nest_admin', //数据库名 12 | dateStrings: true, // 设置返回日期为字符串 13 | autoLoadEntities: true, // 使用这个配置自动导入entities 14 | synchronize: true, //根据实体自动创建数据库表, 生产环境建议关闭 15 | }, 16 | // redis配置 17 | REDIS: { 18 | port: 6379, //Redis 端口 19 | host: '127.0.0.1', //Redis 域名 20 | db: 0, 21 | family: 4, 22 | password: '123456', //'Redis 访问密码' 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /config/env.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // token密钥 3 | SECRET: 'test123456', 4 | // 数据库配置 5 | DATABASE_CONFIG: { 6 | type: 'mysql', 7 | host: 'localhost', // 主机,默认为localhost 8 | port: 3306, // 端口号 9 | username: 'root', // 用户名 10 | password: '123456', // 密码 11 | database: 'nest_admin', //数据库名 12 | dateStrings: true, // 设置返回日期为字符串 13 | autoLoadEntities: true, // 使用这个配置自动导入entities 14 | synchronize: true, //根据实体自动创建数据库表, 生产环境建议关闭 15 | }, 16 | // redis配置 17 | REDIS: { 18 | port: 6379, //Redis 端口 19 | host: '127.0.0.1', //Redis 域名 20 | db: 0, 21 | family: 4, 22 | password: '123456', //'Redis 访问密码' 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | import localEnv from './env'; 2 | import prodEnv from './env.prod'; 3 | 4 | const parseEnv = { 5 | development: localEnv, 6 | production: prodEnv, 7 | }; 8 | 9 | export default parseEnv[process.env.NODE_ENV || 'development']; 10 | -------------------------------------------------------------------------------- /config/log4jsConfig.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | const baseLogPath = path.resolve(__dirname, '../../logs'); 3 | 4 | const log4jsConfig = { 5 | appenders: { 6 | console: { type: 'console' }, // 控制打印至控制台 7 | // 统计日志 8 | access: { 9 | type: 'dateFile', // 写入文件格式,并按照日期分类 10 | filename: `${baseLogPath}/access/access.log`, // 日志文件名,会命名为:access.2021-04-01.log 11 | alwaysIncludePattern: true, // 为true, 则每个文件都会按pattern命名,否则最新的文件不会按照pattern命名 12 | pattern: 'yyyy-MM-dd', // 日期格式 13 | // maxLogSize: 10485760, // 日志大小 14 | daysToKeep: 15, // 文件保存日期15天 15 | numBackups: 3, // 配置日志文件最多存在个数 16 | compress: true, // 配置日志文件是否压缩 17 | category: 'http', // category 类型 18 | keepFileExt: true, // 是否保留文件后缀 19 | }, 20 | // 一些app的 应用日志 21 | app: { 22 | type: 'dateFile', 23 | filename: `${baseLogPath}/app-out/app.log`, 24 | alwaysIncludePattern: true, 25 | layout: { 26 | type: 'pattern', 27 | pattern: 28 | "[%d{yyyy-MM-dd hh:mm:ss SSS}] [%p] -h: %h -pid: %z msg: '%m' ", 29 | }, // 自定义的输出格式, 可参考 https://blog.csdn.net/hello_word2/article/details/79295344 30 | pattern: 'yyyy-MM-dd', 31 | daysToKeep: 30, 32 | numBackups: 3, 33 | keepFileExt: true, 34 | }, 35 | // 异常日志 36 | errorFile: { 37 | type: 'dateFile', 38 | filename: `${baseLogPath}/error/error.log`, 39 | alwaysIncludePattern: true, 40 | layout: { 41 | type: 'pattern', 42 | pattern: 43 | "[%d{yyyy-MM-dd hh:mm:ss SSS}] [%p] -h: %h -pid: %z msg: '%m' ", 44 | }, 45 | pattern: 'yyyy-MM-dd', 46 | daysToKeep: 30, 47 | numBackups: 3, 48 | keepFileExt: true, 49 | }, 50 | errors: { 51 | type: 'logLevelFilter', 52 | level: 'ERROR', 53 | appender: 'errorFile', 54 | }, 55 | }, 56 | 57 | categories: { 58 | default: { 59 | appenders: ['console', 'access', 'app', 'errors'], 60 | level: 'DEBUG', 61 | }, 62 | mysql: { appenders: ['access', 'errors'], level: 'info' }, 63 | http: { appenders: ['access'], level: 'DEBUG' }, 64 | }, 65 | }; 66 | 67 | export default log4jsConfig; 68 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn_nest_service", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^9.0.0", 25 | "@nestjs/config": "^2.2.0", 26 | "@nestjs/core": "^9.0.0", 27 | "@nestjs/jwt": "^9.0.0", 28 | "@nestjs/passport": "^9.0.0", 29 | "@nestjs/platform-express": "^9.0.0", 30 | "@nestjs/swagger": "^6.1.3", 31 | "@nestjs/typeorm": "^9.0.1", 32 | "@types/passport": "^1.0.11", 33 | "@types/passport-jwt": "^3.0.7", 34 | "@types/passport-local": "^1.0.34", 35 | "bcryptjs": "^2.4.3", 36 | "class-transformer": "^0.5.1", 37 | "class-validator": "^0.13.2", 38 | "ioredis": "^5.2.4", 39 | "log4js": "^6.7.0", 40 | "moment": "^2.29.4", 41 | "mysql2": "^2.3.3", 42 | "passport": "^0.6.0", 43 | "passport-jwt": "^4.0.0", 44 | "passport-local": "^1.0.0", 45 | "reflect-metadata": "^0.1.13", 46 | "rimraf": "^3.0.2", 47 | "rxjs": "^7.2.0", 48 | "stacktrace-js": "^2.0.2", 49 | "swagger-ui-express": "^4.6.0", 50 | "typeorm": "^0.2.45" 51 | }, 52 | "devDependencies": { 53 | "@nestjs/cli": "^9.0.0", 54 | "@nestjs/schematics": "^9.0.0", 55 | "@nestjs/testing": "^9.0.0", 56 | "@types/express": "^4.17.13", 57 | "@types/jest": "28.1.8", 58 | "@types/node": "^16.0.0", 59 | "@types/supertest": "^2.0.11", 60 | "@typescript-eslint/eslint-plugin": "^5.0.0", 61 | "@typescript-eslint/parser": "^5.0.0", 62 | "eslint": "^8.0.1", 63 | "eslint-config-prettier": "^8.3.0", 64 | "eslint-plugin-prettier": "^4.0.0", 65 | "jest": "28.1.3", 66 | "prettier": "^2.3.2", 67 | "source-map-support": "^0.5.20", 68 | "supertest": "^6.1.3", 69 | "ts-jest": "28.0.8", 70 | "ts-loader": "^9.2.3", 71 | "ts-node": "^10.0.0", 72 | "tsconfig-paths": "4.1.0", 73 | "typescript": "^4.7.4" 74 | }, 75 | "jest": { 76 | "moduleFileExtensions": [ 77 | "js", 78 | "json", 79 | "ts" 80 | ], 81 | "rootDir": "src", 82 | "testRegex": ".*\\.spec\\.ts$", 83 | "transform": { 84 | "^.+\\.(t|j)s$": "ts-jest" 85 | }, 86 | "collectCoverageFrom": [ 87 | "**/*.(t|j)s" 88 | ], 89 | "coverageDirectory": "../coverage", 90 | "testEnvironment": "node" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { AppService } from './app.service'; 4 | 5 | @ApiTags('验证') 6 | @Controller('auth') 7 | export class AppController { 8 | constructor(private readonly appService: AppService) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModule } from '@nestjs/typeorm'; 2 | import { Module } from '@nestjs/common'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { UserModule } from './user/user.module'; 6 | import { OrganizationModule } from './organization/organization.module'; 7 | import { AuthModule } from './auth/auth.module'; 8 | import { MenuModule } from './menu/menu.module'; 9 | import { RoleModule } from './role/role.module'; 10 | 11 | // 环境配置信息 12 | import envConfig from '../config'; 13 | 14 | @Module({ 15 | imports: [ 16 | TypeOrmModule.forRoot(envConfig.DATABASE_CONFIG), 17 | UserModule, 18 | OrganizationModule, 19 | AuthModule, 20 | MenuModule, 21 | RoleModule, 22 | ], 23 | controllers: [AppController], 24 | providers: [AppService], 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthController } from './auth.controller'; 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthController', () => { 6 | let controller: AuthController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [AuthController], 11 | providers: [AuthService], 12 | }).compile(); 13 | 14 | controller = module.get(AuthController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | ClassSerializerInterceptor, 10 | Req, 11 | UseGuards, 12 | UseInterceptors, 13 | } from '@nestjs/common'; 14 | import { AuthGuard } from '@nestjs/passport'; 15 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 16 | import { LoginUserDto } from './dto/login-user.dto'; 17 | import { AuthService } from './auth.service'; 18 | 19 | @ApiTags('验证') 20 | @Controller('auth') 21 | export class AuthController { 22 | constructor(private readonly authService: AuthService) {} 23 | 24 | @Post('login') 25 | @ApiOperation({ summary: '用户登录' }) 26 | @UseInterceptors(ClassSerializerInterceptor) 27 | @UseGuards(AuthGuard('local')) 28 | login(@Body() user: LoginUserDto, @Req() req) { 29 | return this.authService.login(req.user); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from 'src/user/entities/user.entity'; 6 | import { PassportModule } from '@nestjs/passport'; 7 | 8 | // 环境配置信息 9 | import envConfig from '../../config'; 10 | import { JwtModule } from '@nestjs/jwt'; 11 | import { JwtStrategy } from './jwt.strategy'; 12 | import { LocalStrategy } from './local.strategy'; 13 | 14 | const jwtModule = JwtModule.registerAsync({ 15 | useFactory: () => { 16 | return { 17 | secret: envConfig.SECRET, 18 | signOptions: { expiresIn: '4h' }, 19 | }; 20 | }, 21 | }); 22 | 23 | @Module({ 24 | imports: [TypeOrmModule.forFeature([User]), PassportModule, jwtModule], 25 | controllers: [AuthController], 26 | providers: [AuthService, LocalStrategy, JwtStrategy], 27 | exports: [jwtModule], 28 | }) 29 | export class AuthModule {} 30 | -------------------------------------------------------------------------------- /src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { RedisInstance } from 'src/cache/redis'; 5 | import { RoleEntity } from 'src/role/entities/role.entity'; 6 | import { User } from 'src/user/entities/user.entity'; 7 | import { listToTree } from 'src/utils/utils'; 8 | import { getConnection, Repository } from 'typeorm'; 9 | 10 | @Injectable() 11 | export class AuthService { 12 | constructor( 13 | @InjectRepository(User) 14 | private readonly userRepository: Repository, 15 | private jwtService: JwtService, 16 | ) {} 17 | 18 | // 获取用户信息 19 | getUser(user: Partial) { 20 | const existUser = this.userRepository.findOne({ 21 | where: { id: user.id, username: user.username }, 22 | }); 23 | 24 | return existUser; 25 | } 26 | 27 | // 生成token 28 | createToken(user: Partial) { 29 | return this.jwtService.sign({ 30 | id: user.id, 31 | username: user.username, 32 | }); 33 | } 34 | 35 | // 用户登录 36 | async login(user: Partial) { 37 | const token = this.createToken(user); 38 | const redis = new RedisInstance(0); 39 | redis.setItem(`user-token-${user.id}-${user.username}`, token, 60 * 60 * 8); 40 | 41 | const Role = await getConnection() 42 | .createQueryBuilder(RoleEntity, 'role') 43 | .where('role.id = :id', { id: user.roleId }) 44 | .leftJoinAndSelect('role.menus', 'menus') 45 | .getOne(); 46 | 47 | return { 48 | permissionList: listToTree(Role?.menus || []), // 菜单权限, 49 | userInfo: user, 50 | token, 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/auth/dto/login-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | // 进行数据验证和转换 3 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; 4 | 5 | export class LoginUserDto { 6 | @ApiProperty({ description: '用户名称' }) 7 | @IsNotEmpty({ message: '用户名不能为空' }) 8 | readonly username: string; 9 | 10 | @ApiProperty({ description: '用户密码' }) 11 | @IsNotEmpty({ message: '密码不能为空' }) 12 | readonly password: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport'; 2 | import { User } from 'src/user/entities/user.entity'; 3 | import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt'; 4 | 5 | // 环境配置信息 6 | import envConfig from '../../config'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { Repository } from 'typeorm'; 9 | import { Request } from 'express'; 10 | import { RedisInstance } from 'src/cache/redis'; 11 | import { HttpException, UnauthorizedException } from '@nestjs/common'; 12 | import { AuthService } from './auth.service'; 13 | 14 | export class JwtStrategy extends PassportStrategy(Strategy) { 15 | constructor( 16 | @InjectRepository(User) 17 | private readonly userRepository: Repository, 18 | private readonly authService: AuthService, 19 | ) { 20 | super({ 21 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 22 | secretOrKey: envConfig.SECRET, 23 | passReqToCallback: true, 24 | } as StrategyOptions); 25 | } 26 | 27 | async validate(req: Request, user: User) { 28 | // 接口token 29 | const originToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req); 30 | // redis token 31 | const redis = new RedisInstance(0); 32 | const key = `user-token-${user.id}-${user.username}`; 33 | const cacheToken = await redis.getItem(key); 34 | 35 | //单点登陆验证 36 | if (cacheToken !== originToken) { 37 | throw new HttpException( 38 | { message: '登录信息已过期,请重新登录!', code: 400 }, 39 | 200, 40 | ); 41 | } 42 | 43 | const existUser = await this.authService.getUser(user); 44 | if (!existUser) { 45 | throw new UnauthorizedException('token不正确'); 46 | } 47 | return existUser; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/auth/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { compareSync } from 'bcryptjs'; 5 | import { IStrategyOptions, Strategy } from 'passport-local'; 6 | import { User } from 'src/user/entities/user.entity'; 7 | import { Repository } from 'typeorm'; 8 | 9 | export class LocalStrategy extends PassportStrategy(Strategy) { 10 | constructor( 11 | @InjectRepository(User) 12 | private readonly userRepository: Repository, 13 | ) { 14 | super({ 15 | usernameField: 'username', 16 | passwordField: 'password', 17 | } as IStrategyOptions); 18 | } 19 | 20 | async validate(username: string, password: string) { 21 | const user = await this.userRepository.findOne({ 22 | where: { username }, 23 | }); 24 | 25 | if (!user) { 26 | throw new HttpException({ message: '用户名不存在', code: 400 }, 200); 27 | } 28 | if (!compareSync(password, user.password)) { 29 | throw new HttpException({ message: '密码错误!', code: 400 }, 200); 30 | } 31 | return user; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/cache/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | import envConfig from '../../config'; 3 | 4 | export class RedisInstance extends Redis { 5 | constructor(db: number = 0) { 6 | super({ ...envConfig.REDIS, db }) 7 | } 8 | 9 | /** 10 | * @Description: 封装设置redis缓存的方法 11 | * @param key {String} key值 12 | * @param value {String} key的值 13 | * @param seconds {Number} 过期时间 14 | * @return: Promise 15 | */ 16 | public async setItem( 17 | key: string, 18 | value: any, 19 | seconds?: number, 20 | ): Promise { 21 | value = JSON.stringify(value); 22 | if (!seconds) { 23 | await this.set(key, value); 24 | } else { 25 | await this.set(key, value, 'EX', seconds); 26 | } 27 | } 28 | 29 | /** 30 | * @Description: 设置获取redis缓存中的值 31 | * @param key {String} 32 | */ 33 | public async getItem(key: string): Promise { 34 | const data = await this.get(key); 35 | if (data) return JSON.parse(data); 36 | return null; 37 | } 38 | 39 | /** 40 | * @Description: 根据key删除redis缓存数据 41 | * @param key {String} 42 | * @return: 43 | */ 44 | public async removeItem(key: string): Promise { 45 | return await this.del(key); 46 | } 47 | 48 | /** 49 | * @Description: 清空redis的缓存 50 | * @param {type} 51 | * @return: 52 | */ 53 | public async clear(): Promise { 54 | return await this.flushall(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/core/filter/any-exception/any-exception.filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { AnyExceptionFilter } from './any-exception.filter'; 2 | 3 | describe('AnyExceptionFilter', () => { 4 | it('should be defined', () => { 5 | expect(new AnyExceptionFilter()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/core/filter/any-exception/any-exception.filter.ts: -------------------------------------------------------------------------------- 1 | // src/filter/any-exception.filter.ts 2 | /** 3 | * 捕获所有异常 4 | */ 5 | import { 6 | ExceptionFilter, 7 | Catch, 8 | ArgumentsHost, 9 | HttpException, 10 | HttpStatus, 11 | } from '@nestjs/common'; 12 | import { Logger } from '../../../utils/log4js'; 13 | 14 | @Catch() 15 | export class AllExceptionsFilter implements ExceptionFilter { 16 | catch(exception: unknown, host: ArgumentsHost) { 17 | const ctx = host.switchToHttp(); 18 | const response = ctx.getResponse(); 19 | const request = ctx.getRequest(); 20 | 21 | const status = 22 | exception instanceof HttpException 23 | ? exception.getStatus() 24 | : HttpStatus.INTERNAL_SERVER_ERROR; 25 | 26 | const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 27 | Request original url: ${request.originalUrl} 28 | Method: ${request.method} 29 | IP: ${request.ip} 30 | Status code: ${status} 31 | Response: ${exception.toString()} \n <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 32 | `; 33 | Logger.error(logFormat); 34 | 35 | response.status(200).json({ 36 | code: status, 37 | data: null, 38 | msg: `Service Error: ${exception.toString()}`, 39 | status: true, 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/core/filter/http-exception/http-exception.filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpExceptionFilter } from './http-exception.filter'; 2 | 3 | describe('HttpExceptionFilter', () => { 4 | it('should be defined', () => { 5 | expect(new HttpExceptionFilter()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/core/filter/http-exception/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | ExceptionFilter, 5 | HttpException, 6 | } from '@nestjs/common'; 7 | import { Logger } from '../../../utils/log4js'; 8 | 9 | @Catch(HttpException) 10 | export class HttpExceptionFilter implements ExceptionFilter { 11 | catch(exception: HttpException, host: ArgumentsHost) { 12 | const ctx = host.switchToHttp(); // 获取请求上下文 13 | const response = ctx.getResponse(); // 获取请求上下文中的 response对象 14 | const request = ctx.getRequest(); 15 | const status = exception.getStatus(); // 获取异常状态码 16 | const message = exception.message; 17 | // 用于接收主动发错的错误信息 18 | const { code } = exception.getResponse() as any; 19 | 20 | // 设置错误信息 21 | const errorResponse = { 22 | code: code || status, 23 | data: null, 24 | msg: message, 25 | status: true, 26 | }; 27 | 28 | const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 29 | Request original url: ${request.originalUrl} 30 | Method: ${request.method} 31 | IP: ${request.ip} 32 | Status code: ${status} 33 | Response: ${exception.toString()} \n <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 34 | `; 35 | Logger.info(logFormat); 36 | 37 | // 设置返回的状态码、请求头、发送错误信息 38 | response.status(status); 39 | response.header('Content-Type', 'application/json; charset=utf-8'); 40 | response.send(errorResponse); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/core/interceptor/transform/transform.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TransformInterceptor } from './transform.interceptor'; 2 | 3 | describe('TransformInterceptor', () => { 4 | it('should be defined', () => { 5 | expect(new TransformInterceptor()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/core/interceptor/transform/transform.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { Observable, map } from 'rxjs'; 8 | import { Logger } from '../../../utils/log4js'; 9 | 10 | @Injectable() 11 | export class TransformInterceptor implements NestInterceptor { 12 | intercept(context: ExecutionContext, next: CallHandler): Observable { 13 | const req = context.getArgByIndex(1).req; 14 | 15 | return next.handle().pipe( 16 | map((data) => { 17 | const logFormat = ` <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 18 | Request original url: ${req.originalUrl} 19 | Method: ${req.method} 20 | IP: ${req.ip} 21 | User: ${JSON.stringify(req.user)} 22 | Response data:\n ${JSON.stringify(data.data)} 23 | <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<`; 24 | Logger.info(logFormat); 25 | Logger.access(logFormat); 26 | 27 | return { 28 | data: data.result || data, 29 | code: 0, 30 | msg: data.msg, 31 | status: true, 32 | }; 33 | }), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import * as express from 'express'; 4 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 5 | import { AppModule } from './app.module'; 6 | import { HttpExceptionFilter } from './core/filter/http-exception/http-exception.filter'; 7 | import { TransformInterceptor } from './core/interceptor/transform/transform.interceptor'; 8 | import { AllExceptionsFilter } from './core/filter/any-exception/any-exception.filter'; 9 | import { LoggerMiddleware } from './middleware/logger/logger.middleware'; 10 | 11 | async function bootstrap() { 12 | const app = await NestFactory.create(AppModule); 13 | // 设置全局路由前缀 14 | app.setGlobalPrefix('api'); 15 | 16 | // 使用全局拦截器 17 | app.useGlobalInterceptors(new TransformInterceptor()); 18 | // 使用全局过滤器 19 | app.useGlobalFilters(new AllExceptionsFilter()); 20 | app.useGlobalFilters(new HttpExceptionFilter()); 21 | // 使用全局管道 22 | app.useGlobalPipes(new ValidationPipe()); 23 | app.use(express.json()); // For parsing application/json 24 | app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded 25 | // 使用中间件; 监听所有的请求路由,并打印日志 26 | app.use(new LoggerMiddleware().use); 27 | 28 | // swagger配置 29 | const config = new DocumentBuilder() 30 | .setTitle('管理后台') 31 | .setDescription('管理后台接口文档') 32 | .setVersion('V1.0') 33 | .addBearerAuth() 34 | .build(); 35 | const document = SwaggerModule.createDocument(app, config); 36 | SwaggerModule.setup('docs', app, document); 37 | 38 | await app.listen(9080); 39 | } 40 | bootstrap(); 41 | -------------------------------------------------------------------------------- /src/menu/dto/menu.dto.ts: -------------------------------------------------------------------------------- 1 | // ApiProperty:必传参数 ApiPropertyOptional:可传参数 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | // 进行数据验证和转换 4 | import { IsNotEmpty } from 'class-validator'; 5 | 6 | export class MenuDto { 7 | @ApiPropertyOptional({ description: '' }) 8 | readonly id: string; 9 | 10 | @ApiPropertyOptional({ description: '菜单名称' }) 11 | readonly name: string; 12 | 13 | @ApiPropertyOptional({ description: '菜单类型' }) 14 | readonly type: string; 15 | 16 | @ApiPropertyOptional({ description: '父级菜单id' }) 17 | readonly parentId: string; 18 | 19 | @ApiPropertyOptional({ description: '菜单URL' }) 20 | readonly url: string; 21 | 22 | @ApiPropertyOptional({ description: '排序号', default: 0 }) 23 | readonly order: number; 24 | 25 | @ApiPropertyOptional({ description: '备注' }) 26 | readonly remark: string; 27 | 28 | @ApiPropertyOptional({ description: '子菜单' }) 29 | readonly children: MenuDto[]; 30 | } 31 | -------------------------------------------------------------------------------- /src/menu/entities/menu.entity.ts: -------------------------------------------------------------------------------- 1 | import { RoleEntity } from 'src/role/entities/role.entity'; 2 | import { 3 | BeforeUpdate, 4 | Column, 5 | Entity, 6 | ManyToMany, 7 | OneToOne, 8 | PrimaryGeneratedColumn, 9 | } from 'typeorm'; 10 | 11 | @Entity('menu') 12 | export class MenuEntity { 13 | @PrimaryGeneratedColumn('uuid') 14 | id: string; 15 | 16 | @Column({ length: 100, default: '' }) 17 | name: string; // 菜单名称 18 | 19 | @Column('simple-enum', { enum: ['catalog', 'menu', 'button'] }) 20 | type: string; //菜单类型 21 | 22 | @Column() 23 | url: string; //菜单URL 24 | 25 | @Column({ default: '' }) 26 | parentId: string; // 父级菜单id 27 | 28 | @Column({ default: '' }) 29 | remark: string; //备注 30 | 31 | @Column({ default: 0 }) 32 | order: number; // 排序号 33 | 34 | @ManyToMany((type) => RoleEntity, (role) => role.menus) 35 | roles: RoleEntity[]; 36 | 37 | @Column({ 38 | name: 'create_time', 39 | type: 'timestamp', 40 | default: () => 'CURRENT_TIMESTAMP', 41 | }) 42 | createTime: Date; 43 | 44 | @Column({ 45 | name: 'update_time', 46 | type: 'timestamp', 47 | default: () => 'CURRENT_TIMESTAMP', 48 | }) 49 | updateTime: Date; 50 | 51 | @BeforeUpdate() 52 | updateTimestamp() { 53 | this.updateTime = new Date(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/menu/menu.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MenuController } from './menu.controller'; 3 | import { MenuService } from './menu.service'; 4 | 5 | describe('MenuController', () => { 6 | let controller: MenuController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [MenuController], 11 | providers: [MenuService], 12 | }).compile(); 13 | 14 | controller = module.get(MenuController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/menu/menu.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | UseGuards, 10 | Query, 11 | } from '@nestjs/common'; 12 | import { MenuService } from './menu.service'; 13 | import { 14 | ApiBearerAuth, 15 | ApiBody, 16 | ApiOperation, 17 | ApiParam, 18 | ApiQuery, 19 | ApiResponse, 20 | ApiTags, 21 | } from '@nestjs/swagger'; 22 | import { MenuDto } from './dto/menu.dto'; 23 | import { AuthGuard } from '@nestjs/passport'; 24 | 25 | @ApiTags('菜单管理') 26 | @Controller('menu') 27 | export class MenuController { 28 | constructor(private readonly menuService: MenuService) {} 29 | 30 | @Post('add') 31 | @ApiOperation({ summary: '新增菜单' }) 32 | @ApiBearerAuth() // swagger文档设置token 33 | @UseGuards(AuthGuard('jwt')) 34 | async create(@Body() post: MenuDto) { 35 | return await this.menuService.create(post); 36 | } 37 | 38 | @Post('edit') 39 | @ApiOperation({ summary: '编辑菜单' }) 40 | @ApiBearerAuth() // swagger文档设置token 41 | @UseGuards(AuthGuard('jwt')) 42 | async edit(@Body() post: MenuDto) { 43 | return await this.menuService.edit(post); 44 | } 45 | 46 | @Post('getTree') 47 | @ApiOperation({ summary: '获取菜单树' }) 48 | @ApiBearerAuth() 49 | @UseGuards(AuthGuard('jwt')) 50 | @ApiQuery({ 51 | name: 'name', 52 | required: false, 53 | description: '菜单名称', 54 | }) 55 | async getTree(@Query('name') name: string): Promise { 56 | return await this.menuService.getTree(name); 57 | } 58 | 59 | @Post('delete') 60 | @ApiOperation({ summary: '删除菜单' }) 61 | @ApiBearerAuth() 62 | @UseGuards(AuthGuard('jwt')) 63 | @ApiQuery({ 64 | name: 'id', 65 | required: true, 66 | description: '菜单id', 67 | }) 68 | async delete(@Query('id') id: string) { 69 | return await this.menuService.delete(id); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/menu/menu.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MenuService } from './menu.service'; 3 | import { MenuController } from './menu.controller'; 4 | import { MenuEntity } from './entities/menu.entity'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([MenuEntity])], 9 | controllers: [MenuController], 10 | providers: [MenuService], 11 | exports: [MenuService], 12 | }) 13 | export class MenuModule {} 14 | -------------------------------------------------------------------------------- /src/menu/menu.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MenuService } from './menu.service'; 3 | 4 | describe('MenuService', () => { 5 | let service: MenuService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [MenuService], 10 | }).compile(); 11 | 12 | service = module.get(MenuService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/menu/menu.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { listToTree } from 'src/utils/utils'; 4 | import { Like, Repository } from 'typeorm'; 5 | import { MenuDto } from './dto/menu.dto'; 6 | import { MenuEntity } from './entities/menu.entity'; 7 | 8 | @Injectable() 9 | export class MenuService { 10 | constructor( 11 | @InjectRepository(MenuEntity) 12 | private readonly menuRepository: Repository, 13 | ) {} 14 | 15 | // 创建组织结构 16 | async create(post: Partial): Promise { 17 | const { name, id } = post; 18 | if (id) { 19 | throw new HttpException({ message: '新增不需要菜单ID', code: 400 }, 200); 20 | } 21 | delete post.id; 22 | const isExist = await this.menuRepository.findOne({ name }); 23 | if (isExist) { 24 | throw new HttpException({ message: '菜单名称已存在', code: 400 }, 200); 25 | } 26 | return await this.menuRepository.save(post); 27 | } 28 | 29 | // 编辑菜单 30 | async edit(post: Partial): Promise { 31 | const { id } = post; 32 | if (!id) { 33 | throw new HttpException({ message: '菜单ID不能为空', code: 400 }, 200); 34 | } 35 | const existPost = await this.menuRepository.findOne(id); 36 | if (!existPost) { 37 | throw new HttpException( 38 | { message: `id为${id}的菜单不存在`, code: 400 }, 39 | 200, 40 | ); 41 | } 42 | const updatePost = this.menuRepository.merge(existPost, post); 43 | return this.menuRepository.save(updatePost); 44 | } 45 | 46 | // 获取菜单结构树 47 | async getTree(name: string): Promise { 48 | const data = await this.menuRepository.find({ 49 | where: { ...(name && { name: Like(`%${name}%`) }) }, // == where: `name like '%销售%'` 50 | order: { 51 | order: 'ASC', 52 | }, 53 | }); 54 | const treeData = listToTree(data); 55 | return treeData; 56 | } 57 | 58 | // 通过菜单id获取数据 59 | async getOrganizationInfo(id: string): Promise { 60 | return this.menuRepository.findOne({ id }); 61 | } 62 | 63 | // 删除菜单 64 | async delete(id: string) { 65 | const existPost = await this.menuRepository.findOne(id); 66 | if (!existPost) { 67 | throw new HttpException(`id为${id}的菜单不存在`, 400); 68 | } 69 | return await this.menuRepository.remove(existPost); 70 | } 71 | 72 | // 通过ids批量查询 73 | async findByIds(ids: string[]) { 74 | return this.menuRepository.findByIds(ids); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/middleware/logger/logger.middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import { LoggerMiddleware } from './logger.middleware'; 2 | 3 | describe('LoggerMiddleware', () => { 4 | it('should be defined', () => { 5 | expect(new LoggerMiddleware()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/middleware/logger/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import { Logger } from '../../utils/log4js'; 4 | 5 | @Injectable() 6 | export class LoggerMiddleware implements NestMiddleware { 7 | use(req: Request, res: Response, next: () => void) { 8 | const code = res.statusCode; // 响应状态码 9 | next(); 10 | // 组装日志信息 11 | const logFormat = ` >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 12 | Request original url: ${req.originalUrl} 13 | Method: ${req.method} 14 | IP: ${req.ip} 15 | Status code: ${code} 16 | Parmas: ${JSON.stringify(req.params)} 17 | Query: ${JSON.stringify(req.query)} 18 | Body: ${JSON.stringify( 19 | req.body, 20 | )} \n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 21 | `; 22 | 23 | // 根据状态码,进行日志类型区分 24 | if (code >= 500) { 25 | Logger.error(logFormat); 26 | } else if (code >= 400) { 27 | Logger.warn(logFormat); 28 | } else { 29 | Logger.access(logFormat); 30 | Logger.log(logFormat); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/organization/dto/organization.dto.ts: -------------------------------------------------------------------------------- 1 | // ApiProperty:必传参数 ApiPropertyOptional:可传参数 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | // 进行数据验证和转换 4 | import { IsNotEmpty } from 'class-validator'; 5 | 6 | export class OrganizationDto { 7 | @ApiPropertyOptional({ description: '' }) 8 | readonly id: string; 9 | 10 | @ApiPropertyOptional({ description: '组织机构名称' }) 11 | readonly name: string; 12 | 13 | @ApiPropertyOptional({ description: '父级组织机构id' }) 14 | readonly parentId: string; 15 | 16 | @ApiPropertyOptional({ description: '备注' }) 17 | readonly remark: string; 18 | 19 | @ApiPropertyOptional({ description: '子机构' }) 20 | readonly children: OrganizationDto[]; 21 | } 22 | -------------------------------------------------------------------------------- /src/organization/entities/organization.entity.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'src/user/entities/user.entity'; 2 | import { 3 | BeforeUpdate, 4 | Column, 5 | Entity, 6 | OneToOne, 7 | PrimaryGeneratedColumn, 8 | } from 'typeorm'; 9 | 10 | @Entity('organization') 11 | export class OrganizationEntity { 12 | @PrimaryGeneratedColumn('uuid') 13 | id: string; 14 | 15 | @Column({ length: 100, default: '' }) 16 | name: string; // 组织机构名称 17 | 18 | @Column({ default: '' }) 19 | parentId: string; // 父级组织id 20 | 21 | @Column({ default: '' }) 22 | remark: string; //备注 23 | 24 | @Column({ 25 | name: 'create_time', 26 | type: 'timestamp', 27 | default: () => 'CURRENT_TIMESTAMP', 28 | }) 29 | createTime: Date; 30 | 31 | @Column({ 32 | name: 'update_time', 33 | type: 'timestamp', 34 | default: () => 'CURRENT_TIMESTAMP', 35 | }) 36 | updateTime: Date; 37 | 38 | @BeforeUpdate() 39 | updateTimestamp() { 40 | this.updateTime = new Date(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/organization/organization.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { OrganizationController } from './organization.controller'; 3 | import { OrganizationService } from './organization.service'; 4 | 5 | describe('OrganizationController', () => { 6 | let controller: OrganizationController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [OrganizationController], 11 | providers: [OrganizationService], 12 | }).compile(); 13 | 14 | controller = module.get(OrganizationController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/organization/organization.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | UseGuards, 10 | Query, 11 | } from '@nestjs/common'; 12 | import { OrganizationService } from './organization.service'; 13 | import { 14 | ApiBearerAuth, 15 | ApiBody, 16 | ApiOperation, 17 | ApiParam, 18 | ApiQuery, 19 | ApiResponse, 20 | ApiTags, 21 | } from '@nestjs/swagger'; 22 | import { OrganizationDto } from './dto/organization.dto'; 23 | import { AuthGuard } from '@nestjs/passport'; 24 | 25 | @ApiTags('组织机构') 26 | @Controller('organization') 27 | export class OrganizationController { 28 | constructor(private readonly organizationService: OrganizationService) {} 29 | 30 | @Post('add') 31 | @ApiOperation({ summary: '新增组织机构' }) 32 | @ApiBearerAuth() // swagger文档设置token 33 | @UseGuards(AuthGuard('jwt')) 34 | async create(@Body() post: OrganizationDto) { 35 | return await this.organizationService.create(post); 36 | } 37 | 38 | @Post('edit') 39 | @ApiOperation({ summary: '编辑组织机构' }) 40 | @ApiBearerAuth() // swagger文档设置token 41 | @UseGuards(AuthGuard('jwt')) 42 | async edit(@Body() post: OrganizationDto) { 43 | return await this.organizationService.edit(post); 44 | } 45 | 46 | @Post('getTree') 47 | @ApiOperation({ summary: '获取组织机构树' }) 48 | @ApiBearerAuth() 49 | @UseGuards(AuthGuard('jwt')) 50 | @ApiQuery({ 51 | name: 'name', 52 | required: false, 53 | description: '组织机构名称', 54 | }) 55 | async getTree(@Query('name') name: string): Promise { 56 | return await this.organizationService.getTree(name); 57 | } 58 | 59 | @Post('delete') 60 | @ApiOperation({ summary: '删除组织' }) 61 | @ApiBearerAuth() 62 | @UseGuards(AuthGuard('jwt')) 63 | @ApiQuery({ 64 | name: 'id', 65 | required: true, 66 | description: '组织机构id', 67 | }) 68 | async delete(@Query('id') id: string) { 69 | return await this.organizationService.delete(id); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/organization/organization.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { OrganizationService } from './organization.service'; 3 | import { OrganizationController } from './organization.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { OrganizationEntity } from './entities/organization.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([OrganizationEntity])], 9 | controllers: [OrganizationController], 10 | providers: [OrganizationService], 11 | exports: [OrganizationService], 12 | }) 13 | export class OrganizationModule {} 14 | -------------------------------------------------------------------------------- /src/organization/organization.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { OrganizationService } from './organization.service'; 3 | 4 | describe('OrganizationService', () => { 5 | let service: OrganizationService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [OrganizationService], 10 | }).compile(); 11 | 12 | service = module.get(OrganizationService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/organization/organization.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { listToTree } from 'src/utils/utils'; 4 | import { Like, Repository } from 'typeorm'; 5 | import { OrganizationDto } from './dto/organization.dto'; 6 | import { OrganizationEntity } from './entities/organization.entity'; 7 | 8 | @Injectable() 9 | export class OrganizationService { 10 | constructor( 11 | @InjectRepository(OrganizationEntity) 12 | private readonly OrganizationRepository: Repository, 13 | ) {} 14 | 15 | // 创建组织结构 16 | async create(post: Partial): Promise { 17 | const { name, id } = post; 18 | if (id) { 19 | throw new HttpException({ message: '新增不需要组织ID', code: 400 }, 200); 20 | } 21 | delete post.id; 22 | const isExist = await this.OrganizationRepository.findOne({ name }); 23 | if (isExist) { 24 | throw new HttpException( 25 | { message: '组织机构名称已存在', code: 400 }, 26 | 200, 27 | ); 28 | } 29 | return await this.OrganizationRepository.save(post); 30 | } 31 | 32 | // 编辑组织结构 33 | async edit(post: Partial): Promise { 34 | const { id } = post; 35 | if (!id) { 36 | throw new HttpException({ message: '组织ID不能为空', code: 400 }, 200); 37 | } 38 | const existPost = await this.OrganizationRepository.findOne(id); 39 | if (!existPost) { 40 | throw new HttpException( 41 | { message: `id为${id}的组织机构不存在`, code: 400 }, 42 | 200, 43 | ); 44 | } 45 | const updatePost = this.OrganizationRepository.merge(existPost, post); 46 | return this.OrganizationRepository.save(updatePost); 47 | } 48 | 49 | // 获取组织结构树 50 | async getTree(name: string): Promise { 51 | const data = await this.OrganizationRepository.find({ 52 | ...(name && { name: Like(`%${name}%`) }), // == where: `name like '%销售%'` 53 | }); 54 | const treeData = listToTree(data); 55 | return treeData; 56 | } 57 | 58 | // 通过组织id获取数据 59 | async getOrganizationInfo(id: string): Promise { 60 | return this.OrganizationRepository.findOne({ id }); 61 | } 62 | 63 | // 删除组织 64 | async delete(id: string) { 65 | const posts = await this.OrganizationRepository.findByIds( 66 | id?.split(',') || [], 67 | ); 68 | if (!posts || !posts.length) { 69 | throw new HttpException(`id为${id}的菜单不存在`, 400); 70 | } 71 | posts.map((item) => { 72 | this.OrganizationRepository.remove(item); 73 | }); 74 | return '删除成功'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/role/dto/query-role.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { PaginationDto } from 'src/utils/common'; 3 | 4 | export class QueryRoleDto extends PartialType(PaginationDto) { 5 | @ApiPropertyOptional({ description: '' }) 6 | name: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/role/dto/role.dto.ts: -------------------------------------------------------------------------------- 1 | // ApiProperty:必传参数 ApiPropertyOptional:可传参数 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | // 进行数据验证和转换 4 | import { IsNotEmpty } from 'class-validator'; 5 | 6 | export class RoleDto { 7 | @ApiPropertyOptional({ description: '' }) 8 | id: string; 9 | 10 | @ApiPropertyOptional({ description: '角色名称' }) 11 | readonly name: string; 12 | 13 | @ApiPropertyOptional({ description: '角色菜单idComma' }) 14 | readonly menuIdComma: string; 15 | 16 | @ApiPropertyOptional({ description: '备注' }) 17 | readonly remark: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/role/entities/role.entity.ts: -------------------------------------------------------------------------------- 1 | import { MenuEntity } from 'src/menu/entities/menu.entity'; 2 | import { User } from 'src/user/entities/user.entity'; 3 | import { 4 | BeforeUpdate, 5 | Column, 6 | Entity, 7 | JoinTable, 8 | ManyToMany, 9 | OneToOne, 10 | PrimaryGeneratedColumn, 11 | } from 'typeorm'; 12 | 13 | @Entity('role') 14 | export class RoleEntity { 15 | @PrimaryGeneratedColumn('uuid') 16 | id: string; 17 | 18 | @Column({ length: 100, default: '' }) 19 | name: string; // 菜单名称 20 | 21 | @Column({ default: '' }) 22 | remark: string; //备注 23 | 24 | @ManyToMany((type) => MenuEntity, (menu) => menu.roles) 25 | @JoinTable({ 26 | name: 'role_menu_relation', 27 | joinColumns: [{ name: 'roleId' }], 28 | inverseJoinColumns: [{ name: 'menuId' }], 29 | }) 30 | menus: MenuEntity[]; 31 | 32 | @Column({ 33 | name: 'create_time', 34 | type: 'timestamp', 35 | default: () => 'CURRENT_TIMESTAMP', 36 | }) 37 | createTime: Date; 38 | 39 | @Column({ 40 | name: 'update_time', 41 | type: 'timestamp', 42 | default: () => 'CURRENT_TIMESTAMP', 43 | }) 44 | updateTime: Date; 45 | 46 | @BeforeUpdate() 47 | updateTimestamp() { 48 | this.updateTime = new Date(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/role/role.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RoleController } from './role.controller'; 3 | import { RoleService } from './role.service'; 4 | 5 | describe('RoleController', () => { 6 | let controller: RoleController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [RoleController], 11 | providers: [RoleService], 12 | }).compile(); 13 | 14 | controller = module.get(RoleController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/role/role.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | UseGuards, 10 | Query, 11 | } from '@nestjs/common'; 12 | import { RoleService } from './role.service'; 13 | import { RoleDto } from './dto/role.dto'; 14 | import { AuthGuard } from '@nestjs/passport'; 15 | import { 16 | ApiOperation, 17 | ApiBearerAuth, 18 | ApiTags, 19 | ApiQuery, 20 | } from '@nestjs/swagger'; 21 | import { QueryRoleDto } from './dto/query-role.dto'; 22 | 23 | @ApiTags('角色管理') 24 | @UseGuards(AuthGuard('jwt')) 25 | @Controller('role') 26 | export class RoleController { 27 | constructor(private readonly roleService: RoleService) {} 28 | 29 | @Post('add') 30 | @ApiOperation({ summary: '新增角色' }) 31 | @ApiBearerAuth() // swagger文档设置token 32 | async create(@Body() post: RoleDto) { 33 | return await this.roleService.create(post); 34 | } 35 | 36 | @Post('edit') 37 | @ApiOperation({ summary: '编辑角色' }) 38 | @ApiBearerAuth() // swagger文档设置token 39 | async edit(@Body() post: RoleDto) { 40 | return await this.roleService.edit(post); 41 | } 42 | 43 | @Post('getPage') 44 | @ApiOperation({ summary: '获取角色分页列表' }) 45 | @ApiBearerAuth() 46 | async getPage(@Body() post: QueryRoleDto) { 47 | return await this.roleService.getPage(post); 48 | } 49 | 50 | @Post('delete') 51 | @ApiOperation({ summary: '删除角色' }) 52 | @ApiBearerAuth() 53 | @ApiQuery({ 54 | name: 'id', 55 | required: true, 56 | description: '组织机构id', 57 | }) 58 | async delete(@Query('id') id: string) { 59 | return await this.roleService.delete(id); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/role/role.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RoleService } from './role.service'; 3 | import { RoleController } from './role.controller'; 4 | import { RoleEntity } from './entities/role.entity'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { MenuModule } from 'src/menu/menu.module'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([RoleEntity]), MenuModule], 10 | controllers: [RoleController], 11 | providers: [RoleService], 12 | exports: [RoleService], 13 | }) 14 | export class RoleModule {} 15 | -------------------------------------------------------------------------------- /src/role/role.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RoleService } from './role.service'; 3 | 4 | describe('RoleService', () => { 5 | let service: RoleService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [RoleService], 10 | }).compile(); 11 | 12 | service = module.get(RoleService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/role/role.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { MenuService } from 'src/menu/menu.service'; 4 | import { IPageResult, Pagination } from 'src/utils/pagination'; 5 | import { createQueryCondition } from 'src/utils/utils'; 6 | import { Repository } from 'typeorm'; 7 | import { QueryRoleDto } from './dto/query-role.dto'; 8 | import { RoleDto } from './dto/role.dto'; 9 | import { RoleEntity } from './entities/role.entity'; 10 | 11 | @Injectable() 12 | export class RoleService { 13 | constructor( 14 | @InjectRepository(RoleEntity) 15 | private readonly RoleRepository: Repository, 16 | private readonly MenuService: MenuService, 17 | ) {} 18 | 19 | // 创建角色 20 | async create(post: Partial): Promise { 21 | const { name, id, menuIdComma } = post; 22 | if (id) { 23 | throw new HttpException({ message: '新增不需要ID', code: 400 }, 200); 24 | } 25 | delete post.id; 26 | const isExist = await this.RoleRepository.findOne({ name }); 27 | if (isExist) { 28 | throw new HttpException({ message: '角色名称已存在', code: 400 }, 200); 29 | } 30 | const menus = await this.MenuService.findByIds( 31 | menuIdComma?.split(',') || [], 32 | ); 33 | const postParam: Partial = { 34 | ...post, 35 | menus: menus, 36 | }; 37 | return await this.RoleRepository.save(postParam); 38 | } 39 | 40 | // 编辑角色 41 | async edit(post: Partial): Promise { 42 | const { id, menuIdComma } = post; 43 | if (!id) { 44 | throw new HttpException({ message: '角色ID不能为空', code: 400 }, 200); 45 | } 46 | const existPost = await this.RoleRepository.findOne(id); 47 | if (!existPost) { 48 | throw new HttpException( 49 | { message: `id为${id}的角色不存在`, code: 400 }, 50 | 200, 51 | ); 52 | } 53 | const menus = await this.MenuService.findByIds( 54 | menuIdComma?.split(',') || [], 55 | ); 56 | const postParam: Partial = { 57 | ...post, 58 | menus: menus, 59 | }; 60 | const updatePost = this.RoleRepository.merge(existPost, postParam); 61 | return this.RoleRepository.save(updatePost); 62 | } 63 | 64 | // 查询角色列表(分页) 65 | async getPage(query: QueryRoleDto): Promise> { 66 | const page = (query.pageNo - 1) * query.pageSize; 67 | const limit = page + query.pageSize; 68 | const pagination = new Pagination( 69 | { current: query.pageNo, size: query.pageSize }, 70 | RoleEntity, 71 | ); 72 | const db = this.RoleRepository.createQueryBuilder('role') 73 | .skip(page) 74 | .take(limit) 75 | .where(createQueryCondition(query, ['name'])) 76 | .orderBy('create_time', 'DESC'); 77 | 78 | const result = pagination.findByPage(db); 79 | return result; 80 | } 81 | 82 | // 删除组织 83 | async delete(id: string) { 84 | const existPost = await this.RoleRepository.findOne(id); 85 | if (!existPost) { 86 | throw new HttpException(`id为${id}的角色不存在`, 400); 87 | } 88 | return await this.RoleRepository.remove(existPost); 89 | } 90 | 91 | // 通过组织id获取数据 92 | async getRoleInfo(id: string): Promise { 93 | return this.RoleRepository.findOne({ id }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/user/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | // 进行数据验证和转换 3 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; 4 | 5 | export class CreateUserDto { 6 | @ApiProperty({ description: '用户名称' }) 7 | @IsNotEmpty({ message: '用户名不能为空' }) 8 | readonly username: string; 9 | 10 | @ApiPropertyOptional({ description: '用户昵称' }) 11 | readonly nickname: string; 12 | 13 | @ApiPropertyOptional({ description: '用户手机号' }) 14 | @IsNumber({}, { message: '手机号只能为数字' }) 15 | readonly phone: number; 16 | 17 | @ApiPropertyOptional({ description: '性别' }) 18 | readonly sex: number; 19 | 20 | @ApiPropertyOptional({ description: '出生日期' }) 21 | readonly birthday: Date; 22 | 23 | @ApiProperty({ description: '用户角色id' }) 24 | readonly roleId: string; 25 | 26 | @ApiProperty({ description: '所属机构id' }) 27 | readonly organizationId: string; 28 | 29 | @ApiProperty({ description: '用户密码' }) 30 | @IsNotEmpty({ message: '密码不能为空' }) 31 | readonly password: string; 32 | 33 | @ApiPropertyOptional({ description: '用户头像' }) 34 | readonly avatar: string; 35 | 36 | @ApiPropertyOptional({ description: '用户邮箱' }) 37 | readonly email: string; 38 | } 39 | -------------------------------------------------------------------------------- /src/user/dto/login-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | // 进行数据验证和转换 3 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; 4 | 5 | export class LoginUserDto { 6 | @ApiProperty({ description: '用户名称' }) 7 | @IsNotEmpty({ message: '用户名不能为空' }) 8 | readonly username: string; 9 | 10 | @ApiProperty({ description: '用户密码' }) 11 | @IsNotEmpty({ message: '密码不能为空' }) 12 | readonly password: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/user/dto/query-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger'; 2 | // 进行数据验证和转换 3 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; 4 | import { PaginationDto } from 'src/utils/common'; 5 | 6 | export class QueryUserDto extends PartialType(PaginationDto) { 7 | @ApiPropertyOptional({ description: '用户名称', default: '' }) 8 | readonly username: string; 9 | 10 | @ApiPropertyOptional({ description: '用户昵称', default: '' }) 11 | readonly nickname: string; 12 | 13 | @ApiPropertyOptional({ description: '用户手机号', default: '' }) 14 | readonly phone: number; 15 | 16 | @ApiPropertyOptional({ description: '用户角色id', default: '' }) 17 | readonly roleId: string; 18 | 19 | @ApiPropertyOptional({ description: '所属机构id', default: '' }) 20 | readonly organizationId: string; 21 | 22 | @ApiPropertyOptional({ description: '性别', default: 0 }) 23 | readonly sex: number; 24 | } 25 | -------------------------------------------------------------------------------- /src/user/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from '@nestjs/swagger'; 2 | 3 | export class UpdateUserDto { 4 | @ApiPropertyOptional({ description: '用户昵称' }) 5 | readonly nickname: string; 6 | 7 | @ApiPropertyOptional({ description: '用户头像' }) 8 | readonly avatar: string; 9 | 10 | @ApiPropertyOptional({ description: '用户邮箱' }) 11 | readonly email: string; 12 | 13 | @ApiPropertyOptional({ description: '用户性别' }) 14 | readonly sex: number; 15 | 16 | @ApiPropertyOptional({ description: '出生日期' }) 17 | readonly birthday: Date; 18 | 19 | @ApiPropertyOptional({ description: '用户角色id', default: '' }) 20 | readonly roleId: string; 21 | 22 | @ApiPropertyOptional({ description: '所属机构id', default: '' }) 23 | readonly organizationId: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/user/dto/updatePass-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | // 进行数据验证和转换 3 | import { IsNotEmpty } from 'class-validator'; 4 | 5 | export class UpdatePassDto { 6 | @ApiProperty({ description: '旧密码' }) 7 | @IsNotEmpty({ message: '旧密码不能为空' }) 8 | readonly password: string; 9 | 10 | @ApiProperty({ description: '新密码' }) 11 | @IsNotEmpty({ message: '新密码不能为空' }) 12 | readonly newPassword: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/user/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | PrimaryGeneratedColumn, 5 | BeforeInsert, 6 | OneToOne, 7 | JoinColumn, 8 | BeforeUpdate, 9 | } from 'typeorm'; 10 | import { Exclude } from 'class-transformer'; 11 | import { OrganizationEntity } from 'src/organization/entities/organization.entity'; 12 | import { RoleEntity } from 'src/role/entities/role.entity'; 13 | const bcrypt = require('bcryptjs'); 14 | 15 | @Entity('user') 16 | export class User { 17 | @PrimaryGeneratedColumn('uuid') 18 | id: string; 19 | 20 | @Column({ length: 100 }) 21 | username: string; // 用户名 22 | 23 | @Column({ length: 100, default: '' }) 24 | nickname: string; //昵称 25 | 26 | @Column('bigint') 27 | phone: number; // 手机号 28 | 29 | @Column() 30 | sex: number; // 性别 0:男 1:女 31 | 32 | @Column({ nullable: true }) 33 | birthday: Date; // 出生日期 34 | 35 | @Column() // 表示查询时隐藏此列 36 | @Exclude() // 返回数据时忽略password,配合ClassSerializerInterceptor使用 37 | password: string; // 密码 38 | 39 | @Column({ default: '' }) 40 | avatar: string; //头像 41 | 42 | @Column({ default: '' }) 43 | email: string; 44 | 45 | @Column({ default: '' }) 46 | organizationId: string; 47 | 48 | @Column({ default: '' }) 49 | roleId: string; 50 | 51 | @Column({ 52 | name: 'create_time', 53 | type: 'timestamp', 54 | default: () => 'CURRENT_TIMESTAMP', 55 | }) 56 | createTime: Date; 57 | 58 | @Column({ 59 | name: 'update_time', 60 | type: 'timestamp', 61 | default: () => 'CURRENT_TIMESTAMP', 62 | }) 63 | updateTime: Date; 64 | 65 | @BeforeUpdate() 66 | updateTimestamp() { 67 | this.updateTime = new Date(); 68 | } 69 | 70 | @BeforeInsert() 71 | async encryptPwd() { 72 | this.password = await bcrypt.hashSync(this.password, 10); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/user/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserController } from './user.controller'; 3 | import { UserService } from './user.service'; 4 | 5 | describe('UserController', () => { 6 | let controller: UserController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [UserController], 11 | providers: [UserService], 12 | }).compile(); 13 | 14 | controller = module.get(UserController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | Query, 10 | UseInterceptors, 11 | ClassSerializerInterceptor, 12 | UseGuards, 13 | Req, 14 | } from '@nestjs/common'; 15 | import { AnyFilesInterceptor } from '@nestjs/platform-express'; 16 | import { UserService } from './user.service'; 17 | import { CreateUserDto } from './dto/create-user.dto'; 18 | import { 19 | ApiBearerAuth, 20 | ApiOperation, 21 | ApiParam, 22 | ApiQuery, 23 | ApiResponse, 24 | ApiTags, 25 | } from '@nestjs/swagger'; 26 | import { User } from './entities/user.entity'; 27 | import { AuthGuard } from '@nestjs/passport'; 28 | import { UpdateUserDto } from './dto/update-user.dto'; 29 | import { UpdatePassDto } from './dto/updatePass-user.dto'; 30 | import { QueryUserDto } from './dto/query-user.dto'; 31 | 32 | @Controller('user') 33 | @ApiTags('用户管理') 34 | export class UserController { 35 | constructor(private readonly userService: UserService) {} 36 | 37 | @Post('register') 38 | @ApiOperation({ summary: '用户注册' }) 39 | // formdata接收方式 40 | @UseInterceptors(AnyFilesInterceptor()) 41 | @UseInterceptors(ClassSerializerInterceptor) 42 | @ApiResponse({ status: 201, type: [User] }) 43 | async register(@Body() createUserDto: CreateUserDto) { 44 | return await this.userService.register(createUserDto); 45 | } 46 | 47 | @Post('update') 48 | @ApiOperation({ summary: '修改用户信息' }) 49 | @ApiBearerAuth() // swagger文档设置token 50 | @UseGuards(AuthGuard('jwt')) 51 | async update(@Req() req, @Body() data: UpdateUserDto) { 52 | const user = await this.userService.update(req.user, data); 53 | delete user.password; 54 | return user; 55 | } 56 | 57 | @Post('updatePass') 58 | @ApiOperation({ summary: '修改用户密码' }) 59 | @ApiBearerAuth() 60 | @UseGuards(AuthGuard('jwt')) 61 | updatePass(@Req() req, @Body() data: UpdatePassDto) { 62 | return this.userService.updatePass(req.user, data); 63 | } 64 | 65 | @Post('getUserInfo') 66 | @ApiOperation({ summary: '根据用户id获取用户信息' }) 67 | @ApiBearerAuth() 68 | @UseGuards(AuthGuard('jwt')) 69 | @ApiQuery({ 70 | name: 'id', 71 | required: false, 72 | description: '用户id', 73 | }) 74 | async getUserInfo(@Req() req, @Query('id') id: string) { 75 | if (!id) id = req.user.id; 76 | const res = await this.userService.getUserInfo(id); 77 | delete res.password; 78 | return res; 79 | } 80 | 81 | @Post('getPage') 82 | @ApiOperation({ summary: '用户列表' }) 83 | @ApiBearerAuth() 84 | @UseGuards(AuthGuard('jwt')) 85 | async getPage(@Body() data: QueryUserDto) { 86 | return await this.userService.getPage(data); 87 | } 88 | 89 | @Post('logout') 90 | @ApiOperation({ summary: '注销登录' }) 91 | @ApiBearerAuth() 92 | @UseGuards(AuthGuard('jwt')) 93 | logout(@Req() req) { 94 | return this.userService.logout(req.user); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserService } from './user.service'; 3 | import { UserController } from './user.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from './entities/user.entity'; 6 | import { OrganizationModule } from 'src/organization/organization.module'; 7 | import { RoleModule } from 'src/role/role.module'; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([User]), OrganizationModule, RoleModule], 11 | controllers: [UserController], 12 | providers: [UserService], 13 | }) 14 | export class UserModule {} 15 | -------------------------------------------------------------------------------- /src/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserService } from './user.service'; 3 | 4 | describe('UserService', () => { 5 | let service: UserService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UserService], 10 | }).compile(); 11 | 12 | service = module.get(UserService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { getConnection, Repository } from 'typeorm'; 4 | import { compareSync, hashSync } from 'bcryptjs'; 5 | import { CreateUserDto } from './dto/create-user.dto'; 6 | import { User } from './entities/user.entity'; 7 | import { UpdateUserDto } from './dto/update-user.dto'; 8 | import { UpdatePassDto } from './dto/updatePass-user.dto'; 9 | import { OrganizationService } from 'src/organization/organization.service'; 10 | import { RoleService } from 'src/role/role.service'; 11 | import { RedisInstance } from 'src/cache/redis'; 12 | import { IPageResult, Pagination } from 'src/utils/pagination'; 13 | import { QueryUserDto } from './dto/query-user.dto'; 14 | import { OrganizationEntity } from 'src/organization/entities/organization.entity'; 15 | import { RoleEntity } from 'src/role/entities/role.entity'; 16 | import { getUserPageSql } from 'src/utils/sql'; 17 | import { createQueryCondition } from 'src/utils/utils'; 18 | 19 | type IUser = User & { 20 | roleInfo?: RoleEntity; 21 | organizationInfo?: OrganizationEntity; 22 | }; 23 | 24 | @Injectable() 25 | export class UserService { 26 | constructor( 27 | @InjectRepository(User) 28 | private userRepository: Repository, 29 | private readonly organizationService: OrganizationService, 30 | private readonly roleService: RoleService, 31 | ) {} 32 | 33 | // 用户注册 34 | async register(createUserDto: CreateUserDto) { 35 | const { username } = createUserDto; 36 | const data = await this.userRepository.findOne({ where: { username } }); 37 | if (data) { 38 | throw new HttpException({ message: '用户已存在', code: 400 }, 200); 39 | } 40 | // 必须先create才能进@BeforeInsert 41 | createUserDto = await this.userRepository.create(createUserDto); 42 | return await this.userRepository.save(createUserDto); 43 | } 44 | 45 | // 更新用户信息 46 | async update(user: Partial, info: UpdateUserDto) { 47 | await this.userRepository 48 | .createQueryBuilder('user') 49 | .update(User) 50 | .set(info) 51 | .where('user.id=:id', { id: user.id }) 52 | .execute(); 53 | return await this.userRepository.findOne({ 54 | where: { id: user.id }, 55 | }); 56 | } 57 | 58 | // 根据用户名获取用户信息 59 | async getUserInfo(id: string): Promise { 60 | const user: IUser = await this.userRepository.findOne({ id }); 61 | const organizationInfo = await getConnection() 62 | .createQueryBuilder(OrganizationEntity, 'organization') 63 | .where('organization.id = :id', { id: user.organizationId }) 64 | .getOne(); 65 | const roleInfo = await getConnection() 66 | .createQueryBuilder(RoleEntity, 'role') 67 | .where('role.id = :id', { id: user.roleId }) 68 | .getOne(); 69 | user.roleInfo = roleInfo; 70 | user.organizationInfo = organizationInfo; 71 | return user; 72 | } 73 | 74 | // 注销登录 75 | async logout(user: Partial) { 76 | const redis = new RedisInstance(0); 77 | redis.removeItem(`user-token-${user.id}-${user.username}`); 78 | return '注销成功!'; 79 | } 80 | 81 | // 修改用户密码 82 | async updatePass(user: Partial, info: UpdatePassDto) { 83 | if (!compareSync(info.password, user.password)) { 84 | throw new HttpException({ message: '用户密码不正确', code: 400 }, 200); 85 | } 86 | if (compareSync(info.newPassword, user.password)) { 87 | throw new HttpException( 88 | { message: '新密码与旧密码一致', code: 400 }, 89 | 200, 90 | ); 91 | } 92 | await this.userRepository 93 | .createQueryBuilder('user') 94 | .update(User) 95 | .set({ password: hashSync(info.newPassword, 10) }) 96 | .where('user.id=:id', { id: user.id }) 97 | .execute(); 98 | // 清空用户redis 99 | const redis = new RedisInstance(0); 100 | redis.removeItem(`user-token-${user.id}-${user.username}`); 101 | 102 | return {}; 103 | } 104 | 105 | // 获取用户列表 106 | async getPage(query: QueryUserDto): Promise> { 107 | const page = (query.pageNo - 1) * query.pageSize; 108 | const pagination = new Pagination( 109 | { current: query.pageNo, size: query.pageSize }, 110 | User, 111 | ); 112 | // const result = pagination.findByPageSql({ 113 | // sql: getUserPageSql(), 114 | // parameters: ['u.username = :name', { name: 'chenxin' }], 115 | // }); 116 | const where = createQueryCondition(query, [ 117 | 'username', 118 | 'nickname', 119 | 'phone', 120 | 'roleId', 121 | 'organizationId', 122 | 'sex', 123 | ]); 124 | const db = await this.userRepository 125 | .createQueryBuilder('user') 126 | .leftJoinAndSelect( 127 | OrganizationEntity, 128 | 'organ', 129 | 'organ.id = user.organizationId', 130 | ) 131 | .leftJoinAndSelect(RoleEntity, 'role', 'role.id = user.roleId') 132 | .select( 133 | `user.id, user.username, user.nickname, user.avatar, user.email, user.create_time, user.phone, user.organizationId, user.roleId, user.birthday, user.sex, 134 | organ.name as organizationName, 135 | role.name as roleName 136 | `, 137 | ) 138 | // .skip(page) 139 | // .take(query.pageSize) 140 | .offset(page) 141 | .limit(query.pageSize) 142 | .where(where) 143 | .orderBy('user.create_time', 'DESC'); 144 | 145 | const result = pagination.findByPage(db, 'getRawMany'); 146 | return result; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | // 分页验证DTO接口 4 | export class PaginationDto { 5 | @ApiProperty({ description: '当前分页' }) 6 | pageNo: number; 7 | 8 | @ApiProperty({ description: '每页数据量' }) 9 | pageSize: number; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/utils/log4js.ts: -------------------------------------------------------------------------------- 1 | // 目前把实例化类配置放在 src\utils\log4js.ts 中 2 | 3 | import * as Path from 'path'; 4 | import * as Log4js from 'log4js'; 5 | import * as Util from 'util'; 6 | import * as Moment from 'moment'; // 处理时间的工具 7 | import * as StackTrace from 'stacktrace-js'; 8 | import Chalk from 'chalk'; 9 | import log4jsConfig from 'config/log4jsConfig'; 10 | import { QueryRunner } from 'typeorm'; 11 | 12 | 13 | // 定义日志级别 14 | export enum LoggerLevel { 15 | ALL = 'ALL', 16 | MARK = 'MARK', 17 | TRACE = 'TRACE', 18 | DEBUG = 'DEBUG', 19 | INFO = 'INFO', 20 | WARN = 'WARN', 21 | ERROR = 'ERROR', 22 | FATAL = 'FATAL', 23 | OFF = 'OFF', 24 | } 25 | 26 | // 内容跟踪类 27 | export class ContextTrace { 28 | constructor( 29 | public readonly context: string, 30 | public readonly path?: string, 31 | public readonly lineNumber?: number, 32 | public readonly columnNumber?: number, 33 | ) { } 34 | } 35 | 36 | // 添加用户自定义的格式化布局函数。 可参考: https://log4js-node.github.io/log4js-node/layouts.html 37 | Log4js.addLayout('json', (logConfig: any) => { 38 | return (logEvent: Log4js.LoggingEvent): string => { 39 | let moduleName: string = ''; 40 | let position: string = ''; 41 | 42 | // 日志组装 43 | const messageList: string[] = []; 44 | logEvent.data.forEach((value: any) => { 45 | if (value instanceof ContextTrace) { 46 | moduleName = value.context; 47 | // 显示触发日志的坐标(行,列) 48 | if (value.lineNumber && value.columnNumber) { 49 | position = `${value.lineNumber}, ${value.columnNumber}`; 50 | } 51 | return; 52 | } 53 | 54 | if (typeof value !== 'string') { 55 | value = Util.inspect(value, false, 3, true); 56 | } 57 | 58 | messageList.push(value); 59 | }); 60 | 61 | // 日志组成部分 62 | const messageOutput: string = messageList.join(' '); 63 | const positionOutput: string = position ? ` [${position}]` : ''; 64 | const typeOutput: string = `[${logConfig.type}] ${logEvent.pid.toString()} - `; 65 | const dateOutput: string = `${Moment(logEvent.startTime).format('YYYY-MM-DD HH:mm:ss')}`; 66 | const moduleOutput: string = moduleName ? `[${moduleName}] ` : '[LoggerService] '; 67 | let levelOutput: string = `[${logEvent.level}] ${messageOutput}`; 68 | 69 | // 根据日志级别,用不同颜色区分 70 | switch (logEvent.level.toString()) { 71 | case LoggerLevel.DEBUG: 72 | levelOutput = Chalk.green(levelOutput); 73 | break; 74 | case LoggerLevel.INFO: 75 | levelOutput = Chalk.cyan(levelOutput); 76 | break; 77 | case LoggerLevel.WARN: 78 | levelOutput = Chalk.yellow(levelOutput); 79 | break; 80 | case LoggerLevel.ERROR: 81 | levelOutput = Chalk.red(levelOutput); 82 | break; 83 | case LoggerLevel.FATAL: 84 | levelOutput = Chalk.hex('#DD4C35')(levelOutput); 85 | break; 86 | default: 87 | levelOutput = Chalk.grey(levelOutput); 88 | break; 89 | } 90 | 91 | return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow(moduleOutput)}${levelOutput}${positionOutput}`; 92 | }; 93 | }); 94 | 95 | // 注入配置 96 | Log4js.configure(log4jsConfig); 97 | 98 | // 实例化 99 | const logger = Log4js.getLogger("default"); 100 | const mysqlLogger = Log4js.getLogger('mysql'); // 添加了typeorm 日志实例 101 | logger.level = LoggerLevel.TRACE; 102 | 103 | // 定义log类方法 104 | export class Logger { 105 | static trace(...args) { 106 | logger.trace(Logger.getStackTrace(), ...args); 107 | } 108 | 109 | static debug(...args) { 110 | logger.debug(Logger.getStackTrace(), ...args); 111 | } 112 | 113 | static log(...args) { 114 | logger.info(Logger.getStackTrace(), ...args); 115 | } 116 | 117 | static info(...args) { 118 | logger.info(Logger.getStackTrace(), ...args); 119 | } 120 | 121 | static warn(...args) { 122 | logger.warn(Logger.getStackTrace(), ...args); 123 | } 124 | 125 | static warning(...args) { 126 | logger.warn(Logger.getStackTrace(), ...args); 127 | } 128 | 129 | static error(...args) { 130 | logger.error(Logger.getStackTrace(), ...args); 131 | } 132 | 133 | static fatal(...args) { 134 | logger.fatal(Logger.getStackTrace(), ...args); 135 | } 136 | 137 | static access(...args) { 138 | const loggerCustom = Log4js.getLogger('http'); 139 | loggerCustom.info(Logger.getStackTrace(), ...args); 140 | } 141 | 142 | // 日志追踪,可以追溯到哪个文件、第几行第几列 143 | // StackTrace 可参考 https://www.npmjs.com/package/stacktrace-js 144 | static getStackTrace(deep: number = 2): string { 145 | const stackList: StackTrace.StackFrame[] = StackTrace.getSync(); 146 | const stackInfo: StackTrace.StackFrame = stackList[deep]; 147 | const lineNumber: number = stackInfo.lineNumber; 148 | const columnNumber: number = stackInfo.columnNumber; 149 | const fileName: string = stackInfo.fileName; 150 | const basename: string = Path.basename(fileName); 151 | return `${basename}(line: ${lineNumber}, column: ${columnNumber}): \n`; 152 | } 153 | } 154 | 155 | 156 | // 自定义typeorm 日志器, 可参考 https://blog.csdn.net/huzzzz/article/details/103191803/ 157 | export class DbLogger implements Logger { 158 | logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { 159 | mysqlLogger.info(query); 160 | } 161 | 162 | logQueryError(error: string, query: string, parameters?: any[], queryRunner?: QueryRunner) { 163 | mysqlLogger.error(query, error); 164 | } 165 | 166 | logQuerySlow(time: number, query: string, parameters?: any[], queryRunner?: QueryRunner) { 167 | mysqlLogger.info(query, time); 168 | } 169 | 170 | logSchemaBuild(message: string, queryRunner?: QueryRunner) { 171 | mysqlLogger.info(message); 172 | } 173 | 174 | logMigration(message: string, queryRunner?: QueryRunner) { 175 | mysqlLogger.info(message); 176 | } 177 | log(level: 'log' | 'info' | 'warn', message: any, queryRunner?: QueryRunner) { 178 | switch (level) { 179 | case 'info': { 180 | mysqlLogger.info(message); 181 | break; 182 | } 183 | case 'warn': { 184 | mysqlLogger.warn(message); 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/utils/pagination.ts: -------------------------------------------------------------------------------- 1 | import { getConnection, SelectQueryBuilder } from 'typeorm'; 2 | 3 | interface IFindPageFun { 4 | tableName: string; 5 | builder: { 6 | skip: number; 7 | take: number; 8 | where: any; 9 | orderBy: any; 10 | [key: string]: any; 11 | }; 12 | [key: string]: any; 13 | } 14 | 15 | // 分页结果接口 16 | export type IPageResult = { 17 | pages?: number; // 总分页数 18 | size?: number; // 每页显示数量 19 | total?: number; // 查询总数 20 | current?: number; // 当前分页 21 | records?: T[]; // 分页列表数据 22 | }; 23 | 24 | export type ISqlOptions = { 25 | sql: string; // 原生sql 26 | parameters?: T[]; 27 | }; 28 | 29 | // 分页实例 30 | export class Pagination { 31 | private entity = null; 32 | private Page: IPageResult = { 33 | pages: 1, // 总页数 34 | size: 10, // 每页显示数量 35 | total: 0, // 总条数 36 | current: 1, // 当前分页 37 | records: [], // 分页列表数据 38 | }; 39 | 40 | constructor(params: IPageResult, entity: any) { 41 | Object.assign(this.Page, params); 42 | this.entity = entity; 43 | } 44 | 45 | // 分页查询 46 | public async findByPage(db: SelectQueryBuilder, queryFun = 'getMany') { 47 | const data = await db[queryFun](); 48 | const total = await db.getCount(); 49 | // 总页数 50 | const pages = Math.ceil(total / this.Page.size); 51 | // 返回分页数据 52 | const result = { ...this.Page, ...{ records: data, total, pages } }; 53 | return result; 54 | } 55 | 56 | //分页查询(原生) 57 | public async findByPageSql(options: ISqlOptions) { 58 | let { sql, parameters } = options; 59 | // 查找sql中select位置 60 | let selectIndex = sql.indexOf('SELECT'); 61 | selectIndex = selectIndex < 0 ? sql.indexOf('select') : selectIndex; 62 | // 查找sql中from的位置 63 | let fromIndex = sql.indexOf('from'); 64 | fromIndex = fromIndex < 0 ? sql.indexOf('FROM') : fromIndex; 65 | 66 | if (selectIndex < 0 || fromIndex < 0) { 67 | throw new Error('sql is invalid'); 68 | } 69 | 70 | const selectFields = sql.slice(selectIndex + 6, fromIndex); 71 | // 重组sql用于查询总数 72 | const countSql = sql.replace(selectFields, ' count(1) as total '); 73 | // 总条数 74 | this.Page.total = 75 | (await getConnection().query(countSql, parameters))[0].total * 1; 76 | // 总页数 77 | this.Page.pages = Math.ceil(this.Page.total / this.Page.size); 78 | const page = (this.Page.current - 1) * this.Page.size; 79 | sql = `${sql} limit ${page},${this.Page.size}`; 80 | // 查询结果数据 81 | this.Page.records = await getConnection().query(sql, parameters); 82 | 83 | return this.Page; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/sql.ts: -------------------------------------------------------------------------------- 1 | // 查询用户列表 2 | export const getUserPageSql = () => { 3 | return `select 4 | u.id, 5 | u.username, 6 | u.nickname, 7 | u.avatar, 8 | u.email, 9 | u.create_time, 10 | u.phone, 11 | u.organizationId, 12 | u.roleId, 13 | u.birthday, 14 | u.sex, 15 | o.name organizationName, 16 | r.name roleName 17 | from 18 | user u 19 | left join organization o on (u.organizationId = o.id) 20 | left join role r on(r.id = u.roleId)`; 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | interface listToTree {} 2 | 3 | /** 4 | * 递归返回树结构 5 | * @param data 原数据列表 6 | * @param replaceFields 替换字段 default:{children:'children', parentId:'parentId', id:'id'} 7 | * @returns 8 | */ 9 | export interface ReplaceFields { 10 | children?: string; 11 | parentId?: string; 12 | id?: string; 13 | [key: string]: string; 14 | } 15 | export const listToTree = (data, replaceFields?: ReplaceFields): T[] => { 16 | replaceFields = Object.assign( 17 | { children: 'children', parentId: 'parentId', id: 'id' }, 18 | replaceFields, 19 | ); 20 | const treeData: T[] = []; 21 | const mapData = {}; 22 | data.forEach((item) => { 23 | mapData[item[replaceFields['id']]] = item; 24 | }); 25 | 26 | data.forEach((item) => { 27 | const parent = mapData[item[replaceFields['parentId']]]; 28 | if (parent) { 29 | const children = parent[replaceFields['children']]; 30 | children || (parent[replaceFields['children']] = []); 31 | parent[replaceFields['children']].push(item); 32 | } else { 33 | treeData.push(item); 34 | } 35 | }); 36 | return treeData; 37 | }; 38 | 39 | /** 40 | * 生成where查询条件 41 | * @param query 接口获取到的查询参数 42 | * @param keys 需要返回的查询参数 43 | */ 44 | 45 | export const createQueryCondition = ( 46 | query: { [key: string]: any }, 47 | keys: string[], 48 | ): { [key: string]: any } => { 49 | let newMap = {}; 50 | const fields = Object.keys(query); 51 | fields.forEach((key) => { 52 | if (keys.includes(key) && query[key] != undefined && query[key] != null) { 53 | newMap[key] = query[key]; 54 | } 55 | }); 56 | return newMap; 57 | }; 58 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | --------------------------------------------------------------------------------