├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── build.mac.sh ├── config ├── config.ali.yaml ├── config.file.yaml └── config.yaml ├── nest-cli.json ├── package.json ├── src ├── account │ ├── account.module.ts │ ├── admin │ │ ├── admin.controller.ts │ │ ├── admin.dto.ts │ │ ├── admin.entity.ts │ │ ├── admin.module.ts │ │ └── admin.service.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.dto.ts │ │ ├── auth.module.ts │ │ └── jwt.strategy.ts │ ├── role │ │ ├── role.controller.ts │ │ ├── role.dto.ts │ │ ├── role.entity.ts │ │ ├── role.module.ts │ │ └── role.service.ts │ └── user │ │ ├── user.controller.ts │ │ ├── user.dto.ts │ │ ├── user.entity.ts │ │ ├── user.module.ts │ │ └── user.service.ts ├── app.module.ts ├── common │ ├── common.module.ts │ ├── controller │ │ ├── common.ts │ │ ├── index.ts │ │ ├── roles.guard.ts │ │ └── validation.pipe.ts │ ├── dto │ │ ├── account.ts │ │ ├── common.ts │ │ ├── index.ts │ │ └── infos.ts │ ├── entity │ │ ├── account.ts │ │ ├── common.ts │ │ ├── index.ts │ │ └── infos.ts │ ├── imports │ │ ├── config │ │ │ └── config.module.ts │ │ ├── index.ts │ │ ├── logger │ │ │ └── logger.module.ts │ │ └── upload │ │ │ ├── upload.controller.ts │ │ │ ├── upload.dto.ts │ │ │ ├── upload.module.ts │ │ │ └── upload.service.ts │ ├── index.ts │ ├── initialize.ts │ ├── providers │ │ ├── all.exception.filter.ts │ │ ├── index.ts │ │ └── transform.interceptor.ts │ ├── service │ │ ├── account.ts │ │ ├── common.ts │ │ └── index.ts │ └── tools │ │ ├── controller.ts │ │ ├── data.ts │ │ ├── dto.ts │ │ ├── entity.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ └── service.ts ├── infos │ ├── article │ │ ├── article.controller.ts │ │ ├── article.dto.ts │ │ ├── article.entity.ts │ │ ├── article.module.ts │ │ └── article.service.ts │ ├── category │ │ ├── category.controller.ts │ │ ├── category.dto.ts │ │ ├── category.entity.ts │ │ ├── category.module.ts │ │ └── category.service.ts │ └── infos.module.ts └── main.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.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 | /build 5 | /uploads 6 | /config/development.yaml 7 | /config/production.yaml 8 | /sqls 9 | /dist.ncc 10 | /dist.sea 11 | /cache 12 | 13 | # lock 14 | yarn.lock 15 | package-lock.json 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | pnpm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | lerna-debug.log* 25 | 26 | # OS 27 | .DS_Store 28 | 29 | # Tests 30 | /coverage 31 | /.nyc_output 32 | 33 | # IDEs and editors 34 | /.idea 35 | .project 36 | .classpath 37 | .c9/ 38 | *.launch 39 | .settings/ 40 | *.sublime-workspace 41 | 42 | # IDE - VSCode 43 | .vscode/* 44 | !.vscode/settings.json 45 | !.vscode/tasks.json 46 | !.vscode/launch.json 47 | !.vscode/extensions.json 48 | 49 | # dotenv environment variable files 50 | .env 51 | .env.development.local 52 | .env.test.local 53 | .env.production.local 54 | .env.local 55 | 56 | # temp directory 57 | .temp 58 | .tmp 59 | 60 | # Runtime data 61 | pids 62 | *.pid 63 | *.seed 64 | *.pid.lock 65 | 66 | # Diagnostic reports (https://nodejs.org/api/report.html) 67 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 68 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nest Serve v4 2 | 3 | [Nestjs 10.x 中文开发文档](https://docs.nestjs.cn/10/firststeps)
4 | 使用 Nestjs 10.x 开发的基础管理后台服务,极大简约了代码,降低开发成本,支持打包单js文件,无需安装依赖即可部署,并且可基于sea打包成单个可执行文件,无需安装node即可运行。
5 | 6 | 7 | ## 使用方式 8 | 9 | ### 开发与打包 10 | 11 | ```sh 12 | npm run start:dev # 开发 13 | ``` 14 | 15 | ### 打包 16 | 17 | ```sh 18 | npm run build # 打包生产环境 19 | npm run build:ncc # 打包单文件 20 | ``` 21 | 22 | ### 部署 23 | 24 | ```sh 25 | node ./dist/main.js # 运行服务 26 | node ./dist.ncc/index.js # 运行单文件服务 27 | 28 | # 建议使用 pm2 运行 29 | pm2 start ./dist/main.js # 运行服务 30 | pm2 start ./dist.ncc/index.js # 运行单文件服务 31 | ``` 32 | 33 | ### 打包单个可执行文件 34 | 35 | 使用Node版本 >= 20以上的 sea 能力
36 | 打包成功后,可以将 dist.sea 文件夹拷贝到服务器上直接运行
37 | 命令行后的参数代表node的分发平台版本后缀 38 | 39 | ```sh 40 | # 打包 41 | ./build.mac.sh darwin-arm64 # mac M芯片 42 | ./build.mac.sh linux-x64 # linux 43 | ./build.mac.sh win-x64 # win 44 | 45 | # 运行 46 | ./dist.sea/run/nest-serve-darwin-arm64 47 | ./dist.sea/run/nest-serve-linux-x64 48 | ./dist.sea/run/nest-serve-win-x64.exe 49 | ``` 50 | 51 | ## 旧版本 52 | 53 | - [v3 使用 monorepo 模式开发](https://github.com/dyb881/nest-serve/tree/monorepo-v3) 54 | - [v2 使用 monorepo 模式开发](https://github.com/dyb881/nest-serve/tree/monorepo) 55 | - [v1 使用 multirepo 模式开发](https://github.com/dyb881/nest-serve/tree/multirepo) 56 | 57 | ## 配置 58 | 59 | 一般情况下可以直接用当前配置,但如果要区分环境的话,就需要在 config 文件夹下添加这两个文件 60 | 61 | - development.yaml 62 | - production.yaml 63 | 64 | 在运行时会根据环境变量 NODE_ENV=配置文件名 进行选择加载,如 65 | 66 | ```sh 67 | NODE_ENV=production yarn start // 加载 production.yaml 覆盖配置 68 | ``` 69 | 70 | 环境变量为空时,默认会尝试加载 development.yaml 71 | 72 | ## 文件目录 73 | 74 | - common 公共模块 75 | - tools 工具函数、二次封装的装饰器 76 | - imports 默认模块 77 | - config 配置模块 78 | - logger 日志模块 79 | - upload 文件上传模块 80 | - 支付模块(待定) 81 | - providers 数据/异常拦截 82 | - controller 公共控制器 83 | - dto 公共数据对象 84 | - entity 公共数据实体 85 | - service 公共服务 86 | - initialize.ts 项目初始化流程 87 | - account 帐号模块 88 | - role 帐号角色 89 | - admin 管理员帐号 90 | - user 用户帐号 91 | - auth 授权模块 92 | - infos 信息模块 93 | - category 基础信息分类 94 | - article 文章管理 95 | - products 商品模块(待定) 96 | - 商品分类 97 | - 商品管理 98 | - trade 交易模块(待定) 99 | - 购物车 100 | - 订单管理 -------------------------------------------------------------------------------- /build.mac.sh: -------------------------------------------------------------------------------- 1 | platform=$1 # linux-x64 win-x64 darwin-arm64 2 | 3 | delimiter="################################################################" 4 | dividingLine="----------------------------------------------------------------" 5 | 6 | log() { 7 | datetime=$(date +'%F %H:%M:%S') 8 | echo "\033[32m" 9 | printf "\n%s\n" "${delimiter}" 10 | printf "${datetime}\t$1" 11 | printf "\n%s\n" "${delimiter}" 12 | echo "\033[0m" 13 | } 14 | 15 | sub_log() { 16 | datetime=$(date +'%F %H:%M:%S') 17 | printf "\n%s\n" "${dividingLine}" 18 | printf "${datetime}\t$1" 19 | printf "\n%s\n" "${dividingLine}" 20 | } 21 | 22 | check_folder() { 23 | folder=$1 24 | if [ -d $folder ]; then 25 | sub_log "文件夹已存在:${folder}" 26 | else 27 | mkdir ${folder} 28 | sub_log "文件夹已创建:${folder}" 29 | fi 30 | } 31 | 32 | log "预构建项目执行代码" 33 | 34 | sub_log "删除上一次打包" 35 | rm -rf ./dist.sea 36 | 37 | sub_log "打包单文件" 38 | npm run build 39 | node_modules/.bin/ncc build ./dist/main.js -o dist.sea/run 40 | 41 | sub_log "拷贝配置到待打包目录" 42 | cp -R ./config ./dist.sea 43 | 44 | sub_log "生成sea可执行文件" 45 | echo "const { createRequire } = require('node:module');\nrequire = createRequire(__filename);\n\n" >temp 46 | cat temp ./dist.sea/run/index.js >./dist.sea/run/nest-serve.js 47 | rm -rf temp 48 | 49 | sub_log "生成blob文件" 50 | echo '{"main": "./dist.sea/run/nest-serve.js","output": "./dist.sea/run/prep.blob","disableExperimentalSEAWarning": true}' >./config.json 51 | node --experimental-sea-config ./config.json 52 | rm -rf ./config.json 53 | 54 | log "下载打包资源" 55 | 56 | check_folder ./cache 57 | 58 | node_version=$(node -v) # node 版本 59 | node_platform_path="node-${node_version}-${platform}" # 分发版本路径 60 | node_file_suffix="tar.gz" # 压缩包后缀 61 | if [ $platform == 'win-x64' ]; then 62 | node_file_suffix="zip" 63 | fi 64 | node_zip_path="./cache/${node_platform_path}.${node_file_suffix}" # 压缩包文件名 65 | 66 | if [ -d "./cache/${node_platform_path}" ]; then 67 | sub_log "${node_platform_path}已存在" 68 | else 69 | sub_log "下载${node_platform_path}" 70 | wget "https://nodejs.org/dist/${node_version}/${node_platform_path}.${node_file_suffix}" -O ${node_zip_path} 71 | 72 | sub_log "解压${node_platform_path}" 73 | if [ $platform == 'win-x64' ]; then 74 | unzip ${node_zip_path} 75 | else 76 | tar -zxf ${node_zip_path} 77 | fi 78 | mv ./${node_platform_path} ./cache/${node_platform_path} 79 | rm -rf ${node_zip_path} 80 | fi 81 | 82 | sub_log "创建node可执行文件的副本并根据需要命名" 83 | node_file_name="bin/node" # node 可执行文件名 84 | run_file="nest-serve" # 拷贝后的执行文件名 85 | if [ $platform == 'win-x64' ]; then 86 | node_file_name="node.exe" 87 | run_file="nest-serve.exe" 88 | fi 89 | cp ./cache/${node_platform_path}/${node_file_name} ./dist.sea/run/${run_file} 90 | 91 | if [ $platform == 'darwin-arm64' ]; then 92 | sub_log "删除二进制文件的签名" 93 | codesign --remove-signature ./dist.sea/run/${run_file} 94 | fi 95 | 96 | sub_log "将blob注入到复制的二进制文件中" 97 | npx postject ./dist.sea/run/${run_file} NODE_SEA_BLOB ./dist.sea/run/prep.blob \ 98 | --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \ 99 | --macho-segment-name NODE_SEA 100 | 101 | if [ $platform == 'darwin-arm64' ]; then 102 | sub_log "签署二进制文件" 103 | codesign --sign - ./dist.sea/run/${run_file} 104 | fi 105 | 106 | sub_log "下载 sqlite 预构建文件" 107 | sqlite_file_name=$platform 108 | if [ $platform == 'win-x64' ]; then 109 | sqlite_file_name='win32-x64' 110 | fi 111 | sqlite_file_name="sqlite3-v5.1.7-napi-v6-${sqlite_file_name}" 112 | 113 | if [ -d "./cache/${sqlite_file_name}" ]; then 114 | sub_log "${sqlite_file_name}已存在" 115 | else 116 | sub_log "下载${sqlite_file_name}" 117 | sqlite_file_url="https://github.com/TryGhost/node-sqlite3/releases/download/v5.1.7/${sqlite_file_name}.tar.gz" 118 | sqlite_zip_path="./cache/${sqlite_file_name}.tar.gz" 119 | 120 | wget ${sqlite_file_url} -O ${sqlite_zip_path} 121 | 122 | sub_log "解压${sqlite_file_name}" 123 | mkdir ./cache/${sqlite_file_name} 124 | tar -zxf $sqlite_zip_path -C ./cache/$sqlite_file_name 125 | rm -rf $sqlite_zip_path 126 | fi 127 | 128 | cp -R ./cache/${sqlite_file_name}/build ./dist.sea/run 129 | 130 | log "打包成功" 131 | -------------------------------------------------------------------------------- /config/config.ali.yaml: -------------------------------------------------------------------------------- 1 | # 阿里云相关服务配置 2 | 3 | ali: 4 | accessKeyId: 5 | accessKeySecret: 6 | 7 | # OSS配置 8 | oss: 9 | bucket: 10 | region: 11 | internal: false 12 | secure: true 13 | arn: -------------------------------------------------------------------------------- /config/config.file.yaml: -------------------------------------------------------------------------------- 1 | # 上传文件存放目录 2 | 3 | uploadPath: uploads 4 | 5 | # 上传文件静态服务器 6 | 7 | uploadHost: http://localhost:3000 8 | 9 | # 文件限制配置 10 | 11 | fileLimit: 12 | image: 13 | name: 图片 14 | maxSizeMB: 5 15 | suffixs: [jpg, png, jpeg, gif, ico] 16 | video: 17 | name: 视频 18 | maxSizeMB: 100 19 | suffixs: [mp4, webm] 20 | audio: 21 | name: 音频 22 | maxSizeMB: 20 23 | suffixs: [mp3] 24 | document: 25 | name: 文档 26 | maxSizeMB: 50 27 | suffixs: [doc, docx, ppt, pptx, xls, xlsx, pdf, md, txt] 28 | -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | # 服务配置(每个单独的服务都需要配置对应的端口) 2 | 3 | serve: 4 | port: 3000 5 | prefix: api # 路径前缀 6 | 7 | # 数据库配置 8 | 9 | db: 10 | type: sqlite # 默认使用 sqlite,如果需要使用其他数据库,请安装对应驱动器 11 | database: 'sqls/db.sql' 12 | autoLoadEntities: true # 自动加载实体 13 | synchronize: true # 自动同步表 14 | 15 | # 缓存配置 16 | 17 | cache: 18 | # 使用文件数据库缓存 19 | sqlite: 20 | database: 'sqls/cache.sql' # 缓存数据库 21 | # 其他持久化储存方式:https://github.com/jaredwray/keyv 22 | 23 | # 鉴权配置 24 | 25 | jwt: 26 | secret: DZhu1yNJcAYYrGsktfQE # 加密串,生产环境必须更改 27 | expiresIn: 604800000 # 过期时间 1000 * 60 * 60 * 24 * 7 毫秒 = 7 天 28 | validateKey: 7kk2RGVBmWM9nuxEsvon # 验证key,生产环境必须更改 29 | 30 | # Swagger 配置 31 | 32 | swagger: 33 | title: PartTime接口文档 34 | description: code:状态码,message:提示信息,data:返回值 35 | path: swagger 36 | 37 | # 日志存放目录 38 | 39 | logsPath: logs 40 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-serve", 3 | "version": "0.0.1", 4 | "description": "使用 Nestjs 10.x 开发的基础管理后台服务,极大简约了代码,降低开发成本,支持打包单js文件,无需安装依赖即可部署,并且可基于sea打包成单个可执行文件,无需安装node即可运行。", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "build:ncc": "npm run build && ncc build ./dist/main.js -o dist.ncc/run && cp -R ./config ./dist.ncc", 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 | "@keyv/sqlite": "^4.0.1", 25 | "@nestjs/cache-manager": "^2.3.0", 26 | "@nestjs/common": "^10.4.12", 27 | "@nestjs/config": "^3.3.0", 28 | "@nestjs/core": "^10.4.12", 29 | "@nestjs/jwt": "^10.2.0", 30 | "@nestjs/passport": "^10.0.3", 31 | "@nestjs/platform-express": "^10.4.12", 32 | "@nestjs/serve-static": "^4.0.2", 33 | "@nestjs/swagger": "^8.0.7", 34 | "@nestjs/typeorm": "^10.0.2", 35 | "@vercel/ncc": "^0.38.3", 36 | "ali-oss": "^6.21.0", 37 | "cache-manager": "5.7.6", 38 | "class-transformer": "^0.5.1", 39 | "class-validator": "^0.14.1", 40 | "dayjs": "^1.11.13", 41 | "js-sha512": "^0.9.0", 42 | "js-yaml": "^4.1.0", 43 | "lodash": "^4.17.21", 44 | "multer": "1.4.5-lts.1", 45 | "nest-winston": "^1.9.7", 46 | "nuid": "^2.0.1-2", 47 | "passport": "^0.7.0", 48 | "passport-jwt": "^4.0.1", 49 | "reflect-metadata": "^0.2.2", 50 | "request-ip": "^3.3.0", 51 | "rxjs": "^7.8.1", 52 | "sqlite3": "^5.1.7", 53 | "typeorm": "^0.3.20", 54 | "winston": "^3.17.0", 55 | "winston-daily-rotate-file": "^5.0.0" 56 | }, 57 | "devDependencies": { 58 | "@nestjs/cli": "^10.4.8", 59 | "@nestjs/schematics": "^10.2.3", 60 | "@nestjs/testing": "^10.4.12", 61 | "@types/ali-oss": "^6.16.11", 62 | "@types/express": "^5.0.0", 63 | "@types/jest": "^29.5.14", 64 | "@types/js-yaml": "^4.0.9", 65 | "@types/lodash": "^4.17.13", 66 | "@types/multer": "^1.4.12", 67 | "@types/node": "^22.10.1", 68 | "@types/passport-jwt": "^4.0.1", 69 | "@types/request-ip": "^0.0.41", 70 | "@types/supertest": "^6.0.2", 71 | "@typescript-eslint/eslint-plugin": "^8.17.0", 72 | "@typescript-eslint/parser": "^8.17.0", 73 | "eslint": "^9.16.0", 74 | "eslint-config-prettier": "^9.1.0", 75 | "eslint-plugin-prettier": "^5.2.1", 76 | "jest": "^29.7.0", 77 | "prettier": "^3.4.1", 78 | "source-map-support": "^0.5.21", 79 | "supertest": "^7.0.0", 80 | "ts-jest": "^29.2.5", 81 | "ts-loader": "^9.5.1", 82 | "ts-node": "^10.9.2", 83 | "tsconfig-paths": "^4.2.0", 84 | "typescript": "^5.7.2" 85 | }, 86 | "jest": { 87 | "moduleFileExtensions": [ 88 | "js", 89 | "json", 90 | "ts" 91 | ], 92 | "rootDir": "src", 93 | "testRegex": ".*\\.spec\\.ts$", 94 | "transform": { 95 | "^.+\\.(t|j)s$": "ts-jest" 96 | }, 97 | "collectCoverageFrom": [ 98 | "**/*.(t|j)s" 99 | ], 100 | "coverageDirectory": "../coverage", 101 | "testEnvironment": "node" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RouterModule } from '@nestjs/core'; 3 | import { RoleModule } from './role/role.module'; 4 | import { AdminModule } from './admin/admin.module'; 5 | import { UserModule } from './user/user.module'; 6 | import { AuthModule } from './auth/auth.module'; 7 | 8 | const imports = [RoleModule, AdminModule, UserModule, AuthModule]; 9 | 10 | @Module({ 11 | imports: [ 12 | ...imports, 13 | 14 | // 路由前缀定义 15 | RouterModule.register([{ path: 'account', children: imports }]), 16 | ], 17 | }) 18 | export class AccountModule {} 19 | -------------------------------------------------------------------------------- /src/account/admin/admin.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiPathAuth, CommonController } from '../../common'; 2 | import { AdminService } from './admin.service'; 3 | import { Admin } from './admin.entity'; 4 | import { 5 | AdminCreateDto, 6 | AdminUpdateDto, 7 | AdminQueryDto, 8 | AdminPaginationQueryDto, 9 | AdminPaginationDto, 10 | } from './admin.dto'; 11 | 12 | @ApiPathAuth('admin', '管理员帐号管理') 13 | export class AdminController extends CommonController( 14 | Admin, 15 | AdminCreateDto, 16 | AdminUpdateDto, 17 | AdminQueryDto, 18 | AdminPaginationQueryDto, 19 | AdminPaginationDto, 20 | AdminService, 21 | ) {} 22 | -------------------------------------------------------------------------------- /src/account/admin/admin.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DtoParam, 3 | PaginationQueryDto, 4 | PaginationDto, 5 | AccountQueryDto, 6 | AccountCreateDto, 7 | AccountUpdateDto, 8 | } from '../../common'; 9 | import { ACCOUNT_ADMIN_STATUS, Admin } from './admin.entity'; 10 | 11 | /** 12 | * 查询条件 13 | */ 14 | export class AdminQueryDto extends AccountQueryDto { 15 | @DtoParam('角色', { required: false }) 16 | roleId?: string; 17 | 18 | @DtoParam('状态', { enum: ACCOUNT_ADMIN_STATUS, isInt: true, required: false }) 19 | status?: number; 20 | } 21 | 22 | /** 23 | * 查询分页数据条件 24 | */ 25 | export class AdminPaginationQueryDto extends PaginationQueryDto(AdminQueryDto) {} 26 | 27 | /** 28 | * 分页数据 29 | */ 30 | export class AdminPaginationDto extends PaginationDto(Admin) {} 31 | 32 | /** 33 | * 创建数据对象 34 | */ 35 | export class AdminCreateDto extends AccountCreateDto { 36 | @DtoParam('角色') 37 | roleId: string; 38 | 39 | @DtoParam('状态', { enum: ACCOUNT_ADMIN_STATUS, isInt: true }) 40 | status: number; 41 | } 42 | 43 | /** 44 | * 编辑数据对象 45 | */ 46 | export class AdminUpdateDto extends AccountUpdateDto { 47 | @DtoParam('角色') 48 | roleId: string; 49 | 50 | @DtoParam('状态', { enum: ACCOUNT_ADMIN_STATUS, isInt: true }) 51 | status: number; 52 | } 53 | -------------------------------------------------------------------------------- /src/account/admin/admin.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'typeorm'; 2 | import { AccountEntity, EntityColumn } from '../../common'; 3 | 4 | /** 5 | * 状态 6 | */ 7 | export const ACCOUNT_ADMIN_STATUS = ['未审核', '已审核', '已冻结']; 8 | 9 | /** 10 | * 管理员 11 | */ 12 | @Entity() 13 | export class Admin extends AccountEntity { 14 | @EntityColumn('角色', 36) 15 | roleId: string; 16 | 17 | @EntityColumn('状态', { enum: ACCOUNT_ADMIN_STATUS }) 18 | status: number; 19 | } 20 | -------------------------------------------------------------------------------- /src/account/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Admin } from './admin.entity'; 4 | import { AdminController } from './admin.controller'; 5 | import { AdminService } from './admin.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Admin])], 9 | controllers: [AdminController], 10 | providers: [AdminService], 11 | exports: [AdminService], 12 | }) 13 | export class AdminModule {} 14 | -------------------------------------------------------------------------------- /src/account/admin/admin.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { AccountService, AccountLoginDto } from '../../common'; 3 | import { Admin, ACCOUNT_ADMIN_STATUS } from './admin.entity'; 4 | import { AdminCreateDto, AdminUpdateDto, AdminQueryDto, AdminPaginationQueryDto } from './admin.dto'; 5 | 6 | @Injectable() 7 | export class AdminService extends AccountService( 8 | Admin, 9 | AdminCreateDto, 10 | AdminUpdateDto, 11 | AdminQueryDto, 12 | AdminPaginationQueryDto, 13 | ) { 14 | /** 15 | * 登录 16 | */ 17 | login(data: AccountLoginDto) { 18 | return super.login(data, (one: Admin) => { 19 | // 验证帐号状态 20 | if (one.status !== 1) throw new UnauthorizedException(`账号${ACCOUNT_ADMIN_STATUS[one.status]}`); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/account/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Body, Req } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { JwtService } from '@nestjs/jwt'; 4 | import { CACHE_MANAGER } from '@nestjs/cache-manager'; 5 | import { Cache } from 'cache-manager'; 6 | import { ApiPath, Method, AccountEntity, AccountLoginDto } from '../../common'; 7 | 8 | import { AdminService } from '../admin/admin.service'; 9 | import { UserService } from '../user/user.service'; 10 | import { RoleService } from '../role/role.service'; 11 | 12 | import { Admin } from '../admin/admin.entity'; 13 | import { AdminAuthDto, AdminDto, UserAuthDto, UserDto } from './auth.dto'; 14 | 15 | @ApiPath('auth', '鉴权') 16 | export class AuthController { 17 | constructor( 18 | private readonly configService: ConfigService, 19 | private readonly jwtService: JwtService, 20 | private readonly adminService: AdminService, 21 | private readonly userService: UserService, 22 | private readonly roleService: RoleService, 23 | @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, 24 | ) {} 25 | 26 | /** 27 | * 生成加密串 28 | */ 29 | getToken(account: AccountEntity) { 30 | const { id, username } = account; 31 | const key = `secret-${this.configService.get('jwt.validateKey')}`; 32 | return this.jwtService.sign({ [key]: id, username }); 33 | } 34 | 35 | async getAdminRole(admin: Admin) { 36 | const role = await this.roleService.get(admin.roleId); 37 | 38 | // 账号权限写入缓存 39 | const expiresIn = this.configService.get('jwt.expiresIn'); 40 | await this.cacheManager.set(`permissions-${admin.id}`, JSON.stringify(role.permissions), expiresIn); 41 | 42 | return role; 43 | } 44 | 45 | @Method('管理员登录', ['Post', 'admin'], { res: AdminAuthDto }) 46 | async admin(@Body() data: AccountLoginDto) { 47 | const admin = await this.adminService.login(data); 48 | 49 | // 获取鉴权 token 50 | const access_token = this.getToken(admin); 51 | 52 | // 查询角色信息 53 | const role = await this.getAdminRole(admin); 54 | 55 | return { ...admin, access_token, role }; 56 | } 57 | 58 | @Method('获取管理员帐号信息', ['Get', 'admin'], { res: AdminDto, auth: true }) 59 | async getAdminInfo(@Req() req: any) { 60 | const admin = await this.adminService.get(req.user.id); 61 | 62 | // 查询角色信息 63 | const role = await this.getAdminRole(admin); 64 | 65 | return { ...admin, role }; 66 | } 67 | 68 | @Method('用户登录', ['Post', 'user'], { res: UserAuthDto }) 69 | async user(@Body() data: AccountLoginDto) { 70 | const user = await this.userService.login(data); 71 | 72 | // 获取鉴权 token 73 | const access_token = this.getToken(user); 74 | 75 | return { ...user, access_token }; 76 | } 77 | 78 | @Method('获取用户帐号信息', ['Get', 'user'], { res: UserDto, auth: true }) 79 | async getUserInfo(@Req() req: any) { 80 | const user = await this.userService.get(req.user.id); 81 | 82 | return { ...user }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/account/auth/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import { DtoParam } from '../../common'; 2 | import { Admin } from '../admin/admin.entity'; 3 | import { User } from '../user/user.entity'; 4 | 5 | /** 6 | * 管理员账号信息 7 | */ 8 | export class AdminDto extends Admin { 9 | @DtoParam('角色信息') 10 | role: any; 11 | } 12 | 13 | /** 14 | * 管理员授权信息 15 | */ 16 | export class AdminAuthDto extends AdminDto { 17 | @DtoParam('headers.Authorization="Bearer ${access_token}" 用于鉴权') 18 | access_token: string; 19 | } 20 | 21 | /** 22 | * 管理员账号信息 23 | */ 24 | export class UserDto extends User { 25 | @DtoParam('角色信息') 26 | role: any; 27 | } 28 | 29 | /** 30 | * 管理员授权信息 31 | */ 32 | export class UserAuthDto extends UserDto { 33 | @DtoParam('headers.Authorization="Bearer ${access_token}" 用于鉴权') 34 | access_token: string; 35 | } 36 | -------------------------------------------------------------------------------- /src/account/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { JwtStrategy } from './jwt.strategy'; 5 | import { AuthController } from './auth.controller'; 6 | import { AdminModule } from '../admin/admin.module'; 7 | import { UserModule } from '../user/user.module'; 8 | import { RoleModule } from '../role/role.module'; 9 | 10 | /** 11 | * 配置模块 12 | */ 13 | @Module({ 14 | imports: [ 15 | JwtModule.registerAsync({ 16 | useFactory: (configService: ConfigService) => { 17 | const { secret, expiresIn } = configService.get('jwt'); 18 | return { global: true, secret, signOptions: { expiresIn } }; 19 | }, 20 | inject: [ConfigService], 21 | }), 22 | AdminModule, 23 | UserModule, 24 | RoleModule, 25 | ], 26 | controllers: [AuthController], 27 | providers: [JwtStrategy], 28 | }) 29 | export class AuthModule {} 30 | -------------------------------------------------------------------------------- /src/account/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class JwtStrategy extends PassportStrategy(Strategy) { 8 | constructor(readonly configService: ConfigService) { 9 | super({ 10 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 11 | ignoreExpiration: false, 12 | secretOrKey: configService.get('jwt.secret'), 13 | }); 14 | } 15 | 16 | async validate(payload: any) { 17 | const key = `secret-${this.configService.get('jwt.validateKey')}`; 18 | return { id: payload[key], username: payload.username }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/account/role/role.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiPathAuth, CommonController } from '../../common'; 2 | import { RoleService } from './role.service'; 3 | import { Role } from './role.entity'; 4 | import { RoleCreateDto, RoleUpdateDto, RoleQueryDto, RolePaginationQueryDto, RolePaginationDto } from './role.dto'; 5 | 6 | @ApiPathAuth('role', '角色管理') 7 | export class RoleController extends CommonController( 8 | Role, 9 | RoleCreateDto, 10 | RoleUpdateDto, 11 | RoleQueryDto, 12 | RolePaginationQueryDto, 13 | RolePaginationDto, 14 | RoleService, 15 | ) {} 16 | -------------------------------------------------------------------------------- /src/account/role/role.dto.ts: -------------------------------------------------------------------------------- 1 | import { DtoParam, PaginationQueryDto, PaginationDto } from '../../common'; 2 | import { Role, Permissions } from './role.entity'; 3 | 4 | /** 5 | * 查询列表对象 6 | */ 7 | export class RoleQueryDto { 8 | @DtoParam('角色名称', { required: false }) 9 | name?: string; 10 | } 11 | 12 | /** 13 | * 查询分页数据条件 14 | */ 15 | export class RolePaginationQueryDto extends PaginationQueryDto(RoleQueryDto) {} 16 | 17 | /** 18 | * 分页数据 19 | */ 20 | export class RolePaginationDto extends PaginationDto(Role) {} 21 | 22 | /** 23 | * 创建数据对象 24 | */ 25 | export class RoleCreateDto { 26 | @DtoParam('角色名称') 27 | name: string; 28 | 29 | @DtoParam('权限配置') 30 | permissions: Permissions; 31 | } 32 | 33 | /** 34 | * 编辑数据对象 35 | */ 36 | export class RoleUpdateDto extends RoleCreateDto {} 37 | -------------------------------------------------------------------------------- /src/account/role/role.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { CommonEntity, EntityColumn } from '../../common'; 4 | 5 | /** 6 | * 权限动作 7 | */ 8 | export class Actions { 9 | constructor(defaultPermissions = false) { 10 | this.query = defaultPermissions; 11 | this.create = defaultPermissions; 12 | this.update = defaultPermissions; 13 | this.delete = defaultPermissions; 14 | } 15 | 16 | @ApiProperty({ description: '查询' }) 17 | query: boolean; 18 | 19 | @ApiProperty({ description: '创建' }) 20 | create: boolean; 21 | 22 | @ApiProperty({ description: '更新' }) 23 | update: boolean; 24 | 25 | @ApiProperty({ description: '删除' }) 26 | delete: boolean; 27 | } 28 | 29 | /** 30 | * 权限配置(模块名称:权限动作) 31 | */ 32 | export class Permissions { 33 | constructor(defaultPermissions?: boolean) { 34 | this.role = new Actions(defaultPermissions); 35 | this.admin = new Actions(defaultPermissions); 36 | this.user = new Actions(defaultPermissions); 37 | this.category = new Actions(defaultPermissions); 38 | this.article = new Actions(defaultPermissions); 39 | } 40 | 41 | @ApiProperty({ description: '角色管理' }) 42 | role: Actions; 43 | 44 | @ApiProperty({ description: '管理员帐号管理' }) 45 | admin: Actions; 46 | 47 | @ApiProperty({ description: '用户帐号管理' }) 48 | user: Actions; 49 | 50 | @ApiProperty({ description: '分类管理' }) 51 | category: Actions; 52 | 53 | @ApiProperty({ description: '文章管理' }) 54 | article: Actions; 55 | } 56 | 57 | /** 58 | * 角色 59 | */ 60 | @Entity() 61 | export class Role extends CommonEntity { 62 | @EntityColumn('角色名称') 63 | name: string; 64 | 65 | @EntityColumn('权限配置', { type: 'simple-json' }) 66 | permissions: Permissions; 67 | } 68 | -------------------------------------------------------------------------------- /src/account/role/role.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Role } from './role.entity'; 4 | import { RoleController } from './role.controller'; 5 | import { RoleService } from './role.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Role])], 9 | controllers: [RoleController], 10 | providers: [RoleService], 11 | exports: [RoleService], 12 | }) 13 | export class RoleModule {} 14 | -------------------------------------------------------------------------------- /src/account/role/role.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CommonService } from '../../common'; 3 | import { Role } from './role.entity'; 4 | import { RoleCreateDto, RoleUpdateDto, RoleQueryDto, RolePaginationQueryDto } from './role.dto'; 5 | 6 | @Injectable() 7 | export class RoleService extends CommonService( 8 | Role, 9 | RoleCreateDto, 10 | RoleUpdateDto, 11 | RoleQueryDto, 12 | RolePaginationQueryDto, 13 | ) {} 14 | -------------------------------------------------------------------------------- /src/account/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiPathAuth, CommonController } from '../../common'; 2 | import { UserService } from './user.service'; 3 | import { User } from './user.entity'; 4 | import { UserCreateDto, UserUpdateDto, UserQueryDto, UserPaginationQueryDto, UserPaginationDto } from './user.dto'; 5 | 6 | @ApiPathAuth('user', '用户帐号管理') 7 | export class UserController extends CommonController( 8 | User, 9 | UserCreateDto, 10 | UserUpdateDto, 11 | UserQueryDto, 12 | UserPaginationQueryDto, 13 | UserPaginationDto, 14 | UserService, 15 | ) {} 16 | -------------------------------------------------------------------------------- /src/account/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { PaginationQueryDto, PaginationDto, AccountQueryDto, AccountCreateDto, AccountUpdateDto } from '../../common'; 2 | import { User } from './user.entity'; 3 | 4 | /** 5 | * 查询条件 6 | */ 7 | export class UserQueryDto extends AccountQueryDto {} 8 | 9 | /** 10 | * 查询分页数据条件 11 | */ 12 | export class UserPaginationQueryDto extends PaginationQueryDto(UserQueryDto) {} 13 | 14 | /** 15 | * 分页数据 16 | */ 17 | export class UserPaginationDto extends PaginationDto(User) {} 18 | 19 | /** 20 | * 创建数据对象 21 | */ 22 | export class UserCreateDto extends AccountCreateDto {} 23 | 24 | /** 25 | * 编辑数据对象 26 | */ 27 | export class UserUpdateDto extends AccountUpdateDto {} 28 | -------------------------------------------------------------------------------- /src/account/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'typeorm'; 2 | import { AccountEntity } from '../../common'; 3 | 4 | /** 5 | * 用户 6 | */ 7 | @Entity() 8 | export class User extends AccountEntity {} 9 | -------------------------------------------------------------------------------- /src/account/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { User } from './user.entity'; 4 | import { UserController } from './user.controller'; 5 | import { UserService } from './user.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([User])], 9 | controllers: [UserController], 10 | providers: [UserService], 11 | exports: [UserService], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /src/account/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AccountService, AccountLoginDto } from '../../common'; 3 | import { User } from './user.entity'; 4 | import { UserCreateDto, UserUpdateDto, UserQueryDto, UserPaginationQueryDto } from './user.dto'; 5 | 6 | @Injectable() 7 | export class UserService extends AccountService( 8 | User, 9 | UserCreateDto, 10 | UserUpdateDto, 11 | UserQueryDto, 12 | UserPaginationQueryDto, 13 | ) {} 14 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommonModule } from './common'; 3 | import { AccountModule } from './account/account.module'; 4 | import { InfosModule } from './infos/infos.module'; 5 | 6 | @Module({ 7 | imports: [ 8 | CommonModule, // 公共模块 9 | AccountModule, // 帐号模块 10 | InfosModule, // 信息模块 11 | ], 12 | }) 13 | export class AppModule {} 14 | -------------------------------------------------------------------------------- /src/common/common.module.ts: -------------------------------------------------------------------------------- 1 | import { APP_PIPE, APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; 2 | import { Module, ValidationPipe } from '@nestjs/common'; 3 | import { existsSync, mkdirSync } from 'fs'; 4 | import path from 'path'; 5 | import _ from 'lodash'; 6 | import { rootPath } from './tools'; 7 | import { ConfigService } from '@nestjs/config'; 8 | import { ConfigModule, LoggerModule, UploadModule } from './imports'; 9 | import { AllExceptionFilter, TransformInterceptor } from './providers'; 10 | 11 | // 缓存 12 | import { CacheModule } from '@nestjs/cache-manager'; 13 | // 其他持久化储存方式:https://github.com/jaredwray/keyv 14 | import KeyvSqlite from '@keyv/sqlite'; 15 | 16 | // 数据库 17 | import { TypeOrmModule } from '@nestjs/typeorm'; 18 | 19 | /** 20 | * 全局模块 21 | */ 22 | @Module({ 23 | imports: [ 24 | ConfigModule, // 配置模块 25 | LoggerModule, // 日志模块 26 | UploadModule, // 文件上传 27 | // 缓存模块 28 | CacheModule.registerAsync({ 29 | isGlobal: true, 30 | inject: [ConfigService], 31 | useFactory: (configService: ConfigService) => { 32 | const database = configService.get('cache.sqlite.database'); 33 | const filePath = path.join(rootPath, database); // 绝对文件路径 34 | 35 | // 自动创建文件夹 36 | const sqlsPath = path.dirname(filePath); 37 | existsSync(sqlsPath) || mkdirSync(sqlsPath); 38 | 39 | const keyvSqlite = new KeyvSqlite(filePath); 40 | return { store: keyvSqlite }; 41 | }, 42 | }), 43 | // 数据库模块 44 | TypeOrmModule.forRootAsync({ 45 | inject: [ConfigService], 46 | useFactory: (configService: ConfigService) => { 47 | const db = configService.get('db'); 48 | if (db.type === 'sqlite') { 49 | const filePath = path.join(rootPath, db.database); // 绝对文件路径 50 | db.database = filePath; 51 | } 52 | return db; 53 | }, 54 | }), 55 | ], 56 | providers: [ 57 | // 全局使用验证管道,并统一报错处理 58 | { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true }) }, 59 | // 异常过滤器 60 | { provide: APP_FILTER, useClass: AllExceptionFilter }, 61 | // 响应参数转化拦截器 62 | { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, 63 | ], 64 | }) 65 | export class CommonModule {} 66 | -------------------------------------------------------------------------------- /src/common/controller/common.ts: -------------------------------------------------------------------------------- 1 | import { Param, Query, Body, Inject, BadRequestException } from '@nestjs/common'; 2 | import { Method } from '../tools'; 3 | import { IdsDto } from '../dto'; 4 | import { TClass } from '../service'; 5 | import { ValidationPipe } from './validation.pipe'; 6 | 7 | /** 8 | * 增刪查改控制器 9 | */ 10 | export function CommonController< 11 | Entity extends object, // 实体 12 | CreateDto extends object, // 创建 13 | UpdateDto extends object, // 更新 14 | QueryDto extends object, // 查询条件 15 | PaginationQueryDto extends object, // 分页查询条件 16 | PaginationDto extends object, // 返回分页数据 17 | Service extends { [key: string]: any }, // 对应服务 18 | >( 19 | _Entity: TClass, 20 | _CreateDto: TClass, 21 | _UpdateDto: TClass, 22 | _QueryDto: TClass, 23 | _PaginationQueryDto: TClass, 24 | _PaginationDto: TClass, 25 | _Service: TClass, 26 | ) { 27 | class CommonController { 28 | constructor(@Inject(_Service) readonly service: Service) {} 29 | 30 | @Method('查询所有数据', ['Get', 'all'], { res: [_Entity], query: _QueryDto, roles: [_Entity.name, 'query'] }) 31 | getList(@Query(new ValidationPipe(_QueryDto)) data: QueryDto) { 32 | return this.service.getList(data); 33 | } 34 | 35 | @Method('查询分页列表', 'Get', { res: _PaginationDto, query: _PaginationQueryDto, roles: [_Entity.name, 'query'] }) 36 | getListAndCount(@Query(new ValidationPipe(_PaginationQueryDto)) data: PaginationQueryDto) { 37 | return this.service.getListAndCount(data); 38 | } 39 | 40 | @Method('查询详情', ['Get', ':id'], { res: _Entity, roles: [_Entity.name, 'query'] }) 41 | get(@Param('id') id: string) { 42 | return this.service.get(id); 43 | } 44 | 45 | @Method('添加', 'Post', { body: _CreateDto, roles: [_Entity.name, 'create'] }) 46 | async create(@Body(new ValidationPipe(_CreateDto)) data: CreateDto) { 47 | await this.service.create(data); 48 | } 49 | 50 | @Method('编辑', ['Put', ':id'], { body: _UpdateDto, roles: [_Entity.name, 'update'] }) 51 | async update(@Param('id') id: string, @Body(new ValidationPipe(_UpdateDto)) data: UpdateDto) { 52 | await this.service.update(id, data); 53 | } 54 | 55 | @Method('删除', 'Delete', { body: IdsDto, roles: [_Entity.name, 'delete'] }) 56 | async delete(@Body() { ids }: IdsDto) { 57 | await this.service.delete(ids); 58 | } 59 | } 60 | 61 | return class extends CommonController {}; 62 | } 63 | -------------------------------------------------------------------------------- /src/common/controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './roles.guard'; 3 | -------------------------------------------------------------------------------- /src/common/controller/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { Reflector } from '@nestjs/core'; 2 | import { Injectable, CanActivate, ExecutionContext, LoggerService, Inject } from '@nestjs/common'; 3 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 4 | import { CACHE_MANAGER } from '@nestjs/cache-manager'; 5 | import { Cache } from 'cache-manager'; 6 | import { get } from 'lodash'; 7 | 8 | /** 9 | * 权限管理 10 | */ 11 | export const Roles = Reflector.createDecorator(); 12 | 13 | /** 14 | * 权限守卫 15 | * 根据对应权限控制 16 | */ 17 | @Injectable() 18 | export class RolesGuard implements CanActivate { 19 | constructor( 20 | private readonly reflector: Reflector, 21 | @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, 22 | @Inject(WINSTON_MODULE_NEST_PROVIDER) 23 | private readonly loggerService: LoggerService, 24 | ) {} 25 | 26 | async canActivate(context: ExecutionContext) { 27 | const roles = this.reflector.get(Roles, context.getHandler()); 28 | const request = context.switchToHttp().getRequest(); 29 | 30 | // 无权限标识的接口,直接通过 31 | if (!roles) return true; 32 | 33 | try { 34 | // 获取角色权限配置 35 | const permissionsString = await this.cacheManager.get(`permissions-${request.user.id}`); 36 | 37 | // 无缓存权限,直接拦截 38 | if (!permissionsString) return false; 39 | 40 | const permissions = JSON.parse(permissionsString); 41 | const key = roles.map((i) => i.toLowerCase()).join('.'); 42 | 43 | // 校验权限是否开启 44 | return get(permissions, key); 45 | } catch (e) { 46 | this.loggerService.error(e, '权限解析异常'); 47 | } 48 | 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/common/controller/validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; 2 | import { plainToInstance } from 'class-transformer' 3 | import { validate } from 'class-validator'; 4 | 5 | @Injectable() 6 | export class ValidationPipe implements PipeTransform { 7 | constructor(private readonly Dto: new (...args: any[]) => object) {} 8 | 9 | async transform(value: any, _: ArgumentMetadata) { 10 | const obj = plainToInstance(this.Dto, value) 11 | const errors = await validate(obj, { stopAtFirstError: true }); 12 | if (errors.length > 0) { 13 | const message = errors.map((i) => i.constraints?.matches).filter(Boolean); 14 | throw new BadRequestException(message); 15 | } 16 | 17 | return value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/dto/account.ts: -------------------------------------------------------------------------------- 1 | import { DtoParam } from '../tools'; 2 | 3 | /** 4 | * 查询分页对象 5 | * 账号常用 6 | */ 7 | export class AccountQueryDto { 8 | @DtoParam('用户名', { required: false }) 9 | username?: string; 10 | 11 | @DtoParam('手机号', { required: false }) 12 | phone?: string; 13 | 14 | @DtoParam('昵称', { required: false }) 15 | nickname?: string; 16 | } 17 | 18 | /** 19 | * 创建账号对象 20 | */ 21 | export class AccountCreateDto { 22 | @DtoParam('用户名', { matches: [/^[\d\w]{4,32}$/, '请输入正确的用户名,4-32位、字母、数字'] }) 23 | username: string; 24 | 25 | @DtoParam('密码', { matches: [/^[\d\w]{4,32}$/, '请输入正确的密码,4-32位、字母、数字'] }) 26 | password: string; 27 | 28 | @DtoParam('手机号', { 29 | required: false, 30 | matches: [ 31 | /^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[189]))\d{8}$/, 32 | '请输入正确格式的手机号', 33 | ], 34 | }) 35 | phone?: string; 36 | 37 | @DtoParam('昵称', { matches: [/^[\d\w\u4e00-\u9fa5]{2,15}$/, '请输入正确的昵称,2-32位、中文、字母、数字'] }) 38 | nickname: string; 39 | 40 | @DtoParam('头像', { required: false }) 41 | avatar?: string; 42 | } 43 | 44 | /** 45 | * 更新账号对象 46 | */ 47 | export class AccountUpdateDto { 48 | @DtoParam('密码', { required: false, matches: [/^[\d\w]{4,32}$/, '请输入正确的密码,4-32位、字母、数字'] }) 49 | password?: string; 50 | 51 | @DtoParam('手机号', { 52 | required: false, 53 | matches: [ 54 | /^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[189]))\d{8}$/, 55 | '请输入正确格式的手机号', 56 | ], 57 | }) 58 | phone?: string; 59 | 60 | @DtoParam('昵称', { matches: [/^[\d\w\u4e00-\u9fa5]{2,15}$/, '请输入正确的昵称,2-32位、中文、字母、数字'] }) 61 | nickname: string; 62 | 63 | @DtoParam('头像', { required: false }) 64 | avatar?: string; 65 | } 66 | 67 | /** 68 | * 账号登录对象 69 | */ 70 | export class AccountLoginDto { 71 | @DtoParam('用户名', { matches: [/^[\d\w]{4,32}$/, '请输入正确的用户名,4-32位、字母、数字'] }) 72 | username: string; 73 | 74 | @DtoParam('密码', { matches: [/^[\d\w]{4,32}$/, '请输入正确的密码,4-32位、字母、数字'] }) 75 | password: string; 76 | } 77 | -------------------------------------------------------------------------------- /src/common/dto/common.ts: -------------------------------------------------------------------------------- 1 | import { DtoParam } from '../tools'; 2 | 3 | /** 4 | * 生成查询分页 5 | */ 6 | export const PaginationQueryDto = any>(_Dto: Dto) => { 7 | class PaginationQueryDto extends _Dto { 8 | @DtoParam('当前页码', { isInt: true, default: 1 }) 9 | current: number; 10 | 11 | @DtoParam('每页数量', { isInt: true, default: 20 }) 12 | pageSize: number; 13 | } 14 | return class extends PaginationQueryDto {}; 15 | }; 16 | 17 | /** 18 | * 分页数据 19 | */ 20 | export const PaginationDto = (_Dto: Dto) => { 21 | class PaginationDto { 22 | @DtoParam('列表', { type: [_Dto] }) 23 | list: Dto[]; 24 | 25 | @DtoParam('总数') 26 | total: number; 27 | } 28 | return class extends PaginationDto {}; 29 | }; 30 | 31 | /** 32 | * ID 数组 33 | */ 34 | export class IdsDto { 35 | @DtoParam('ID数组') 36 | ids: string[]; 37 | } 38 | -------------------------------------------------------------------------------- /src/common/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './account'; 3 | export * from './infos'; 4 | -------------------------------------------------------------------------------- /src/common/dto/infos.ts: -------------------------------------------------------------------------------- 1 | import { DtoParam } from '../tools'; 2 | import { INFOS_STATUS } from '../entity/infos'; 3 | 4 | // ------------------------ 基础信息 ------------------------ // 5 | 6 | export class InfoBasicQueryDto { 7 | @DtoParam('标题', { required: false }) 8 | title?: string; 9 | 10 | @DtoParam('状态', { enum: INFOS_STATUS, isInt: true, required: false }) 11 | status?: number; 12 | } 13 | 14 | export class InfoBasicCreateDto { 15 | @DtoParam('标题') 16 | title: string; 17 | 18 | @DtoParam('图标', { required: false }) 19 | icon?: string; 20 | 21 | @DtoParam('优先级', { isInt: true, required: false }) 22 | priority: number; 23 | 24 | @DtoParam('状态', { enum: INFOS_STATUS, isInt: true }) 25 | status: number; 26 | } 27 | 28 | export class InfoBasicUpdateDto extends InfoBasicCreateDto {} 29 | 30 | // ------------------------ 基础信息 ------------------------ // 31 | 32 | // ------------------------ 信息分类 ------------------------ // 33 | 34 | export class InfoCategoryQueryDto extends InfoBasicQueryDto { 35 | @DtoParam('上级ID', { required: false }) 36 | parentId?: string; 37 | } 38 | 39 | export class InfoCategoryCreateDto extends InfoBasicCreateDto { 40 | @DtoParam('上级ID', { required: false }) 41 | parentId?: string; 42 | } 43 | 44 | export class InfoCategoryUpdateDto extends InfoCategoryCreateDto {} 45 | 46 | // ------------------------ 信息分类 ------------------------ // 47 | 48 | // ------------------------ 文章信息 ------------------------ // 49 | 50 | export class InfoArticleQueryDto extends InfoBasicQueryDto { 51 | @DtoParam('简介', { required: false }) 52 | summary?: string; 53 | 54 | @DtoParam('内容', { required: false }) 55 | content?: string; 56 | } 57 | 58 | export class InfoArticleCreateDto extends InfoBasicCreateDto { 59 | @DtoParam('图组', { required: false }) 60 | pictureGroup?: string[]; 61 | 62 | @DtoParam('简介', { required: false }) 63 | summary?: string; 64 | 65 | @DtoParam('内容', { required: false }) 66 | content?: string; 67 | 68 | @DtoParam('热度', { isInt: true, required: false }) 69 | hot: number; 70 | } 71 | 72 | export class InfoArticleUpdateDto extends InfoArticleCreateDto {} 73 | 74 | // ------------------------ 文章信息 ------------------------ // 75 | -------------------------------------------------------------------------------- /src/common/entity/account.ts: -------------------------------------------------------------------------------- 1 | import { dateTransformer, sha512Transformer, toIpTransformer, EntityColumn } from '../tools'; 2 | import { CommonEntity } from './common'; 3 | 4 | /** 5 | * 基础账号实体 6 | */ 7 | export class AccountEntity extends CommonEntity { 8 | @EntityColumn('用户名', 32, { unique: true }) 9 | username: string; 10 | 11 | @EntityColumn('密码', 128, { transformer: sha512Transformer, exclude: true }) 12 | password: string; 13 | 14 | @EntityColumn('注册IP', 15, { transformer: toIpTransformer, nullable: true }) 15 | reg_ip: string; 16 | 17 | @EntityColumn('登录IP', 15, { transformer: toIpTransformer, nullable: true }) 18 | login_ip: string; 19 | 20 | @EntityColumn('登录时间', { transformer: dateTransformer, nullable: true }) 21 | login_date: Date; 22 | 23 | @EntityColumn('手机号', 11, { nullable: true }) 24 | phone: string; 25 | 26 | @EntityColumn('昵称', 32) 27 | nickname: string; 28 | 29 | @EntityColumn('头像', { nullable: true }) 30 | avatar: string; 31 | } 32 | -------------------------------------------------------------------------------- /src/common/entity/common.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { dateTransformer } from '../tools'; 4 | 5 | /** 6 | * 公用实体 7 | * 一条数据必须存在的属性 8 | */ 9 | export class CommonEntity { 10 | @ApiProperty({ description: 'ID' }) 11 | @PrimaryGeneratedColumn('uuid') 12 | id: string; 13 | 14 | @ApiProperty({ description: '创建时间' }) 15 | @CreateDateColumn({ comment: '创建时间', transformer: dateTransformer }) 16 | create_date: Date; 17 | 18 | @ApiProperty({ description: '更新时间' }) 19 | @UpdateDateColumn({ comment: '更新时间', transformer: dateTransformer }) 20 | update_date: Date; 21 | } 22 | -------------------------------------------------------------------------------- /src/common/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './account'; 3 | export * from './infos'; 4 | -------------------------------------------------------------------------------- /src/common/entity/infos.ts: -------------------------------------------------------------------------------- 1 | import { EntityColumn } from '../tools'; 2 | import { CommonEntity } from './common'; 3 | 4 | /** 5 | * 状态 6 | */ 7 | export const INFOS_STATUS = ['隐藏', '显示']; 8 | 9 | /** 10 | * 基础信息实体 11 | */ 12 | export class InfoBasicEntity extends CommonEntity { 13 | @EntityColumn('标题') 14 | title: string; 15 | 16 | @EntityColumn('图标', { nullable: true }) 17 | icon: string; 18 | 19 | @EntityColumn('优先级', { default: 0 }) 20 | priority: number; 21 | 22 | @EntityColumn('状态', { enum: INFOS_STATUS }) 23 | status: number; 24 | } 25 | 26 | /** 27 | * 信息分类实体 28 | * 树状数据 29 | */ 30 | export class InfoCategoryEntity extends InfoBasicEntity { 31 | @EntityColumn('上级ID', 36, { nullable: true }) 32 | parentId: string; 33 | } 34 | 35 | /** 36 | * 文章信息实体 37 | */ 38 | export class InfoArticleEntity extends InfoBasicEntity { 39 | @EntityColumn('图组', { type: 'simple-array', nullable: true }) 40 | pictureGroup: string[]; 41 | 42 | @EntityColumn('简介', { nullable: true }) 43 | summary: string; 44 | 45 | @EntityColumn('内容', { type: 'text', nullable: true }) 46 | content: string; 47 | 48 | @EntityColumn('热度', { default: 0 }) 49 | hot: number; 50 | } 51 | -------------------------------------------------------------------------------- /src/common/imports/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule as ConfigModuleSource } from '@nestjs/config'; 3 | import { rootPath } from '../../tools'; 4 | import yaml from 'js-yaml'; 5 | import path from 'path'; 6 | import _ from 'lodash'; 7 | import fs from 'fs'; 8 | 9 | /** 10 | * 配置模块 11 | */ 12 | @Module({ 13 | imports: [ 14 | ConfigModuleSource.forRoot({ 15 | cache: true, 16 | isGlobal: true, 17 | load: [ 18 | () => { 19 | const configPath = path.join(rootPath, 'config'); // 配置文件目录 20 | const envNames = ['development.yaml', 'production.yaml']; // 环境配置 21 | let configFileNames = fs.readdirSync(configPath); // 获取所有配置文件名 22 | configFileNames = configFileNames.filter((fileName) => !envNames.includes(fileName)); // 过滤环境配置 23 | configFileNames.push(`${process.env.NODE_ENV || 'development'}.yaml`); // 插入当前环境配置 24 | 25 | // 合并配置 26 | let config: any = {}; 27 | 28 | configFileNames.forEach((fileName) => { 29 | try { 30 | const filePath = path.join(configPath, fileName); // 配置文件路径 31 | const exists = fs.existsSync(filePath); // 文件存在 32 | if (exists) { 33 | const currentConfigText = fs.readFileSync(filePath, 'utf8'); // 配置文本 34 | const currentConfig = yaml.load(currentConfigText); // 配置对象 35 | config = _.merge(config, currentConfig); // 深合并配置 36 | } 37 | } catch {} 38 | }); 39 | 40 | // 递归处理配置值 41 | config = _.cloneDeepWith(config, (value) => { 42 | if (value === null) return ''; // null 转为 空字符串 43 | }); 44 | 45 | return config; 46 | }, 47 | ], 48 | }), 49 | ], 50 | }) 51 | export class ConfigModule {} 52 | -------------------------------------------------------------------------------- /src/common/imports/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config/config.module'; // 配置模块 2 | export * from './logger/logger.module'; // 日志模块 3 | export * from './upload/upload.module'; // 文件上传 4 | -------------------------------------------------------------------------------- /src/common/imports/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { WinstonModule } from 'nest-winston'; 4 | import winston from 'winston'; 5 | import DailyRotateFile from 'winston-daily-rotate-file'; 6 | import { rootPath } from '../..//tools'; 7 | import path from 'path'; 8 | import _ from 'lodash'; 9 | 10 | // 终端加上颜色 11 | const lc = (code: number | string, text: any) => `\x1B[${code}m${text}\x1B[0m`; 12 | 13 | // 定义日志级别颜色 14 | const levelsColors = { error: 31, warn: 33, info: 32, debug: 34, verbose: 36 }; 15 | 16 | /** 17 | * 日志模块 18 | */ 19 | @Module({ 20 | imports: [ 21 | WinstonModule.forRootAsync({ 22 | inject: [ConfigService], 23 | useFactory: (configService: ConfigService) => { 24 | const logsPath = configService.get(`logsPath`); // 获取配置文件路径 25 | const filePath = path.join(rootPath, logsPath); // 绝对文件路径 26 | 27 | return { 28 | format: winston.format.combine( 29 | winston.format.ms(), 30 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), 31 | ), 32 | transports: [ 33 | new DailyRotateFile({ 34 | format: winston.format.printf((i) => { 35 | const { code, error, message, data } = i; 36 | let log = [ 37 | i.timestamp, // 添加时间 38 | `[${i.level}]`, // 添加等级 39 | i.ms, // 添加毫秒 40 | `[${i.context || i.stack[0]}]`, // 内容类型 41 | ].join(' '); 42 | 43 | // 处理打印 44 | if (error instanceof Error) { 45 | log += '\n' + error.stack; 46 | } else if (code && error && message) { 47 | log += '\n' + JSON.stringify({ code, error, message }, null, 2); 48 | } else log += '\t' + message; 49 | 50 | return log; 51 | }), 52 | filename: `${filePath}/%DATE%.log`, // 日志文件名 53 | datePattern: 'YYYY-MM-DD', // 按天生成日志文件 54 | zippedArchive: true, // 压缩日志文件 55 | maxSize: '20m', // 日志文件最大20MB 56 | maxFiles: '14d', // 保留最近 14 天的日志 57 | }), 58 | new winston.transports.Console({ 59 | format: winston.format.printf((i) => { 60 | const { code, error, message, data } = i; 61 | let log = [ 62 | lc(30, i.timestamp), // 添加时间 63 | lc(levelsColors[i.level], `[${i.level}]`), // 添加等级 64 | lc(37, i.ms), // 添加毫秒 65 | lc(33, `[${i.context || i.stack[0]}]`), // 内容类型 66 | ].join(' '); 67 | 68 | // 处理打印 69 | if (error instanceof Error) { 70 | log += '\n' + lc(31, error.stack); 71 | } else if (code && error && message) { 72 | log += '\n' + lc(31, JSON.stringify({ code, error, message }, null, 2)); 73 | } else log += '\t' + lc(32, message); 74 | 75 | return log; 76 | }), 77 | }), // 控制台输出 78 | ], 79 | exitOnError: false, // 防止意外退出 80 | }; 81 | }, 82 | }), 83 | ], 84 | }) 85 | export class LoggerModule {} 86 | -------------------------------------------------------------------------------- /src/common/imports/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { UseInterceptors, UploadedFile, Query } from '@nestjs/common'; 2 | import { FileInterceptor } from '@nestjs/platform-express'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { ApiConsumes } from '@nestjs/swagger'; 5 | import { Method, ApiPathAuth } from '../../tools'; 6 | 7 | import { UploadDto, UploadResDto, OSSSTSOptionsDto, OSSValidateDto, OSSPutObjectDto } from './upload.dto'; 8 | import { UploadService } from './upload.service'; 9 | 10 | @ApiPathAuth('', '上传文件') 11 | export class UploadController { 12 | uploadHost: string; 13 | uploadPath: string; 14 | 15 | constructor( 16 | private readonly configService: ConfigService, 17 | private readonly uploadService: UploadService, 18 | ) { 19 | this.uploadHost = this.configService.get('uploadHost'); 20 | this.uploadPath = this.configService.get('uploadPath'); 21 | } 22 | 23 | @ApiConsumes('multipart/form-data') 24 | @UseInterceptors(FileInterceptor('file')) 25 | @Method('上传到服务器', ['Post', 'upload'], { res: UploadResDto, body: UploadDto }) 26 | async uploadServer(@UploadedFile() file) { 27 | this.uploadService.verify(file); 28 | const [_, path] = file.path.split('uploads'); 29 | const url = `${this.uploadHost}/${this.uploadPath}/${path}`; 30 | return { url }; 31 | } 32 | 33 | @Method('获取临时授权(15分钟有效期,尽量在14分钟内获取新的授权)', ['Get', 'oss/sts'], { res: OSSSTSOptionsDto }) 34 | getSTS() { 35 | return this.uploadService.getSTS(); 36 | } 37 | 38 | @Method('获取OSS上传对象', ['Get', 'oss/put/object'], { res: OSSPutObjectDto }) 39 | async getPutObject(@Query() { name, size }: OSSValidateDto) { 40 | this.uploadService.verify({ name, size }); 41 | return this.uploadService.getPutObject(name); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/common/imports/upload/upload.dto.ts: -------------------------------------------------------------------------------- 1 | import { DtoParam } from '../../tools'; 2 | 3 | /** 4 | * 文件限制配置 5 | */ 6 | export class FileLimitItem { 7 | name: string; // 文件类型名称 8 | maxSizeMB: number; // 最大尺寸 M 9 | suffixs: string[]; // 后缀名 10 | } 11 | 12 | /** 13 | * 文件限制列表 14 | */ 15 | export class FileLimit { 16 | [key: string]: FileLimitItem; 17 | } 18 | 19 | /** 20 | * 文件上传数据 21 | */ 22 | export class UploadDto { 23 | @DtoParam('上传文件', { type: 'string', format: 'binary' }) 24 | file: Express.Multer.File; 25 | } 26 | 27 | /** 28 | * 上传后响应数据 29 | */ 30 | export class UploadResDto { 31 | @DtoParam('访问地址') 32 | url: string; 33 | } 34 | 35 | /** 36 | * OSS 临时授权配置 37 | */ 38 | export class OSSSTSOptionsDto { 39 | @DtoParam('阿里云 accessKeyId') 40 | accessKeyId: string; 41 | 42 | @DtoParam('阿里云 accessKeySecret') 43 | accessKeySecret: string; 44 | 45 | @DtoParam('STS 临时授权 token') 46 | stsToken: string; 47 | 48 | @DtoParam('OSS 区域') 49 | region: string; 50 | 51 | @DtoParam('OSS 桶') 52 | bucket: string; 53 | } 54 | 55 | /** 56 | * OSS 验证属性 57 | */ 58 | export class OSSValidateDto { 59 | @DtoParam('文件名') 60 | name: string; 61 | 62 | @DtoParam('文件大小') 63 | size: number; 64 | } 65 | 66 | /** 67 | * OSS 上传对象属性 68 | */ 69 | export class OSSPutObjectDto extends UploadResDto { 70 | @DtoParam('OSS对象名称') 71 | name: string; 72 | } 73 | -------------------------------------------------------------------------------- /src/common/imports/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ServeStaticModule } from '@nestjs/serve-static'; 3 | import { MulterModule } from '@nestjs/platform-express'; 4 | import { ConfigService } from '@nestjs/config'; 5 | 6 | import { existsSync, mkdirSync } from 'fs'; 7 | import { join, extname } from 'path'; 8 | import { diskStorage } from 'multer'; 9 | import { nuid } from 'nuid'; 10 | import dayjs from 'dayjs'; 11 | 12 | import { UploadController } from './upload.controller'; 13 | import { UploadService } from './upload.service'; 14 | import { rootPath } from '../../tools'; 15 | 16 | /** 17 | * 文件上传模块 18 | */ 19 | @Module({ 20 | imports: [ 21 | // 上传文件模块 22 | MulterModule.registerAsync({ 23 | inject: [ConfigService], 24 | useFactory: (configService: ConfigService) => { 25 | const uploadPath = configService.get('uploadPath'); 26 | const path = join(rootPath, uploadPath); 27 | existsSync(path) || mkdirSync(path); 28 | 29 | return { 30 | // 文件储存 31 | storage: diskStorage({ 32 | destination: function (_req, _file, cb) { 33 | const day = dayjs().format('YYYY-MM-DD'); 34 | const folder = `${path}/${day}`; 35 | existsSync(folder) || mkdirSync(folder); 36 | cb(null, folder); 37 | }, 38 | filename: (_req, { originalname }, cb) => { 39 | return cb(null, nuid.next() + extname(originalname)); 40 | }, 41 | }), 42 | }; 43 | }, 44 | }), 45 | // 静态文件服务 46 | ServeStaticModule.forRootAsync({ 47 | inject: [ConfigService], 48 | useFactory: (configService: ConfigService) => { 49 | const uploadPath = configService.get('uploadPath'); 50 | return [ 51 | { 52 | rootPath: join(rootPath, uploadPath), 53 | serveRoot: `/${uploadPath}`, 54 | renderPath: `/${uploadPath}/:path*`, 55 | exclude: ['/api/:path*', '/swagger/:path*'], 56 | }, 57 | ]; 58 | }, 59 | }), 60 | ], 61 | controllers: [UploadController], 62 | providers: [UploadService], 63 | }) 64 | export class UploadModule {} 65 | -------------------------------------------------------------------------------- /src/common/imports/upload/upload.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { FileLimit } from './upload.dto'; 4 | import { promises as fs } from 'fs'; 5 | import { findKey } from 'lodash'; 6 | import { extname } from 'path'; 7 | import dayjs from 'dayjs'; 8 | 9 | import { nuid } from 'nuid'; 10 | import OSS from 'ali-oss'; 11 | 12 | type verifyOptions = { 13 | originalname?: string; // 文件原名 14 | name?: string; // 文件名 15 | size: number; // 文件大小 16 | path?: string; // 文件路径 17 | }; 18 | 19 | type aliConfig = { 20 | accessKeyId?: string; 21 | accessKeySecret?: string; 22 | oss: { 23 | bucket: string; 24 | region: string; 25 | internal: boolean; 26 | secure: boolean; 27 | arn: string; 28 | }; 29 | }; 30 | 31 | /** 32 | * 文件上传 33 | */ 34 | @Injectable() 35 | export class UploadService { 36 | uploadPath: string; // 文件储存路径 37 | fileLimit: FileLimit; // 文件限制配置 38 | ali: aliConfig; // 阿里云相关配置 39 | 40 | constructor(private readonly configService: ConfigService) { 41 | this.uploadPath = configService.get('uploadPath'); 42 | this.fileLimit = this.configService.get('fileLimit'); 43 | this.ali = configService.get('ali'); 44 | this.initOss(); 45 | } 46 | 47 | /** 48 | * 验证文件 49 | */ 50 | verify({ originalname, name = originalname, size, path }: verifyOptions) { 51 | let error; 52 | 53 | if (!name) error = '请上传有效文件'; 54 | 55 | const suffix = extname(name).slice(1); 56 | const fileLimitKey = findKey(this.fileLimit, { suffixs: [suffix] }); 57 | if (!fileLimitKey) error = '禁止上传该类型文件'; 58 | 59 | const fileLimit = this.fileLimit[fileLimitKey]; 60 | if (size > fileLimit.maxSizeMB * 1024 * 1024) error = `${fileLimit.name}文件大小不能大于 ${fileLimit.maxSizeMB} MB`; 61 | 62 | if (error) { 63 | path && this.delete(path); 64 | throw new UnsupportedMediaTypeException(error); 65 | } 66 | } 67 | 68 | /** 69 | * 删除文件 70 | */ 71 | async delete(path: string) { 72 | try { 73 | await fs.unlink(path); 74 | } catch {} 75 | } 76 | 77 | // ------------------------------------------- 阿里云 oss 上传 start ------------------------------------------- // 78 | 79 | oss: OSS; 80 | sts: OSS.STS; 81 | policy: object; 82 | expirationSeconds = 15 * 60; // sts 到期时间/秒 83 | 84 | initOss = () => { 85 | const { accessKeyId, accessKeySecret, oss } = this.ali; 86 | const { bucket, region, internal, secure } = oss; 87 | if (!accessKeyId || !accessKeySecret) return; 88 | this.oss = new OSS({ accessKeyId, accessKeySecret, bucket, region, internal, secure }); 89 | this.sts = new OSS.STS({ accessKeyId, accessKeySecret }); 90 | this.policy = { 91 | Statement: [{ Effect: 'Allow', Action: ['oss:PutObject'], Resource: [`acs:oss:*:*:${bucket}/*`] }], 92 | Version: '1', 93 | }; 94 | }; 95 | 96 | /** 97 | * 获取临时上传密钥 98 | */ 99 | async getSTS() { 100 | const token = await this.sts.assumeRole(this.ali.oss.arn, this.policy, this.expirationSeconds); 101 | const { region, bucket } = this.ali.oss; 102 | 103 | return { 104 | accessKeyId: token.credentials.AccessKeyId, 105 | accessKeySecret: token.credentials.AccessKeySecret, 106 | stsToken: token.credentials.SecurityToken, 107 | region, 108 | bucket, 109 | }; 110 | } 111 | 112 | /** 113 | * 获取 OSS 上传对象属性 114 | */ 115 | getPutObject(fileName: string) { 116 | const { bucket, region } = this.ali.oss; 117 | const day = dayjs().format('YYYY-MM-DD'); 118 | const name = `${this.uploadPath}/${day}/${nuid.next() + extname(fileName)}`; 119 | 120 | return { 121 | name, // OSS 对象名称 122 | url: `https://${bucket}.${region}.aliyuncs.com/${name}`, // OSS 访问地址 123 | }; 124 | } 125 | 126 | // ------------------------------------------- 阿里云 oss 上传 end ------------------------------------------- // 127 | } 128 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common.module'; 2 | export * from './tools'; 3 | export * from './entity'; 4 | export * from './dto'; 5 | export * from './service'; 6 | export * from './controller'; 7 | -------------------------------------------------------------------------------- /src/common/initialize.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, LoggerService } from '@nestjs/common'; 2 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 3 | import { AdminService } from '../account/admin/admin.service'; 4 | import { RoleService } from '../account/role/role.service'; 5 | import { Permissions } from '../account/role/role.entity'; 6 | 7 | /** 8 | * 初始化项目 9 | * 新项目的情况下创建初始角色与帐号 10 | */ 11 | export const initialize = async (app: INestApplication) => { 12 | // 插入日志 13 | const loggerService = await app.resolve(WINSTON_MODULE_NEST_PROVIDER); 14 | app.useLogger(loggerService); 15 | 16 | // ------------------------------ 角色自检 start ------------------------------ // 17 | 18 | const roleService = await app.resolve(RoleService); 19 | let [role] = await roleService.getList({}); 20 | if (!role) { 21 | loggerService.log('当前无角色数据', '角色自检'); 22 | loggerService.log('创建默认角色:超级管理员', '角色自检'); 23 | loggerService.log('权限范围:所有', '角色自检'); 24 | const permissions = new Permissions(true); // 默认权限 25 | await roleService.create({ name: '超级管理员', permissions }); 26 | const roles = await roleService.getList({}); 27 | role = roles[0]; 28 | loggerService.log('默认角色创建成功', '角色自检'); 29 | } 30 | 31 | // ------------------------------ 角色自检 end ------------------------------ // 32 | 33 | // ------------------------------ 帐号自检 start ------------------------------ // 34 | 35 | const adminService = await app.resolve(AdminService); 36 | 37 | const list = await adminService.getList({}); 38 | loggerService.log(`当前存在${list.length}个管理员帐号`, '帐号自检'); 39 | 40 | if (list.length === 1 && list[0].username === 'admin') { 41 | loggerService.log('正在使用默认管理员帐号,请及时创建新的管理员帐号,删除默认帐号', '帐号自检'); 42 | loggerService.log('默认帐号:admin', '帐号自检'); 43 | loggerService.log('默认密码:admin', '帐号自检'); 44 | return; 45 | } 46 | 47 | if (list.length === 0) { 48 | loggerService.log('当前无管理员帐号,自动创建默认帐号', '帐号自检'); 49 | await adminService.create({ 50 | username: 'admin', 51 | password: 'admin', 52 | phone: '15118888888', 53 | nickname: '超级管理员', 54 | avatar: '', 55 | roleId: role.id, 56 | status: 1, 57 | }); 58 | loggerService.log('默认帐号创建成功', '帐号自检'); 59 | loggerService.log('默认帐号:admin', '帐号自检'); 60 | loggerService.log('默认密码:admin', '帐号自检'); 61 | } 62 | 63 | // ------------------------------ 帐号自检 end ------------------------------ // 64 | }; 65 | -------------------------------------------------------------------------------- /src/common/providers/all.exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpException, 6 | HttpStatus, 7 | Inject, 8 | LoggerService, 9 | } from '@nestjs/common'; 10 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 11 | import { requestLogger } from '../tools'; 12 | 13 | /** 14 | * 报错过滤器 15 | */ 16 | @Catch() 17 | export class AllExceptionFilter implements ExceptionFilter { 18 | constructor( 19 | @Inject(WINSTON_MODULE_NEST_PROVIDER) 20 | private readonly loggerService: LoggerService, 21 | ) {} 22 | 23 | catch(exception: any, host: ArgumentsHost) { 24 | const ctx = host.switchToHttp(); 25 | const request = ctx.getRequest(); 26 | const response = ctx.getResponse(); 27 | 28 | let errorLog = exception; 29 | let code = HttpStatus.INTERNAL_SERVER_ERROR; 30 | let error = 'Internal Server Error'; 31 | let msg; 32 | 33 | requestLogger(this.loggerService, request, () => { 34 | if (exception instanceof HttpException) { 35 | const res = exception.getResponse(); 36 | if (typeof res !== 'string') { 37 | const { statusCode = exception.getStatus(), message, error: err = message } = res as any; 38 | code = statusCode; 39 | error = err; 40 | msg = Array.isArray(message) ? message[0] : message; 41 | } 42 | } else { 43 | this.loggerService.error(errorLog, '服务运行错误'); 44 | } 45 | 46 | // 尽可能转为中文 47 | const message = (chinese.test(msg) && msg) || HttpStatusText[error] || error; 48 | 49 | const resJson = { code, error, message }; 50 | 51 | // 错误日志 52 | this.loggerService.error(resJson, '响应错误'); 53 | 54 | response.status(code).json(resJson); 55 | }); 56 | } 57 | } 58 | 59 | // 判断是否中文 60 | const chinese = /.*[\u4e00-\u9fa5]+.*/; 61 | 62 | // 错误状态码中文 63 | const HttpStatusText = { 64 | 'Bad Request': '请求参数错误', 65 | Unauthorized: '未经授权', 66 | 'Not Found': '未找到地址', 67 | 'Internal Server Error': '服务器错误', 68 | Forbidden: '权限不足', 69 | 'Request Timeout': '请求超时异常', 70 | }; 71 | 72 | /* 73 | 74 | Bad Request Exception 错误的请求异常 75 | Unauthorized Exception 未经授权的例外 76 | Not Found Exception 找不到异常 77 | Forbidden Exception 禁止例外 78 | Not Acceptable Exception 不可接受的例外 79 | Request Timeout Exception 请求超时异常 80 | Conflict Exception 冲突例外 81 | Gone Exception 异常消失 82 | Pay load Too Large Exception 有效负载过大 83 | Unsupported Media Type Exception 不支持的媒体类型异常 84 | Unprocessab le Exception 无法处理的异常 85 | Internal Server Error Exception 内部服务器错误异常 86 | Not Imp lemented Exception 未实施异常 87 | Bad Gateway Exception 错误的网关异常 88 | Service Unavailab le Exception 服务不可用异常 89 | Gateway Timeout Exception 网关超时异常 90 | 91 | */ 92 | -------------------------------------------------------------------------------- /src/common/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './all.exception.filter'; // 异常过滤器 2 | export * from './transform.interceptor'; // 响应参数转化为统一格式 3 | -------------------------------------------------------------------------------- /src/common/providers/transform.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject, LoggerService } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { map, tap } from 'rxjs/operators'; 4 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 5 | import { requestLogger } from '../tools'; 6 | 7 | export interface Response { 8 | code: number; 9 | data: T; 10 | } 11 | 12 | /** 13 | * 响应参数转化为统一格式 14 | */ 15 | @Injectable() 16 | export class TransformInterceptor implements NestInterceptor> { 17 | constructor(@Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly loggerService: LoggerService) {} 18 | 19 | intercept(context: ExecutionContext, next: CallHandler): Observable> { 20 | const ctx = context.switchToHttp(); 21 | const req = ctx.getRequest(); 22 | const res = ctx.getResponse(); 23 | const resNext = next.handle(); 24 | 25 | return resNext.pipe( 26 | map((data) => ({ code: res.statusCode, data })), // 响应参数转化为统一格式 27 | tap((res) => { 28 | requestLogger(this.loggerService, req, () => { 29 | this.loggerService.log('\n' + JSON.stringify(res, null, 2), '响应结果'); 30 | }); 31 | }), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/common/service/account.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, UnauthorizedException, Req } from '@nestjs/common'; 2 | import { FindManyOptions } from 'typeorm'; 3 | import { TransformInstanceToPlain } from 'class-transformer'; 4 | import { sha512 } from 'js-sha512'; 5 | import { IdsDto, AccountLoginDto } from '../dto'; 6 | import { insLike, insNull, toIp, format } from '../tools'; 7 | import { CommonService, TClass } from './common'; 8 | 9 | /** 10 | * crud 帐号服务 11 | */ 12 | export const AccountService = < 13 | Entity, // 实体 14 | CreateDto, // 创建 15 | UpdateDto, // 更新 16 | QueryDto, // 查询条件 17 | PaginationQueryDto, // 分页查询条件 18 | >( 19 | _Entity: TClass, 20 | _CreateDto: TClass, 21 | _UpdateDto: TClass, 22 | _QueryDto: TClass, 23 | _PaginationQueryDto: TClass, 24 | ) => { 25 | class AccountService extends CommonService(_Entity, _CreateDto, _UpdateDto, _QueryDto, _PaginationQueryDto) { 26 | /** 27 | * 给账号固定参数加上模糊查询 28 | */ 29 | getList(data: QueryDto, options?: FindManyOptions) { 30 | insLike(data, ['username', 'phone', 'nickname']); 31 | return super.getList(data, options); 32 | } 33 | 34 | /** 35 | * 给账号固定参数加上模糊查询 36 | */ 37 | getListAndCount(data: PaginationQueryDto, options?: FindManyOptions) { 38 | insLike(data, ['username', 'phone', 'nickname']); 39 | return super.getListAndCount(data, options); 40 | } 41 | 42 | /** 43 | * 验证用户名是否存在 并创建用户 44 | */ 45 | async create(data: CreateDto) { 46 | const { username } = data as any; 47 | const one = await this.repository.findOne({ where: { username } as any }); 48 | if (one) throw new BadRequestException('用户名已存在'); 49 | Object.assign(data, { reg_ip: this.req?.clientIp }); // 注入创建ip 50 | await super.create(data); 51 | } 52 | 53 | /** 54 | * 给账号固定参数插入null,避免参数为空时修改失败 55 | */ 56 | update(id: string, data: UpdateDto) { 57 | insNull(data, ['phone', 'avatar']); 58 | return super.update(id, data); 59 | } 60 | 61 | /** 62 | * 登录 63 | */ 64 | @TransformInstanceToPlain() 65 | async login( 66 | { username, password }: AccountLoginDto, 67 | validatorAccount?: (_Entity: Entity) => void, 68 | ): Promise { 69 | const one: any = await this.repository.findOne({ where: { username } as any }); 70 | 71 | // 账号不存在或密码错误的情况下,提示登录失败 72 | if (!one || one.password !== sha512(password)) { 73 | throw new UnauthorizedException('登录失败'); 74 | } 75 | 76 | // 验证账号 77 | validatorAccount?.(one); 78 | 79 | // 注入登录IP和登录时间 80 | const insLoginInfo: any = { login_ip: toIp(this.req.clientIp), login_date: format(new Date()) }; 81 | this.repository.update(one.id, insLoginInfo); 82 | 83 | Object.assign(one, insLoginInfo); 84 | 85 | return one; 86 | } 87 | } 88 | 89 | return class extends AccountService {}; 90 | }; 91 | -------------------------------------------------------------------------------- /src/common/service/common.ts: -------------------------------------------------------------------------------- 1 | import { REQUEST } from '@nestjs/core'; 2 | import { BadRequestException, Inject } from '@nestjs/common'; 3 | import { TransformInstanceToPlain } from 'class-transformer'; 4 | import { Repository, FindManyOptions } from 'typeorm'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { ConfigService } from '@nestjs/config'; 7 | import { toWhere } from '../tools'; 8 | import { IdsDto } from '../dto'; 9 | 10 | export type TClass = new (...args: any[]) => T; 11 | 12 | /** 13 | * crud 公共服务 14 | */ 15 | export const CommonService = < 16 | Entity, // 实体 17 | CreateDto, // 创建 18 | UpdateDto, // 更新 19 | QueryDto, // 查询条件 20 | PaginationQueryDto, // 分页查询条件 21 | >( 22 | _Entity: TClass, 23 | _CreateDto: TClass, 24 | _UpdateDto: TClass, 25 | _QueryDto: TClass, 26 | _PaginationQueryDto: TClass, 27 | ) => { 28 | class CommonService { 29 | constructor( 30 | @InjectRepository(_Entity) readonly repository: Repository, 31 | @Inject(REQUEST) readonly req: any, 32 | readonly configService: ConfigService, 33 | ) {} 34 | 35 | /** 36 | * 查询所有数据 37 | */ 38 | @TransformInstanceToPlain() 39 | async getList(data: QueryDto, options?: FindManyOptions): Promise { 40 | const list = await this.repository.find({ 41 | where: toWhere(data), 42 | order: { create_date: 'DESC' } as any, 43 | ...options, 44 | }); 45 | return list; 46 | } 47 | 48 | /** 49 | * 查询数据数据与总数 50 | */ 51 | @TransformInstanceToPlain() 52 | async getListAndCount( 53 | _data: PaginationQueryDto, 54 | options?: FindManyOptions, 55 | ): Promise<{ list: Entity[]; total: number }> { 56 | const { current, pageSize, ...data } = _data as any; 57 | const [list, total] = await this.repository.findAndCount({ 58 | where: toWhere(data), 59 | order: { create_date: 'DESC' } as any, 60 | skip: (current - 1) * pageSize, 61 | take: pageSize, 62 | ...options, 63 | }); 64 | return { list, total }; 65 | } 66 | 67 | /** 68 | * 查询一条数据 69 | */ 70 | @TransformInstanceToPlain() 71 | async get(id: string) { 72 | const one = await this.repository.findOneBy({ id } as any); 73 | if (!one) throw new BadRequestException('该数据不存在'); 74 | return one; 75 | } 76 | 77 | /** 78 | * 创建数据 79 | */ 80 | async create(data: CreateDto) { 81 | await this.repository.save(data as any); 82 | } 83 | 84 | /** 85 | * 更新数据 86 | */ 87 | async update(id: string, data: UpdateDto) { 88 | await this.repository.update(id, data as any); 89 | } 90 | 91 | /** 92 | * 删除数据 93 | */ 94 | async delete(ids: IdsDto['ids']) { 95 | if (!ids.length) throw new BadRequestException('ids 不可为空'); 96 | await this.repository.delete(ids); 97 | } 98 | } 99 | 100 | return class extends CommonService {}; 101 | }; 102 | -------------------------------------------------------------------------------- /src/common/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './account'; 3 | -------------------------------------------------------------------------------- /src/common/tools/controller.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, Get, Post, Put, Delete, Controller, UseGuards } from '@nestjs/common'; 2 | import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiQuery, ApiBearerAuth } from '@nestjs/swagger'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | 5 | import { Roles, RolesGuard } from '../controller/roles.guard'; 6 | 7 | const methods = { Get, Post, Put, Delete }; 8 | 9 | export type MethodOptions = { 10 | res?: Function | [Function]; // 接口响应数据类型 11 | body?: Function | [Function]; // 接口请求体内容类型 12 | query?: Function | [Function]; // 接口请求参数类型 13 | auth?: boolean; // 开启授权守卫 14 | roles?: string[]; // 角色权限 15 | }; 16 | 17 | /** 18 | * 请求方法注册 19 | */ 20 | export const Method = ( 21 | summary: string, // 方法描述 22 | methodAndPath: string | [string, string], // 请求方法类型与路径 23 | options?: MethodOptions, 24 | ) => { 25 | const decorators: Array = []; 26 | 27 | // 定义接口文档标题 28 | decorators.push(ApiOperation({ summary })); 29 | 30 | // 定义请求方法与路径 31 | if (typeof methodAndPath === 'string') methodAndPath = [methodAndPath, undefined]; 32 | const [method, path] = methodAndPath; 33 | decorators.push(methods[method](path)); 34 | 35 | const { res, body, query, auth, roles } = options || {}; 36 | 37 | res && decorators.push(ApiResponse({ type: res })); 38 | body && decorators.push(ApiBody({ type: body })); 39 | query && decorators.push(ApiQuery({ type: query })); 40 | 41 | auth && decorators.push(ApiAuth()); 42 | roles && decorators.push(Roles(roles)); 43 | 44 | return applyDecorators(...decorators); 45 | }; 46 | 47 | /** 48 | * api 接口路径定义 49 | */ 50 | export const ApiPath = ( 51 | prefix: string | string[], // 控制器接口路由 52 | tag: string, // 控制器描述 53 | ) => { 54 | return applyDecorators( 55 | ApiTags(tag), // 接口描述 56 | Controller(prefix), // 定义路由 57 | ); 58 | }; 59 | 60 | /** 61 | * api 授权拦截 62 | */ 63 | export const ApiAuth = () => { 64 | return applyDecorators(ApiBearerAuth(), UseGuards(AuthGuard('jwt'), RolesGuard)); 65 | }; 66 | 67 | /** 68 | * api 接口路径定义 and 授权拦截 69 | */ 70 | export const ApiPathAuth: typeof ApiPath = (...args) => { 71 | return applyDecorators( 72 | ApiPath(...args), // 路由定义 73 | ApiAuth(), // 授权 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /src/common/tools/data.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | /** 6 | * 跟目录 7 | */ 8 | export const rootPath = (() => { 9 | const defaultPath = path.join(__dirname, '../../../'); 10 | if (fs.existsSync(path.join(defaultPath, 'config'))) return defaultPath; 11 | 12 | const nccPath = path.join(__dirname, '../'); // 单文件打包根目录 13 | if (fs.existsSync(path.join(nccPath, 'config'))) return nccPath; 14 | 15 | return ''; 16 | })(); 17 | 18 | // ---------------------- URL 相关数据转化 ---------------------- // 19 | 20 | /** 21 | * 客户端 ip 字符串 转化纯 ip 22 | */ 23 | export const toIp = (ip: string) => ip?.replace?.('::ffff:', '').replace('::1', '127.0.0.1'); 24 | 25 | // ---------------------- URL 相关数据转化 ---------------------- // 26 | 27 | // ---------------------- 时间相关数据转化 ---------------------- // 28 | 29 | /** 30 | * 时间格式化精度 31 | */ 32 | export enum Precision { 33 | Second = 'YYYY-MM-DD HH:mm:ss', // 秒 34 | Minute = 'YYYY-MM-DD HH:mm', // 分钟 35 | Hour = 'YYYY-MM-DD HH', // 小时 36 | Day = 'YYYY-MM-DD', // 天 37 | Month = 'YYYY-MM', // 月 38 | Year = 'YYYY', // 年 39 | } 40 | 41 | /** 42 | * 时间格式化 43 | */ 44 | export const format = (value?: any, template = Precision.Second) => value && dayjs(value).format(template); 45 | 46 | /** 47 | * 毫秒转化为时间字符串 48 | */ 49 | export const toTimeString = (time: number) => { 50 | let text = ''; 51 | const second = Math.round(time / 1000); 52 | if (second < 60) text += `${second}秒`; 53 | else { 54 | const s = second % 60; 55 | const minute = (second - s) / 60; 56 | if (minute < 60) text += `${minute}分钟${s}秒`; 57 | else { 58 | const m = minute % 60; 59 | const hour = (minute - m) / 60; 60 | if (hour < 24) text += `${hour}小时${m}分钟${s}秒`; 61 | else { 62 | const h = hour % 24; 63 | const day = (hour - h) / 24; 64 | text += `${day}天${h}小时${m}分钟${s}秒`; 65 | } 66 | } 67 | } 68 | return text; 69 | }; 70 | 71 | // ---------------------- 时间相关数据转化 ---------------------- // 72 | 73 | // ---------------------- 内容数据转化 ---------------------- // 74 | 75 | /** 76 | * 获取对象真实 key 数组 77 | */ 78 | export const getKeys = (object: object) => { 79 | // 获取 key 列表 80 | let keys: (string | number)[] = Object.keys(object); 81 | // 数组对象的 key 转化为数字 82 | if (Array.isArray(object)) keys = keys.map((i) => +i); 83 | return keys; 84 | }; 85 | 86 | /** 87 | * 获取枚举备注文本 88 | */ 89 | export const getEnumRemark = (object: object) => { 90 | return Object.keys(object) 91 | .map((i) => `${i}:${object[i]}`) 92 | .join('、'); 93 | }; 94 | 95 | // ---------------------- 内容数据转化 ---------------------- // 96 | -------------------------------------------------------------------------------- /src/common/tools/dto.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators } from '@nestjs/common'; 2 | import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; 3 | import { Transform, Type } from 'class-transformer'; 4 | import { IsIn, Matches, IsInt, IsNotEmpty, IsDate, IsOptional } from 'class-validator'; 5 | import { getKeys, getEnumRemark } from './data'; 6 | 7 | export type DtoParamOptions = ApiPropertyOptions & { 8 | isInt?: boolean; // 自动转换并限制为数字类型 9 | isDate?: boolean; // 自动转换并限制为时间类型 10 | matches?: [RegExp, string]; // 【正则匹配,提示文案】 11 | }; 12 | 13 | /** 14 | * 数据传输对象参数装饰器 15 | */ 16 | export const DtoParam = (name: string, _options?: DtoParamOptions) => { 17 | const decorators: Array = []; 18 | 19 | let { isInt, isDate, matches, required = true, enum: _enum, ...options } = _options || {}; 20 | 21 | Object.assign(options, { required }); 22 | if (required) { 23 | // 必填校验 24 | decorators.push(IsNotEmpty({ message: `请输入${name}` })); 25 | } else { 26 | // 非必填的参数,空值的时候跳过验证 27 | decorators.push( 28 | Transform(({ value }) => (value === '' ? undefined : value)), // 空字符串转为 undefined 29 | IsOptional(), // null 和 undefined 的时候跳过所有验证 30 | ); 31 | } 32 | 33 | // 数字校验 34 | if (isInt) { 35 | decorators.push( 36 | Type(() => Number), // 转为数字 37 | IsInt({ message: `${name}只能为数字` }), // 校验只能为数字 38 | ); 39 | } 40 | 41 | // 时间类型校验 42 | if (isDate) { 43 | decorators.push( 44 | Type(() => Date), // 转为时间对象 45 | IsDate({ message: `${name}只能为时间类型` }), // 校验只能为时间类型 46 | ); 47 | } 48 | 49 | // 正则验证 50 | if (matches) { 51 | const [pattern, message] = matches; 52 | decorators.push(Matches(pattern, { message })); // 正则匹配 53 | } 54 | 55 | // 枚举类型限制 56 | if (_enum) { 57 | decorators.push(IsIn(getKeys(_enum), { message: `请选择正确的${name},${getEnumRemark(_enum)}` })); // 校验只能在范围内的值 58 | Object.assign(options, { enum: getKeys(_enum) }); // 追加文档枚举设置 59 | name += `,${getEnumRemark(_enum)}`; // 追加文档描述 60 | } 61 | 62 | decorators.push(ApiProperty({ description: name, ...options })); // 文档 63 | 64 | return applyDecorators(...decorators); 65 | }; 66 | -------------------------------------------------------------------------------- /src/common/tools/entity.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators } from '@nestjs/common'; 2 | import { Column, ColumnOptions, ValueTransformer } from 'typeorm'; 3 | import { Exclude } from 'class-transformer'; 4 | import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; 5 | import { getKeys, getEnumRemark, format, toIp } from './data'; 6 | import { sha512 } from 'js-sha512'; 7 | 8 | export type EntityColumnOptions = ColumnOptions & { 9 | apiPropertyOptions?: ApiPropertyOptions; // 文档配置 10 | exclude?: boolean; // 排除在文档中 11 | }; 12 | 13 | /** 14 | * 实体列装饰器 15 | */ 16 | export const EntityColumn = (name: string, length?: number | EntityColumnOptions, _options?: EntityColumnOptions) => { 17 | const decorators: Array = []; 18 | 19 | // 合成配置项 20 | if (typeof length === 'number') length = { length }; 21 | const { apiPropertyOptions = {}, exclude, enum: _enum, ...options } = { ...length, ..._options }; 22 | 23 | // 枚举类型补充备注 24 | if (_enum) { 25 | name += `,${getEnumRemark(_enum)}`; 26 | apiPropertyOptions.enum = getKeys(_enum); 27 | } 28 | 29 | // 数组类型 30 | if (options.type === 'simple-array') { 31 | if (!apiPropertyOptions.type) apiPropertyOptions.type = [String]; 32 | } 33 | 34 | // 合成装饰器 35 | decorators.push(Column({ comment: name, ...options })); // 数据库 36 | if (exclude) 37 | decorators.push(Exclude()); // 排除字段 38 | else decorators.push(ApiProperty({ description: name, ...apiPropertyOptions })); // 文档 39 | 40 | return applyDecorators(...decorators); 41 | }; 42 | 43 | // ---------------------- 数据转化器生成 ---------------------- // 44 | 45 | // 默认转化方法 46 | const toOrFrom = (value: any) => value; 47 | 48 | /** 49 | * 生成转化对象 50 | */ 51 | export const createTransformer = ({ to = toOrFrom, from = toOrFrom }: Partial) => ({ to, from }); 52 | 53 | // ---------------------- 数据转化器生成 ---------------------- // 54 | 55 | /** 56 | * 时间转化 57 | */ 58 | export const dateTransformer = createTransformer({ from: format }); 59 | 60 | /** 61 | * sha512 密码转化 62 | */ 63 | export const sha512Transformer = createTransformer({ to: sha512 }); 64 | 65 | /** 66 | * 客户端 ip 字符串 转化纯 ip 67 | */ 68 | export const toIpTransformer = createTransformer({ to: toIp }); 69 | -------------------------------------------------------------------------------- /src/common/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data'; 2 | export * from './logger'; 3 | export * from './entity'; 4 | export * from './dto'; 5 | export * from './service'; 6 | export * from './controller'; 7 | -------------------------------------------------------------------------------- /src/common/tools/logger.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from '@nestjs/common'; 2 | import { toIp } from './data'; 3 | 4 | let num = 0; 5 | const line = '▬'.repeat(50); 6 | 7 | /** 8 | * 请求日志 9 | */ 10 | export const requestLogger = (logget: LoggerService, request: any, content: Function) => { 11 | num += 1; 12 | logget.log(`\nstart ${line} ${num} ${line} start`, '请求开始'); 13 | const { url, clientIp, method, body, user } = request; 14 | user?.id && logget.log(`${user.id}`, '操作用户'); 15 | logget.log(`${toIp(clientIp)} ${method} ${url}`, '请求路径'); 16 | Object.keys(body).length && logget.log('\n' + JSON.stringify(body, null, 2), '请求参数'); 17 | content(); 18 | logget.log(`\n-end- ${line} ${num} ${line} -end-`, '请求结束'); 19 | }; 20 | -------------------------------------------------------------------------------- /src/common/tools/service.ts: -------------------------------------------------------------------------------- 1 | import { Like, In, SelectQueryBuilder } from 'typeorm'; 2 | import { pickBy, mapValues } from 'lodash'; 3 | 4 | // ---------------------- 请求入参操作 ---------------------- // 5 | 6 | /** 7 | * 插入模糊查询 8 | */ 9 | export const insLike = (data: any, keys: string[]) => { 10 | keys.forEach((i) => { 11 | if (data[i]) data[i] = Like(`%${data[i]}%`); 12 | }); 13 | }; 14 | 15 | /** 16 | * 当未提交对应字段时,插入空值 17 | */ 18 | export const insNull = (data: any, keys: string[]) => { 19 | keys.forEach((i) => { 20 | if (data[i] === undefined) data[i] = null; 21 | }); 22 | }; 23 | 24 | /** 25 | * 转为筛选条件对象 26 | */ 27 | export const toWhere = (where: any) => { 28 | // 转化 29 | where = mapValues(where, (v) => { 30 | // 如果查询值为数组,则使用 in 的 31 | if (Array.isArray(v)) { 32 | v = v.length ? In(v) : undefined; 33 | } 34 | return v; 35 | }); 36 | 37 | // 过滤空值 38 | where = pickBy(where, (v) => ![undefined, null, ''].includes(v)); 39 | 40 | return where; 41 | }; 42 | 43 | // ---------------------- 请求入参操作 ---------------------- // 44 | -------------------------------------------------------------------------------- /src/infos/article/article.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiPathAuth, CommonController } from '../../common'; 2 | import { ArticleService } from './article.service'; 3 | import { Article } from './article.entity'; 4 | import { 5 | ArticleCreateDto, 6 | ArticleUpdateDto, 7 | ArticleQueryDto, 8 | ArticlePaginationQueryDto, 9 | ArticlePaginationDto, 10 | } from './article.dto'; 11 | 12 | @ApiPathAuth('article', '文章管理') 13 | export class ArticleController extends CommonController( 14 | Article, 15 | ArticleCreateDto, 16 | ArticleUpdateDto, 17 | ArticleQueryDto, 18 | ArticlePaginationQueryDto, 19 | ArticlePaginationDto, 20 | ArticleService, 21 | ) {} 22 | -------------------------------------------------------------------------------- /src/infos/article/article.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PaginationQueryDto, 3 | PaginationDto, 4 | InfoArticleQueryDto, 5 | InfoArticleCreateDto, 6 | InfoArticleUpdateDto, 7 | } from '../../common'; 8 | import { Article } from './article.entity'; 9 | 10 | /** 11 | * 查询条件 12 | */ 13 | export class ArticleQueryDto extends InfoArticleQueryDto {} 14 | 15 | /** 16 | * 查询分页数据条件 17 | */ 18 | export class ArticlePaginationQueryDto extends PaginationQueryDto(ArticleQueryDto) {} 19 | 20 | /** 21 | * 分页数据 22 | */ 23 | export class ArticlePaginationDto extends PaginationDto(Article) {} 24 | 25 | /** 26 | * 创建数据对象 27 | */ 28 | export class ArticleCreateDto extends InfoArticleCreateDto {} 29 | 30 | /** 31 | * 编辑数据对象 32 | */ 33 | export class ArticleUpdateDto extends InfoArticleUpdateDto {} 34 | -------------------------------------------------------------------------------- /src/infos/article/article.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'typeorm'; 2 | import { InfoArticleEntity } from '../../common'; 3 | 4 | /** 5 | * 文章 6 | */ 7 | @Entity() 8 | export class Article extends InfoArticleEntity {} 9 | -------------------------------------------------------------------------------- /src/infos/article/article.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Article } from './article.entity'; 4 | import { ArticleController } from './article.controller'; 5 | import { ArticleService } from './article.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Article])], 9 | controllers: [ArticleController], 10 | providers: [ArticleService], 11 | exports: [ArticleService], 12 | }) 13 | export class ArticleModule {} 14 | -------------------------------------------------------------------------------- /src/infos/article/article.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CommonService } from '../../common'; 3 | import { Article } from './article.entity'; 4 | import { ArticleCreateDto, ArticleUpdateDto, ArticleQueryDto, ArticlePaginationQueryDto } from './article.dto'; 5 | 6 | @Injectable() 7 | export class ArticleService extends CommonService( 8 | Article, 9 | ArticleCreateDto, 10 | ArticleUpdateDto, 11 | ArticleQueryDto, 12 | ArticlePaginationQueryDto, 13 | ) {} 14 | -------------------------------------------------------------------------------- /src/infos/category/category.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiPathAuth, CommonController } from '../../common'; 2 | import { CategoryService } from './category.service'; 3 | import { Category } from './category.entity'; 4 | import { 5 | CategoryCreateDto, 6 | CategoryUpdateDto, 7 | CategoryQueryDto, 8 | CategoryPaginationQueryDto, 9 | CategoryPaginationDto, 10 | } from './category.dto'; 11 | 12 | @ApiPathAuth('category', '分类管理') 13 | export class CategoryController extends CommonController( 14 | Category, 15 | CategoryCreateDto, 16 | CategoryUpdateDto, 17 | CategoryQueryDto, 18 | CategoryPaginationQueryDto, 19 | CategoryPaginationDto, 20 | CategoryService, 21 | ) {} 22 | -------------------------------------------------------------------------------- /src/infos/category/category.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PaginationQueryDto, 3 | PaginationDto, 4 | InfoCategoryQueryDto, 5 | InfoCategoryCreateDto, 6 | InfoCategoryUpdateDto, 7 | } from '../../common'; 8 | import { Category } from './category.entity'; 9 | 10 | /** 11 | * 查询条件 12 | */ 13 | export class CategoryQueryDto extends InfoCategoryQueryDto {} 14 | 15 | /** 16 | * 查询分页数据条件 17 | */ 18 | export class CategoryPaginationQueryDto extends PaginationQueryDto(CategoryQueryDto) {} 19 | 20 | /** 21 | * 分页数据 22 | */ 23 | export class CategoryPaginationDto extends PaginationDto(Category) {} 24 | 25 | /** 26 | * 创建数据对象 27 | */ 28 | export class CategoryCreateDto extends InfoCategoryCreateDto {} 29 | 30 | /** 31 | * 编辑数据对象 32 | */ 33 | export class CategoryUpdateDto extends InfoCategoryUpdateDto {} 34 | -------------------------------------------------------------------------------- /src/infos/category/category.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'typeorm'; 2 | import { InfoCategoryEntity } from '../../common'; 3 | 4 | /** 5 | * 分类 6 | */ 7 | @Entity() 8 | export class Category extends InfoCategoryEntity {} 9 | -------------------------------------------------------------------------------- /src/infos/category/category.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Category } from './category.entity'; 4 | import { CategoryController } from './category.controller'; 5 | import { CategoryService } from './category.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Category])], 9 | controllers: [CategoryController], 10 | providers: [CategoryService], 11 | exports: [CategoryService], 12 | }) 13 | export class CategoryModule {} 14 | -------------------------------------------------------------------------------- /src/infos/category/category.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CommonService } from '../../common'; 3 | import { Category } from './category.entity'; 4 | import { CategoryCreateDto, CategoryUpdateDto, CategoryQueryDto, CategoryPaginationQueryDto } from './category.dto'; 5 | 6 | @Injectable() 7 | export class CategoryService extends CommonService( 8 | Category, 9 | CategoryCreateDto, 10 | CategoryUpdateDto, 11 | CategoryQueryDto, 12 | CategoryPaginationQueryDto, 13 | ) {} 14 | -------------------------------------------------------------------------------- /src/infos/infos.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RouterModule } from '@nestjs/core'; 3 | import { CategoryModule } from './category/category.module'; 4 | import { ArticleModule } from './article/article.module'; 5 | 6 | const imports = [CategoryModule, ArticleModule]; 7 | 8 | @Module({ 9 | imports: [ 10 | ...imports, 11 | 12 | // 路由前缀定义 13 | RouterModule.register([{ path: 'infos', children: imports }]), 14 | ], 15 | }) 16 | export class InfosModule {} 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { LoggerService } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; 5 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 6 | import { AppModule } from './app.module'; 7 | import { mw } from 'request-ip'; 8 | import { initialize } from './common/initialize'; 9 | 10 | async function bootstrap() { 11 | const app = await NestFactory.create(AppModule, { 12 | bufferLogs: true, 13 | }); 14 | 15 | // 获取客户端真实IP 16 | app.use(mw()); 17 | 18 | // 获取配置服务 19 | const configService = await app.resolve(ConfigService); 20 | 21 | // 插入日志 22 | const loggerService = await app.resolve(WINSTON_MODULE_NEST_PROVIDER); 23 | app.useLogger(loggerService); 24 | 25 | const serve = configService.get('serve'); // 服务配置 26 | const swagger = configService.get('swagger'); // swagger 配置 27 | 28 | // 接口文档 29 | const documentBuilder = new DocumentBuilder() 30 | .setTitle(swagger.title) 31 | .setDescription(swagger.description) 32 | .addBearerAuth() 33 | .addServer(serve.prefix) 34 | .build(); 35 | const document = SwaggerModule.createDocument(app, documentBuilder, { ignoreGlobalPrefix: true }); 36 | SwaggerModule.setup(swagger.path, app, document); 37 | 38 | app.setGlobalPrefix(serve.prefix); // 接口请求前缀 39 | await app.listen(process.env.PORT ?? serve.port); // 启动服务 40 | 41 | // 捕获进程错误 42 | process.on('uncaughtException', function (err) { 43 | loggerService.error(err, '进程异常'); 44 | }); 45 | 46 | // 输出链接 47 | loggerService.log(`http://localhost:${serve.port}/${swagger.path}`, '接口文档'); 48 | loggerService.log(`http://localhost:${serve.port}/${serve.prefix}`, '接口地址'); 49 | 50 | initialize(app); 51 | } 52 | 53 | bootstrap(); 54 | -------------------------------------------------------------------------------- /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()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /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": "ESNext", 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 | "esModuleInterop": true, 21 | "importHelpers": true 22 | } 23 | } 24 | --------------------------------------------------------------------------------