├── .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 |
--------------------------------------------------------------------------------