├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── README.md ├── generator.sh ├── nest-cli.json ├── ormconfig.json ├── package.json ├── pnpm-lock.yaml ├── prune.sql ├── src ├── app.module.ts ├── config │ ├── database.config.ts │ └── index.ts ├── main.ts ├── modules │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.decorator.ts │ │ ├── auth.service.ts │ │ ├── authority.guard.ts │ │ ├── constants.ts │ │ ├── jwt.strategy.ts │ │ └── login.guard.ts │ ├── core │ │ ├── constants.ts │ │ ├── decorators │ │ │ ├── dto-validation.decorator.ts │ │ │ └── index.ts │ │ ├── helpers │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ └── providers │ │ │ ├── app.filter.ts │ │ │ ├── app.interceptor.ts │ │ │ ├── app.pipe.ts │ │ │ └── index.ts │ ├── database │ │ ├── base │ │ │ ├── entity.ts │ │ │ ├── index.ts │ │ │ ├── repository.ts │ │ │ ├── service.ts │ │ │ ├── subscriber.ts │ │ │ └── tree.repository.ts │ │ ├── constants.ts │ │ ├── constraints │ │ │ ├── data.exist.constraint.ts │ │ │ ├── index.ts │ │ │ ├── tree.unique.constraint.ts │ │ │ ├── tree.unique.exist.constraint.ts │ │ │ ├── unique.constraint.ts │ │ │ └── unique.exist.constraint.ts │ │ ├── database.module.ts │ │ ├── decorators │ │ │ ├── index.ts │ │ │ └── repository.decorator.ts │ │ ├── helpers.ts │ │ └── types.ts │ ├── org │ │ ├── constants.ts │ │ ├── controllers │ │ │ ├── index.ts │ │ │ ├── org.controller.ts │ │ │ ├── role-authority.controller.ts │ │ │ ├── role.controller.ts │ │ │ ├── station.controller.ts │ │ │ ├── user-role.controller.ts │ │ │ └── user.controller.ts │ │ ├── dtos │ │ │ ├── index.ts │ │ │ ├── org.dto.ts │ │ │ ├── role-authority.dto.ts │ │ │ ├── role.dto.ts │ │ │ ├── station.dto.ts │ │ │ ├── user-role.dto.ts │ │ │ └── user.dto.ts │ │ ├── entities │ │ │ ├── index.ts │ │ │ ├── org.entity.ts │ │ │ ├── role-authority.entity.ts │ │ │ ├── role.entity.ts │ │ │ ├── station.entity.ts │ │ │ ├── user-role-relation.entity.ts │ │ │ ├── user-role.entity.ts │ │ │ └── user.entity.ts │ │ ├── org.module.ts │ │ ├── repositories │ │ │ ├── index.ts │ │ │ ├── org.repository.ts │ │ │ ├── role-authority.repository.ts │ │ │ ├── role.repository.ts │ │ │ ├── station.repository.ts │ │ │ ├── user-role.repository.ts │ │ │ └── user.repository.ts │ │ └── services │ │ │ ├── index.ts │ │ │ ├── org.service.ts │ │ │ ├── role-authority.service.ts │ │ │ ├── role.service.ts │ │ │ ├── station.service.ts │ │ │ ├── user-role.service.ts │ │ │ └── user.service.ts │ ├── resource │ │ ├── controllers │ │ │ ├── index.ts │ │ │ └── oss.controller.ts │ │ ├── dtos │ │ │ ├── index.ts │ │ │ └── oss.dto.ts │ │ ├── entities │ │ │ ├── index.ts │ │ │ └── oss.entity.ts │ │ ├── repositories │ │ │ ├── index.ts │ │ │ └── oss.repository.ts │ │ ├── resource.module.ts │ │ └── services │ │ │ ├── index.ts │ │ │ └── oss.service.ts │ ├── restful │ │ ├── base │ │ │ ├── code.constants.ts │ │ │ ├── controller.ts │ │ │ ├── index.ts │ │ │ └── result.type.ts │ │ ├── constants.ts │ │ ├── decorators │ │ │ ├── crud.decorator.ts │ │ │ └── index.ts │ │ ├── dtos │ │ │ ├── delete-with-trash.dto.ts │ │ │ ├── delete.dto.ts │ │ │ ├── index.ts │ │ │ ├── query.dto.ts │ │ │ └── restore.dto.ts │ │ ├── oss │ │ │ ├── oss-middleware.controller.ts │ │ │ ├── oss-middleware.module.ts │ │ │ ├── oss-middleware.service.ts │ │ │ └── oss-middleware.type.ts │ │ ├── types.ts │ │ └── upload │ │ │ ├── fastify-file-interceptor.ts │ │ │ ├── file-mappter.ts │ │ │ └── file-upload-util.ts │ └── system │ │ ├── constants.ts │ │ ├── controllers │ │ ├── area.controller.ts │ │ ├── dictionary.controller.ts │ │ ├── index.ts │ │ ├── menu.controller.ts │ │ ├── parameter.controller.ts │ │ └── resource.controller.ts │ │ ├── dtos │ │ ├── area.dto.ts │ │ ├── dictionary.dto.ts │ │ ├── index.ts │ │ ├── menu.dto.ts │ │ ├── parameter.dto.ts │ │ └── resource.dto.ts │ │ ├── entities │ │ ├── area.entity.ts │ │ ├── dictionary.entity.ts │ │ ├── index.ts │ │ ├── menu.entity.ts │ │ ├── parameter.entity.ts │ │ └── resource.entity.ts │ │ ├── helpers.ts │ │ ├── repositories │ │ ├── area.repository.ts │ │ ├── dictionary.repository.ts │ │ ├── index.ts │ │ ├── menu.repository.ts │ │ ├── parameter.repository.ts │ │ └── resource.repository.ts │ │ ├── services │ │ ├── area.service.ts │ │ ├── dictionary.service.ts │ │ ├── index.ts │ │ ├── menu.service.ts │ │ ├── parameter.service.ts │ │ └── resource.service.ts │ │ └── system.module.ts └── typings │ └── global.d.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PROTOCOL=http:// 2 | HOST=127.0.0.1 3 | PORT=7441 4 | JWT_SECRET= 5 | #数据库配置 6 | MYSQL_HOST=127.0.0.1 7 | MYSQL_PORT=3306 8 | MYSQL_USERNAME=root 9 | MYSQL_PASSWORD=12345678 10 | MYSQL_DATABASE=prune 11 | # 阿里云密钥 12 | ALI_ACCESS_KEY= 13 | ALI_ACCESS_KEY_SECRET= 14 | # 七牛云密钥 15 | QINIU_ACCESS_KEY= 16 | QINIU_SECRET_KEY= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | back 3 | node_modules 4 | pnpm-lock.yaml 5 | docker 6 | Dockerfile* 7 | LICENSE 8 | yarn-error.log 9 | .history 10 | .vscode 11 | .docusaurus 12 | .dockerignore 13 | .DS_Store 14 | .eslintignore 15 | .editorconfig 16 | .gitignore 17 | .prettierignore 18 | .eslintcache 19 | *.lock 20 | **/*.svg 21 | **/*.md 22 | **/*.svg 23 | **/*.ejs 24 | **/*.html 25 | **/*.png 26 | **/*.toml 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | ecmaVersion: 'latest', 7 | sourceType: 'module', 8 | }, 9 | root: true, 10 | env: { 11 | node: true, 12 | jest: true, 13 | }, 14 | plugins: ['@typescript-eslint', 'jest', 'prettier', 'import', 'unused-imports'], 15 | extends: [ 16 | // airbnb规范 17 | // https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb 18 | 'airbnb-base', 19 | // 兼容typescript的airbnb规范 20 | // https://github.com/iamturns/eslint-config-airbnb-typescript 21 | 'airbnb-typescript/base', 22 | 23 | // typescript的eslint插件 24 | // https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md 25 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin 26 | 'plugin:@typescript-eslint/recommended', 27 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 28 | 29 | // 支持jest 30 | 'plugin:jest/recommended', 31 | // 使用prettier格式化代码 32 | // https://github.com/prettier/eslint-config-prettier#readme 33 | 'prettier', 34 | // 整合typescript-eslint与prettier 35 | // https://github.com/prettier/eslint-plugin-prettier 36 | 'plugin:prettier/recommended', 37 | ], 38 | rules: { 39 | /* ********************************** ES6+ ********************************** */ 40 | 'no-console': 0, 41 | 'no-var-requires': 0, 42 | 'no-restricted-syntax': 0, 43 | 'no-continue': 0, 44 | 'no-await-in-loop': 0, 45 | 'no-return-await': 0, 46 | 'no-unused-vars': 0, 47 | 'no-multi-assign': 0, 48 | 'no-param-reassign': [2, { props: false }], 49 | 'import/prefer-default-export': 0, 50 | 'import/no-cycle': 0, 51 | 'import/no-dynamic-require': 0, 52 | 'max-classes-per-file': 0, 53 | 'class-methods-use-this': 0, 54 | 'guard-for-in': 0, 55 | 'no-underscore-dangle': 0, 56 | 'no-plusplus': 0, 57 | 'no-lonely-if': 0, 58 | 'no-bitwise': ['error', { allow: ['~'] }], 59 | 60 | /* ********************************** Module Import ********************************** */ 61 | 62 | 'import/no-absolute-path': 0, 63 | 'import/extensions': 0, 64 | 'import/no-named-default': 0, 65 | 'no-restricted-exports': 0, 66 | 67 | // 一部分文件在导入devDependencies的依赖时不报错 68 | 'import/no-extraneous-dependencies': [ 69 | 1, 70 | { 71 | devDependencies: ['**/*.test.{ts,js}', '**/*.spec.{ts,js}', './test/**.{ts,js}'], 72 | }, 73 | ], 74 | // 模块导入顺序规则 75 | 'import/order': [ 76 | 1, 77 | { 78 | pathGroups: [ 79 | { 80 | pattern: '@/**', 81 | group: 'external', 82 | position: 'after', 83 | }, 84 | ], 85 | alphabetize: { order: 'asc', caseInsensitive: false }, 86 | 'newlines-between': 'always-and-inside-groups', 87 | warnOnUnassignedImports: true, 88 | }, 89 | ], 90 | // 自动删除未使用的导入 91 | // https://github.com/sweepline/eslint-plugin-unused-imports 92 | 'unused-imports/no-unused-imports': 1, 93 | 'unused-imports/no-unused-vars': [ 94 | 'error', 95 | { 96 | vars: 'all', 97 | args: 'none', 98 | ignoreRestSiblings: true, 99 | }, 100 | ], 101 | /* ********************************** Typescript ********************************** */ 102 | '@typescript-eslint/no-unused-vars': 0, 103 | '@typescript-eslint/no-empty-interface': 0, 104 | '@typescript-eslint/no-this-alias': 0, 105 | '@typescript-eslint/no-var-requires': 0, 106 | '@typescript-eslint/no-use-before-define': 0, 107 | '@typescript-eslint/explicit-member-accessibility': 0, 108 | '@typescript-eslint/no-non-null-assertion': 0, 109 | '@typescript-eslint/no-unnecessary-type-assertion': 0, 110 | '@typescript-eslint/require-await': 0, 111 | '@typescript-eslint/no-for-in-array': 0, 112 | '@typescript-eslint/interface-name-prefix': 0, 113 | '@typescript-eslint/explicit-function-return-type': 0, 114 | '@typescript-eslint/no-explicit-any': 0, 115 | '@typescript-eslint/explicit-module-boundary-types': 0, 116 | '@typescript-eslint/no-floating-promises': 0, 117 | '@typescript-eslint/restrict-template-expressions': 0, 118 | '@typescript-eslint/no-unsafe-assignment': 0, 119 | '@typescript-eslint/no-unsafe-return': 0, 120 | '@typescript-eslint/no-unused-expressions': 0, 121 | '@typescript-eslint/no-misused-promises': 0, 122 | '@typescript-eslint/no-unsafe-member-access': 0, 123 | '@typescript-eslint/no-unsafe-call': 0, 124 | '@typescript-eslint/no-unsafe-argument': 0, 125 | '@typescript-eslint/ban-ts-comment': 0, 126 | }, 127 | 128 | settings: { 129 | extensions: ['.ts', '.d.ts', '.cts', '.mts', '.js', '.cjs', 'mjs', '.json'], 130 | }, 131 | }; 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | # other 38 | .history 39 | .env.dev 40 | .env.prod 41 | /entities 42 | *.http -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | pnpm-lock.yaml 4 | docker 5 | Dockerfile* 6 | LICENSE 7 | yarn-error.log 8 | .history 9 | .docusaurus 10 | .dockerignore 11 | .DS_Store 12 | .eslintignore 13 | .editorconfig 14 | .gitignore 15 | .prettierignore 16 | .eslintcache 17 | *.lock 18 | **/*.svg 19 | **/*.md 20 | **/*.svg 21 | **/*.ejs 22 | **/*.html 23 | **/*.png 24 | **/*.toml 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "proseWrap": "never", 6 | "endOfLine": "auto", 7 | "semi": true, 8 | "tabWidth": 4, 9 | "overrides": [ 10 | { 11 | "files": ".prettierrc", 12 | "options": { 13 | "parser": "json" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "debug start", 9 | "request": "launch", 10 | "runtimeArgs": ["run-script", "start:debug"], 11 | "autoAttachChildProcesses": true, 12 | "console": "integratedTerminal", 13 | "runtimeExecutable": "pnpm", 14 | "skipFiles": ["/**"], 15 | "type": "node" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "javascript.preferences.importModuleSpecifier": "project-relative", 7 | "typescript.suggest.jsdoc.generateReturns": false, 8 | "typescript.tsserver.experimental.enableProjectDiagnostics": true, 9 | "typescript.tsdk": "node_modules/typescript/lib", 10 | "stylelint.packageManager": "pnpm", 11 | "npm.packageManager": "pnpm" 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Prune Api

5 |
6 | 7 | ## 简介 8 | 9 | Prune Api 是现代化风格的快速开发平台 [Prune Admin](https://github.com/KangodYan/prune-admin) 的后端数据服务,基于 NestJS、TypeScript、TypeORM、MySQL 构建。 10 | 11 | ## 快速开始 12 | 13 | ### 获取项目代码 14 | 15 | ```bash 16 | git clone https://github.com/KangodYan/prune-api.git 17 | ``` 18 | 19 | ### 导入数据库文件 20 | 21 | - 在 MySQL8 数据库中新建数据库,命名 `prune` 22 | - 找到项目根目录下的 `prune.sql` 文件,导入到 `prune` 数据库中 23 | - 在数据库配置文件中去掉 sql_mode 配置项的 `ONLY_FULL_GROUP_BY` 选项,然后重启服务 24 | 25 | ### 安装依赖 26 | 27 | 在项目根目录下运行以下命令安装项目依赖: 28 | 29 | ```bash 30 | pnpm install 31 | ``` 32 | ### 添加配置 33 | 34 | 在根目录下创建 `.env.dev` 文件,手动复制 `.env.example` 文件内容到 `.env.dev`,并填写对应相应配置 35 | 36 | ### 启动开发服务器 37 | 38 | 运行以下命令以启动开发服务器: 39 | 40 | ```bash 41 | pnpm start:dev 42 | ``` 43 | 44 | 访问 [http://localhost:7441](http://localhost:7441) 查看您的应用程序。 45 | 46 | ### 构建生产版本 47 | 48 | 创建 `.env.prod` 文件并填写好配置,运行以下命令以构建生产版本: 49 | 50 | ```bash 51 | pnpm build 52 | ``` 53 | 54 | ### 在生产环境启动服务 55 | 56 | ```bash 57 | pnpm start:prod 58 | ``` 59 | -------------------------------------------------------------------------------- /generator.sh: -------------------------------------------------------------------------------- 1 | # 生成entity 2 | npx typeorm-model-generator -h localhost -d prune -u root -x 123456 -e mysql -o . 3 | # 生成controller、service、model 4 | nest g resource entity_name 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ormconfig.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "default", 4 | "type": "mysql", 5 | "host": "localhost", 6 | "port": 3306, 7 | "username": "root", 8 | "password": "123456", 9 | "database": "", 10 | "synchronize": false, 11 | "entities": ["entities/*.js"] 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prune-api", 3 | "version": "1.0.0", 4 | "author": "KangodYan", 5 | "private": true, 6 | "scripts": { 7 | "build": "nest build", 8 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 9 | "start": "cross-env NODE_ENV=dev nest start", 10 | "start:dev": "cross-env NODE_ENV=dev nest start --watch", 11 | "start:debug": "cross-env NODE_ENV=dev nest start --debug --watch", 12 | "start:prod": "cross-env NODE_ENV=prod node dist/main", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "test:cov": "jest --coverage", 17 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 18 | "test:e2e": "jest --config ./test/jest-e2e.json" 19 | }, 20 | "dependencies": { 21 | "@nestjs/common": "^10.0.1", 22 | "@nestjs/config": "^3.0.0", 23 | "@nestjs/core": "^10.0.1", 24 | "@nestjs/jwt": "^10.1.0", 25 | "@nestjs/mapped-types": "^2.0.0", 26 | "@nestjs/passport": "^10.0.0", 27 | "@nestjs/platform-express": "^10.0.1", 28 | "@nestjs/platform-fastify": "^10.0.1", 29 | "@nestjs/swagger": "^7.0.1", 30 | "@nestjs/typeorm": "^10.0.0", 31 | "@types/ali-oss": "^6.16.8", 32 | "@types/lodash": "^4.14.195", 33 | "@types/multer": "^1.4.7", 34 | "ali-oss": "^6.18.0", 35 | "class-transformer": "^0.5.1", 36 | "class-validator": "^0.14.0", 37 | "cross-env": "^7.0.3", 38 | "dayjs": "^1.11.9", 39 | "deepmerge": "^4.3.1", 40 | "dotenv": "^16.3.1", 41 | "exceljs": "^4.3.0", 42 | "fastify": "^4.21.0", 43 | "fastify-multer": "^2.0.3", 44 | "lodash": "^4.17.21", 45 | "md5": "^2.3.0", 46 | "mysql2": "^3.3.5", 47 | "passport-jwt": "^4.0.1", 48 | "qiniu": "^7.11.0", 49 | "reflect-metadata": "^0.1.13", 50 | "rxjs": "^7.8.1", 51 | "sanitize-html": "^2.10.0", 52 | "simple-flakeid": "^0.0.5", 53 | "typeorm": "^0.3.16" 54 | }, 55 | "devDependencies": { 56 | "@nestjs/cli": "^10.0.2", 57 | "@nestjs/schematics": "^10.0.1", 58 | "@nestjs/testing": "^10.0.1", 59 | "@types/express": "^4.17.17", 60 | "@types/jest": "29.5.2", 61 | "@types/md5": "^2.3.2", 62 | "@types/node": "20.3.1", 63 | "@types/passport-jwt": "^3.0.9", 64 | "@types/supertest": "^2.0.12", 65 | "@typescript-eslint/eslint-plugin": "^5.59.11", 66 | "@typescript-eslint/parser": "^5.59.11", 67 | "eslint": "^8.22.0", 68 | "eslint-config-airbnb-base": "^15.0.0", 69 | "eslint-config-airbnb-typescript": "^17.0.0", 70 | "eslint-config-prettier": "^8.8.0", 71 | "eslint-plugin-import": "^2.27.5", 72 | "eslint-plugin-jest": "^27.2.1", 73 | "eslint-plugin-prettier": "^4.2.1", 74 | "eslint-plugin-unused-imports": "^2.0.0", 75 | "jest": "29.5.0", 76 | "prettier": "^2.8.8", 77 | "source-map-support": "^0.5.21", 78 | "supertest": "^6.3.3", 79 | "ts-jest": "29.1.0", 80 | "ts-loader": "^9.4.3", 81 | "ts-node": "^10.9.1", 82 | "tsconfig-paths": "4.2.0", 83 | "typeorm-model-generator": "^0.4.6", 84 | "typescript": "^5.1.3" 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 | "pnpm": { 104 | "peerDependencyRules": { 105 | "ignoreMissing": [ 106 | "webpack" 107 | ] 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Logger, Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; 4 | 5 | import { JwtService } from '@nestjs/jwt'; 6 | 7 | import { database } from './config'; 8 | import { LoginGuard } from './modules/auth/login.guard'; 9 | import { AppFilter, AppIntercepter, AppPipe } from './modules/core/providers'; 10 | import { DatabaseModule } from './modules/database/database.module'; 11 | import { OrgModule } from './modules/org/org.module'; 12 | import { ResourceModule } from './modules/resource/resource.module'; 13 | import { OSSMiddlewareModule } from './modules/restful/oss/oss-middleware.module'; 14 | import { SystemModule } from './modules/system/system.module'; 15 | 16 | const envFilePath = ['.env']; 17 | 18 | export const IS_DEV = process.env.NODE_ENV !== 'prod'; 19 | 20 | if (IS_DEV) { 21 | envFilePath.unshift('.env.dev'); 22 | } else { 23 | envFilePath.unshift('.env.prod'); 24 | } 25 | 26 | @Module({ 27 | imports: [ 28 | DatabaseModule.forRoot(database), 29 | SystemModule, 30 | OrgModule, 31 | OSSMiddlewareModule, 32 | ResourceModule, 33 | ConfigModule.forRoot({ 34 | isGlobal: true, 35 | envFilePath, 36 | }), 37 | // 这里的secret无效,读取不到,异步也一样 38 | // JwtModule.register({ 39 | // global: true, 40 | // secret: process.env.JWT_SECRET, 41 | // signOptions: { 42 | // expiresIn: `${60 * 60 * 24}s`, 43 | // }, 44 | // }), 45 | ], 46 | providers: [ 47 | { 48 | provide: APP_PIPE, 49 | useValue: new AppPipe({ 50 | transform: true, 51 | forbidUnknownValues: false, 52 | validationError: { target: false }, 53 | }), 54 | }, 55 | { 56 | provide: APP_INTERCEPTOR, 57 | useClass: AppIntercepter, 58 | }, 59 | { 60 | provide: APP_FILTER, 61 | useClass: AppFilter, 62 | }, 63 | { 64 | provide: APP_GUARD, 65 | useClass: LoginGuard, 66 | }, 67 | // { 68 | // provide: APP_GUARD, 69 | // useClass: AuthorityGuard, 70 | // }, 71 | JwtService, 72 | Logger, 73 | ], 74 | }) 75 | export class AppModule {} 76 | -------------------------------------------------------------------------------- /src/config/database.config.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 2 | 3 | import { config } from 'dotenv'; 4 | 5 | config({ path: `.env.${process.env.NODE_ENV}` }); 6 | 7 | /** 8 | * 数据库配置 9 | */ 10 | export const database = (): TypeOrmModuleOptions => ({ 11 | charset: 'utf8mb4', 12 | logging: 'all', 13 | type: 'mysql', 14 | host: process.env.MYSQL_HOST, 15 | port: parseInt(process.env.MYSQL_PORT, 10), 16 | username: process.env.MYSQL_USERNAME, 17 | password: process.env.MYSQL_PASSWORD, 18 | database: process.env.MYSQL_DATABASE, 19 | synchronize: false, 20 | autoLoadEntities: true, 21 | bigNumberStrings: false, 22 | }); 23 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './database.config'; 2 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; 4 | import { useContainer } from 'class-validator'; 5 | import { contentParser } from 'fastify-multer'; 6 | 7 | import { AppModule } from './app.module'; 8 | 9 | const { PROTOCOL, HOST, PORT } = process.env; 10 | 11 | async function bootstrap() { 12 | const app = await NestFactory.create(AppModule, new FastifyAdapter()); 13 | // 加全局前缀 14 | app.setGlobalPrefix('api'); 15 | // 允许跨域 16 | app.enableCors(); 17 | useContainer(app.select(AppModule), { 18 | fallbackOnErrors: true, 19 | }); 20 | // 添加fastify上传文件的支持,FastifyAdapter不支持multipart/form-data 21 | // @ts-ignore (fastify-multer 在 TS 上没有适配 fastify 4.x,所以关闭,但不影响运行) 22 | app.register(contentParser); 23 | await app.listen(PORT, '0.0.0.0', () => { 24 | Logger.log(`服务已经启动,接口请访问:${PROTOCOL}${HOST}:${PORT}`); 25 | }); 26 | } 27 | bootstrap(); 28 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | 3 | import { LoginUserDto } from '../org/dtos'; 4 | 5 | import { AuthService } from './auth.service'; 6 | 7 | @Controller('auth') 8 | export class AuthController { 9 | constructor(private readonly authService: AuthService) {} 10 | 11 | @Post('signin') 12 | async signin(@Body() loginUserDto: LoginUserDto) { 13 | return this.authService.signin(loginUserDto); 14 | } 15 | 16 | @Get('refresh') 17 | async refresh(@Query('refreshToken') refreshToken: string) { 18 | return this.authService.refresh(refreshToken); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/auth/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const RequireLogin = () => SetMetadata('require-login', true); 4 | 5 | export const RequireAuthority = (...resources: string[]) => 6 | SetMetadata('require-resource', resources); 7 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | 3 | import { JwtService } from '@nestjs/jwt'; 4 | import { isEmpty } from 'lodash'; 5 | 6 | import md5 from 'md5'; 7 | 8 | import { LoginUserDto } from '../org/dtos'; 9 | import { UserService } from '../org/services'; 10 | import { ACCOUNT_NOT_EXIST, LOGIN_ERROR, SUCCESS } from '../restful/base/code.constants'; 11 | 12 | import { REFRESH_TOKEN_EXPIRE_TIME, TOKEN_EXPIRE_TIME } from './constants'; 13 | 14 | @Injectable() 15 | export class AuthService { 16 | constructor( 17 | private readonly userService: UserService, 18 | private readonly jwtService: JwtService, 19 | ) {} 20 | 21 | /** 22 | * 登录 23 | */ 24 | async signin(loginUserDto: LoginUserDto) { 25 | // 验证账号 26 | const userList = await this.userService.getUserAndRoles({ 27 | account: loginUserDto.account, 28 | page: 1, 29 | limit: 1, 30 | }); 31 | if (isEmpty(userList)) { 32 | return { code: ACCOUNT_NOT_EXIST, message: '该账号不存在' }; 33 | } 34 | const user = userList[0]; 35 | // 验证密码 36 | if (user.password !== md5(loginUserDto.password)) { 37 | return { code: LOGIN_ERROR, message: '账号或密码错误' }; 38 | } 39 | // 返回token 40 | const accessToken = this.jwtService.sign( 41 | { 42 | id: user.id, 43 | name: user.name, 44 | userRoles: user.userRoles, 45 | }, 46 | { 47 | expiresIn: TOKEN_EXPIRE_TIME, 48 | }, 49 | ); 50 | const refreshToken = this.jwtService.sign( 51 | { 52 | id: user.id, 53 | }, 54 | { expiresIn: REFRESH_TOKEN_EXPIRE_TIME }, 55 | ); 56 | // 返回登录后用户信息 57 | const userSigninAfterInfo = await this.userService.getUserSigninAfterInfo({ 58 | userId: user.id, 59 | }); 60 | return { 61 | status: SUCCESS, 62 | message: '登录成功', 63 | data: { user: userSigninAfterInfo, accessToken, refreshToken }, 64 | }; 65 | } 66 | 67 | /** 68 | * 刷新token 69 | */ 70 | async refresh(refreshToken: string) { 71 | if (!refreshToken) { 72 | return null; 73 | } 74 | let data; 75 | // 验证账号 76 | try { 77 | data = this.jwtService.verify(refreshToken, { 78 | secret: process.env.JWT_SECRET, 79 | }); 80 | } catch (e) { 81 | // 出现 TokenExpiredError,表示 refreshToken 也过期了,抛给前端处理 82 | throw new UnauthorizedException('refresh 失效,请重新登录'); 83 | } 84 | // 查询需要的角色数据 85 | const userList = await this.userService.getUserAndRoles({ 86 | id: data.id, 87 | page: 1, 88 | limit: 1, 89 | }); 90 | if (isEmpty(userList)) { 91 | return { code: ACCOUNT_NOT_EXIST, message: '该账号不存在' }; 92 | } 93 | const user = userList[0]; 94 | // 返回token 95 | const accessToken = this.jwtService.sign( 96 | { 97 | id: user.id, 98 | name: user.name, 99 | userRoles: user.userRoles, 100 | }, 101 | { 102 | expiresIn: TOKEN_EXPIRE_TIME, 103 | }, 104 | ); 105 | const newRefreshToken = this.jwtService.sign( 106 | { 107 | id: user.id, 108 | }, 109 | { expiresIn: REFRESH_TOKEN_EXPIRE_TIME }, 110 | ); 111 | return { accessToken, refreshToken: newRefreshToken }; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/modules/auth/authority.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | CanActivate, 4 | ExecutionContext, 5 | Inject, 6 | ForbiddenException, 7 | } from '@nestjs/common'; 8 | import { Reflector } from '@nestjs/core'; 9 | import { Request } from 'express'; 10 | 11 | import { AUTHORITY_TYPE_RESOURCE, LIST_MAX_LIMIT } from '../org/constants'; 12 | import { RoleAuthorityService } from '../org/services'; 13 | 14 | @Injectable() 15 | export class AuthorityGuard implements CanActivate { 16 | @Inject(RoleAuthorityService) 17 | private roleAuthorityService: RoleAuthorityService; 18 | 19 | @Inject(Reflector) 20 | private reflector: Reflector; 21 | 22 | async canActivate(context: ExecutionContext): Promise { 23 | const request: Request = context.switchToHttp().getRequest(); 24 | if (!request.user) { 25 | return true; 26 | } 27 | // 查询访问API注解上的资源编码 28 | const requiredResources = this.reflector.getAllAndOverride('require-resource', [ 29 | context.getClass(), 30 | context.getHandler(), 31 | ]); 32 | if (!requiredResources) { 33 | return true; 34 | } 35 | // 查询用户的角色对应的资源 36 | const roleAuthorities = await this.roleAuthorityService.listRoleAuthorityByRoleIds({ 37 | roleIds: request.user.userRoles.map((item) => item.role.id), 38 | authorityType: AUTHORITY_TYPE_RESOURCE, 39 | page: 1, 40 | limit: LIST_MAX_LIMIT, 41 | }); 42 | const resources = roleAuthorities.map((item) => item.resource); 43 | if (!resources) { 44 | throw new ForbiddenException('您没有访问该接口的权限'); 45 | } 46 | // 循环对比是否对应 47 | for (const curResource of requiredResources) { 48 | const found = resources.find((item) => item.code.includes(curResource)); 49 | if (!found) { 50 | throw new ForbiddenException('您没有访问该接口的权限'); 51 | } 52 | } 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/auth/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * token失效时间 3 | */ 4 | export const TOKEN_EXPIRE_TIME = `${60 * 60 * 24}s`; 5 | /** 6 | * refresh_token失效时间 7 | */ 8 | export const REFRESH_TOKEN_EXPIRE_TIME = '7d'; 9 | -------------------------------------------------------------------------------- /src/modules/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | 5 | import { UserEntity } from '../org/entities'; 6 | /** 7 | * @Deprecated 由LoginGuard中逻辑替代 8 | */ 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor() { 12 | super({ 13 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 14 | secretOrKey: process.env.JWT_SECRET, 15 | }); 16 | } 17 | 18 | // 在每次请求会走的验证,从请求头中验证token中的用户 19 | async validate(user: UserEntity): Promise { 20 | if (!user.id) { 21 | throw new UnauthorizedException(); 22 | } 23 | return user; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/auth/login.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | CanActivate, 4 | ExecutionContext, 5 | UnauthorizedException, 6 | Inject, 7 | } from '@nestjs/common'; 8 | import { Reflector } from '@nestjs/core'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | import { Request } from 'express'; 11 | import { Observable } from 'rxjs'; 12 | 13 | import { UserRoleRelationEntity } from '../org/entities'; 14 | 15 | declare module 'express' { 16 | interface Request { 17 | user: { 18 | id: number; 19 | name: string; 20 | userRoles: UserRoleRelationEntity[]; 21 | }; 22 | } 23 | } 24 | 25 | @Injectable() 26 | export class LoginGuard implements CanActivate { 27 | @Inject(JwtService) 28 | private jwtService: JwtService; 29 | 30 | @Inject() 31 | private reflector: Reflector; 32 | 33 | canActivate(context: ExecutionContext): boolean | Promise | Observable { 34 | const request: Request = context.switchToHttp().getRequest(); 35 | // 读取自定义注解,有此注解才拦截,需要登录 36 | const requireLogin = this.reflector.getAllAndOverride('require-login', [ 37 | context.getClass(), 38 | context.getHandler(), 39 | ]); 40 | if (!requireLogin) { 41 | return true; 42 | } 43 | // 从请求头获取授权信息 44 | const { authorization } = request.headers; 45 | if (!authorization) { 46 | throw new UnauthorizedException('用户未登录'); 47 | } 48 | try { 49 | const token = authorization.split(' ')[1]; 50 | const data = this.jwtService.verify(token, { 51 | secret: process.env.JWT_SECRET, 52 | }); 53 | // token解析后的用户数据放到request.user中,后续从request中获取 54 | request.user = data; 55 | return true; 56 | } catch (e) { 57 | console.log(e); 58 | throw new UnauthorizedException('token 失效,请重新登录'); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/modules/core/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * DTOValidation装饰器选项 3 | */ 4 | export const DTO_VALIDATION_OPTIONS = 'dto_validation_options'; 5 | -------------------------------------------------------------------------------- /src/modules/core/decorators/dto-validation.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Paramtype, SetMetadata } from '@nestjs/common'; 2 | import { ClassTransformOptions } from '@nestjs/common/interfaces/external/class-transform-options.interface'; 3 | import { ValidatorOptions } from '@nestjs/common/interfaces/external/validator-options.interface'; 4 | 5 | import { DTO_VALIDATION_OPTIONS } from '../constants'; 6 | 7 | /** 8 | * 用于配置通过全局验证管道验证数据的DTO类装饰器 9 | * @param options 10 | */ 11 | export const DtoValidation = ( 12 | options?: ValidatorOptions & { 13 | transformOptions?: ClassTransformOptions; 14 | } & { type?: Paramtype }, 15 | ) => SetMetadata(DTO_VALIDATION_OPTIONS, options ?? {}); 16 | -------------------------------------------------------------------------------- /src/modules/core/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dto-validation.decorator'; 2 | -------------------------------------------------------------------------------- /src/modules/core/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | -------------------------------------------------------------------------------- /src/modules/core/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from 'lodash'; 2 | import { ValueTransformer } from 'typeorm'; 3 | 4 | /** 5 | * 用于请求验证中的boolean数据转义 6 | * @param value 7 | */ 8 | export function toBoolean(value?: string | boolean): boolean { 9 | if (isNil(value)) return false; 10 | if (typeof value === 'boolean') return value; 11 | try { 12 | return JSON.parse(value.toLowerCase()); 13 | } catch (error) { 14 | return value as unknown as boolean; 15 | } 16 | } 17 | 18 | /** 19 | * 用于请求验证中转义null 20 | * @param value 21 | */ 22 | export function toNull(value?: string | null): string | null | undefined { 23 | return value === 'null' ? null : value; 24 | } 25 | 26 | /** 27 | * MySQL中bit类型转换为boolean 28 | */ 29 | export class BoolBitTransformer implements ValueTransformer { 30 | // To db from typeorm 31 | to(value: boolean | null): Buffer | null { 32 | if (value === null) { 33 | return null; 34 | } 35 | const res = Buffer.from('1'); 36 | res[0] = value ? 1 : 0; 37 | return res; 38 | } 39 | 40 | // From db to typeorm 41 | from(value: Buffer): boolean | null { 42 | if (value === null) { 43 | return null; 44 | } 45 | return value[0] === 1; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/core/providers/app.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, HttpException, HttpStatus, Type } from '@nestjs/common'; 2 | import { BaseExceptionFilter } from '@nestjs/core'; 3 | import { isObject } from 'lodash'; 4 | import { EntityPropertyNotFoundError, QueryFailedError } from 'typeorm'; 5 | import { EntityNotFoundError } from 'typeorm/error/EntityNotFoundError'; 6 | 7 | /** 8 | * 全局过滤器,用于响应自定义异常 9 | */ 10 | @Catch() 11 | export class AppFilter extends BaseExceptionFilter { 12 | protected resExceptions: Array<{ class: Type; status?: number } | Type> = [ 13 | { class: EntityNotFoundError, status: HttpStatus.NOT_FOUND }, 14 | { class: QueryFailedError, status: HttpStatus.BAD_REQUEST }, 15 | { class: EntityPropertyNotFoundError, status: HttpStatus.BAD_REQUEST }, 16 | ]; 17 | 18 | catch(exception: T, host: ArgumentsHost) { 19 | const applicationRef = 20 | this.applicationRef || (this.httpAdapterHost && this.httpAdapterHost.httpAdapter)!; 21 | // 是否在自定义的异常处理类列表中 22 | const resException = this.resExceptions.find((item) => 23 | 'class' in item ? exception instanceof item.class : exception instanceof item, 24 | ); 25 | 26 | // 如果不在自定义异常处理类列表也没有继承自HttpException 27 | if (!resException && !(exception instanceof HttpException)) { 28 | return this.handleUnknownError(exception, host, applicationRef); 29 | } 30 | let res: string | object = ''; 31 | let status = HttpStatus.INTERNAL_SERVER_ERROR; 32 | if (exception instanceof HttpException) { 33 | res = exception.getResponse(); 34 | status = exception.getStatus(); 35 | } else if (resException) { 36 | // 如果在自定义异常处理类列表中 37 | const e = exception as unknown as Error; 38 | res = e.message; 39 | if ('class' in resException && resException.status) { 40 | status = resException.status; 41 | } 42 | } 43 | const message = isObject(res) 44 | ? res 45 | : { 46 | statusCode: status, 47 | message: res, 48 | }; 49 | applicationRef!.reply(host.getArgByIndex(1), message, status); 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/core/providers/app.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { ClassSerializerInterceptor, PlainLiteralObject, StreamableFile } from '@nestjs/common'; 2 | import { ClassTransformOptions } from '@nestjs/common/interfaces/external/class-transform-options.interface'; 3 | import { isArray, isNil, isObject } from 'lodash'; 4 | /** 5 | * 全局拦截器,用于序列化数据 6 | */ 7 | export class AppIntercepter extends ClassSerializerInterceptor { 8 | serialize( 9 | response: PlainLiteralObject | Array, 10 | options: ClassTransformOptions, 11 | ): PlainLiteralObject | PlainLiteralObject[] { 12 | if ((!isObject(response) && !isArray(response)) || response instanceof StreamableFile) { 13 | return response; 14 | } 15 | 16 | // 如果是响应数据是数组,则遍历对每一项进行序列化 17 | if (isArray(response)) { 18 | return (response as PlainLiteralObject[]).map((item) => 19 | !isObject(item) ? item : this.transformToPlain(item, options), 20 | ); 21 | } 22 | // 如果是分页数据,则对items中的每一项进行序列化 23 | if ('meta' in response && 'items' in response) { 24 | const items = !isNil(response.items) && isArray(response.items) ? response.items : []; 25 | return { 26 | ...response, 27 | items: (items as PlainLiteralObject[]).map((item) => { 28 | return !isObject(item) ? item : this.transformToPlain(item, options); 29 | }), 30 | }; 31 | } 32 | // 如果响应是个对象则直接序列化 33 | return this.transformToPlain(response, options); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/core/providers/app.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, Injectable, Paramtype, ValidationPipe } from '@nestjs/common'; 2 | import merge from 'deepmerge'; 3 | import { isObject, omit } from 'lodash'; 4 | 5 | import { DTO_VALIDATION_OPTIONS } from '../constants'; 6 | 7 | /** 8 | * 全局管道,用于处理DTO验证 9 | */ 10 | @Injectable() 11 | export class AppPipe extends ValidationPipe { 12 | async transform(value: any, metadata: ArgumentMetadata) { 13 | const { metatype, type } = metadata; 14 | // 获取要验证的dto类 15 | const dto = metatype as any; 16 | // 获取dto类的装饰器元数据中的自定义验证选项 17 | const options = Reflect.getMetadata(DTO_VALIDATION_OPTIONS, dto) || {}; 18 | // 把当前已设置的选项解构到备份对象 19 | const originOptions = { ...this.validatorOptions }; 20 | // 把当前已设置的class-transform选项解构到备份对象 21 | const originTransform = { ...this.transformOptions }; 22 | // 把自定义的class-transform和type选项解构出来 23 | const { transformOptions, type: optionsType, ...customOptions } = options; 24 | // 根据DTO类上设置的type来设置当前的DTO请求类型,默认为'body' 25 | const requestType: Paramtype = optionsType ?? 'body'; 26 | // 如果被验证的DTO设置的请求类型与被验证的数据的请求类型不是同一种类型则跳过此管道 27 | if (requestType !== type) return value; 28 | 29 | // 合并当前transform选项和自定义选项 30 | if (transformOptions) { 31 | this.transformOptions = merge(this.transformOptions, transformOptions ?? {}, { 32 | arrayMerge: (_d, s, _o) => s, 33 | }); 34 | } 35 | // 合并当前验证选项和自定义选项 36 | this.validatorOptions = merge(this.validatorOptions, customOptions ?? {}, { 37 | arrayMerge: (_d, s, _o) => s, 38 | }); 39 | const toValidate = isObject(value) 40 | ? Object.fromEntries( 41 | Object.entries(value as Record).map(([key, v]) => { 42 | if (!isObject(v) || !('mimetype' in v)) return [key, v]; 43 | return [key, omit(v, ['fields'])]; 44 | }), 45 | ) 46 | : value; 47 | // 序列化并验证dto对象 48 | let result = await super.transform(toValidate, metadata); 49 | // 如果dto类的中存在transform静态方法,则返回调用进一步transform之后的结果 50 | if (typeof result.transform === 'function') { 51 | result = await result.transform(result); 52 | const { transform, ...data } = result; 53 | result = data; 54 | } 55 | // 重置验证选项 56 | this.validatorOptions = originOptions; 57 | // 重置transform选项 58 | this.transformOptions = originTransform; 59 | return result; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/modules/core/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.pipe'; 2 | export * from './app.filter'; 3 | export * from './app.interceptor'; 4 | -------------------------------------------------------------------------------- /src/modules/database/base/entity.ts: -------------------------------------------------------------------------------- 1 | import { Expose } from 'class-transformer'; 2 | import { BaseEntity as TypeOrmBaseEntity, Column } from 'typeorm'; 3 | 4 | export class BaseEntity extends TypeOrmBaseEntity { 5 | @Expose() 6 | @Column('varchar', { primary: true, name: 'id', comment: '主键', length: 20 }) 7 | id: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/database/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entity'; 2 | export * from './repository'; 3 | export * from './service'; 4 | export * from './tree.repository'; 5 | // export * from './subscriber'; 6 | -------------------------------------------------------------------------------- /src/modules/database/base/repository.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from 'lodash'; 2 | import { ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm'; 3 | 4 | import { OrderType } from '../constants'; 5 | import { getOrderByQuery } from '../helpers'; 6 | import { OrderQueryType } from '../types'; 7 | 8 | /** 9 | * 基础存储类 10 | */ 11 | export abstract class BaseRepository extends Repository { 12 | /** 13 | * 构建查询时默认的模型对应的查询名称 14 | */ 15 | protected abstract _qbName: string; 16 | 17 | /** 18 | * 默认排序规则,可以通过每个方法的orderBy选项进行覆盖 19 | */ 20 | protected orderBy?: string | { name: string; order: `${OrderType}` }; 21 | 22 | /** 23 | * 返回查询器名称 24 | */ 25 | get qbName() { 26 | return this._qbName; 27 | } 28 | 29 | /** 30 | * 构建基础查询器 31 | */ 32 | buildBaseQB(): SelectQueryBuilder { 33 | return this.createQueryBuilder(this.qbName); 34 | } 35 | 36 | /** 37 | * 生成排序的QueryBuilder 38 | * @param qb 39 | * @param orderBy 40 | */ 41 | addOrderByQuery(qb: SelectQueryBuilder, orderBy?: OrderQueryType) { 42 | const orderByQuery = orderBy ?? this.orderBy; 43 | return !isNil(orderByQuery) ? getOrderByQuery(qb, this.qbName, orderByQuery) : qb; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/database/base/subscriber.ts: -------------------------------------------------------------------------------- 1 | // import { isNil } from 'lodash'; 2 | // import { 3 | // EntitySubscriberInterface, 4 | // EventSubscriber, 5 | // ObjectLiteral, 6 | // ObjectType, 7 | // UpdateEvent, 8 | // InsertEvent, 9 | // SoftRemoveEvent, 10 | // RemoveEvent, 11 | // RecoverEvent, 12 | // TransactionStartEvent, 13 | // TransactionCommitEvent, 14 | // TransactionRollbackEvent, 15 | // EntityTarget, 16 | // DataSource, 17 | // } from 'typeorm'; 18 | // 19 | // import { getCustomRepository } from '../helpers'; 20 | // 21 | // import { RepositoryType } from '../types'; 22 | // 23 | // type SubscriberEvent = 24 | // | InsertEvent 25 | // | UpdateEvent 26 | // | SoftRemoveEvent 27 | // | RemoveEvent 28 | // | RecoverEvent 29 | // | TransactionStartEvent 30 | // | TransactionCommitEvent 31 | // | TransactionRollbackEvent; 32 | // 33 | // /** 34 | // * 基础模型观察者 35 | // */ 36 | // @EventSubscriber() 37 | // export abstract class BaseSubscriber 38 | // implements EntitySubscriberInterface 39 | // { 40 | // /** 41 | // * 监听的模型 42 | // */ 43 | // protected abstract entity: ObjectType; 44 | // 45 | // /** 46 | // * 构造函数 47 | // * @param dataSource 数据连接池 48 | // */ 49 | // constructor(protected dataSource: DataSource) { 50 | // this.dataSource.subscribers.push(this); 51 | // } 52 | // 53 | // listenTo() { 54 | // return this.entity; 55 | // } 56 | // 57 | // async afterLoad(entity: any) { 58 | // // 是否启用树形 59 | // if ('parent' in entity && isNil(entity.depth)) entity.depth = 0; 60 | // } 61 | // 62 | // protected getDataSource(event: SubscriberEvent) { 63 | // return event.connection; 64 | // } 65 | // 66 | // protected getManage(event: SubscriberEvent) { 67 | // return event.manager; 68 | // } 69 | // 70 | // protected getRepositoy< 71 | // C extends ClassType, 72 | // T extends RepositoryType, 73 | // A extends EntityTarget, 74 | // >(event: SubscriberEvent, repository?: C, entity?: A) { 75 | // return isNil(repository) 76 | // ? this.getDataSource(event).getRepository(entity ?? this.entity) 77 | // : getCustomRepository(this.getDataSource(event), repository); 78 | // } 79 | // 80 | // /** 81 | // * 判断某个字段是否被更新 82 | // * @param cloumn 83 | // * @param event 84 | // */ 85 | // protected isUpdated(cloumn: keyof E, event: UpdateEvent) { 86 | // return !!event.updatedColumns.find((item) => item.propertyName === cloumn); 87 | // } 88 | // } 89 | -------------------------------------------------------------------------------- /src/modules/database/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 传入CustomRepository装饰器的metadata数据标识 3 | */ 4 | export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA'; 5 | 6 | /** 7 | * 软删除数据查询类型 8 | */ 9 | export enum SelectTrashMode { 10 | ALL = 'all', // 包含已软删除和未软删除的数据 11 | ONLY = 'only', // 只包含软删除的数据 12 | NONE = 'none', // 只包含未软删除的数据 13 | } 14 | 15 | /** 16 | * 排序方式 17 | */ 18 | export enum OrderType { 19 | ASC = 'ASC', 20 | DESC = 'DESC', 21 | } 22 | 23 | /** 24 | * 树形模型在删除父级后子级的处理方式 25 | */ 26 | export enum TreeChildrenResolve { 27 | DELETE = 'delete', 28 | UP = 'up', 29 | ROOT = 'root', 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/database/constraints/data.exist.constraint.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | registerDecorator, 4 | ValidationArguments, 5 | ValidationOptions, 6 | ValidatorConstraint, 7 | ValidatorConstraintInterface, 8 | } from 'class-validator'; 9 | import { DataSource, ObjectType, Repository } from 'typeorm'; 10 | 11 | type Condition = { 12 | entity: ObjectType; 13 | /** 14 | * 用于查询的比对字段,默认id 15 | */ 16 | map?: string; 17 | }; 18 | /** 19 | * 查询某个字段的值是否在数据表中存在 20 | */ 21 | @ValidatorConstraint({ name: 'dataExist', async: true }) 22 | @Injectable() 23 | export class DataExistConstraint implements ValidatorConstraintInterface { 24 | constructor(private dataSource: DataSource) {} 25 | 26 | async validate(value: string, args: ValidationArguments) { 27 | let repo: Repository; 28 | if (!value) return true; 29 | // 默认对比字段是id 30 | let map = 'id'; 31 | // 通过传入的entity获取其repository 32 | if ('entity' in args.constraints[0]) { 33 | map = args.constraints[0].map ?? 'id'; 34 | repo = this.dataSource.getRepository(args.constraints[0].entity); 35 | } else { 36 | repo = this.dataSource.getRepository(args.constraints[0]); 37 | } 38 | // 通过查询记录是否存在进行验证 39 | const item = await repo.findOne({ where: { [map]: value } }); 40 | return !!item; 41 | } 42 | 43 | defaultMessage(args: ValidationArguments) { 44 | if (!args.constraints[0]) { 45 | return 'Model not been specified!'; 46 | } 47 | return `All instance of ${args.constraints[0].name} must been exists in databse!`; 48 | } 49 | } 50 | 51 | /** 52 | * 模型存在性验证 53 | * @param entity 54 | * @param validationOptions 55 | */ 56 | function IsDataExist( 57 | entity: ObjectType, 58 | validationOptions?: ValidationOptions, 59 | ): (object: Record, propertyName: string) => void; 60 | 61 | /** 62 | * 模型存在性验证 63 | * @param condition 64 | * @param validationOptions 65 | */ 66 | function IsDataExist( 67 | condition: Condition, 68 | validationOptions?: ValidationOptions, 69 | ): (object: Record, propertyName: string) => void; 70 | 71 | /** 72 | * 模型存在性验证 73 | * @param condition 74 | * @param validationOptions 75 | */ 76 | function IsDataExist( 77 | condition: ObjectType | Condition, 78 | validationOptions?: ValidationOptions, 79 | ): (object: Record, propertyName: string) => void { 80 | return (object: Record, propertyName: string) => { 81 | registerDecorator({ 82 | target: object.constructor, 83 | propertyName, 84 | options: validationOptions, 85 | constraints: [condition], 86 | validator: DataExistConstraint, 87 | }); 88 | }; 89 | } 90 | 91 | export { IsDataExist }; 92 | -------------------------------------------------------------------------------- /src/modules/database/constraints/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data.exist.constraint'; 2 | export * from './unique.constraint'; 3 | export * from './unique.exist.constraint'; 4 | export * from './tree.unique.constraint'; 5 | export * from './tree.unique.exist.constraint'; 6 | -------------------------------------------------------------------------------- /src/modules/database/constraints/tree.unique.constraint.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | registerDecorator, 4 | ValidationArguments, 5 | ValidationOptions, 6 | ValidatorConstraint, 7 | ValidatorConstraintInterface, 8 | } from 'class-validator'; 9 | import merge from 'deepmerge'; 10 | import { isNil } from 'lodash'; 11 | import { DataSource, ObjectType } from 'typeorm'; 12 | 13 | type Condition = { 14 | entity: ObjectType; 15 | parentKey?: string; 16 | property?: string; 17 | }; 18 | 19 | /** 20 | * 验证树形模型下同父节点同级别某个字段的唯一性 21 | */ 22 | @Injectable() 23 | @ValidatorConstraint({ name: 'treeDataUnique', async: true }) 24 | export class UniqueTreeConstraint implements ValidatorConstraintInterface { 25 | constructor(private dataSource: DataSource) {} 26 | 27 | async validate(value: any, args: ValidationArguments) { 28 | const config: Omit = { 29 | parentKey: 'parent', 30 | property: args.property, 31 | }; 32 | const condition = ('entity' in args.constraints[0] 33 | ? merge(config, args.constraints[0]) 34 | : { 35 | ...config, 36 | entity: args.constraints[0], 37 | }) as unknown as Required; 38 | // 需要查询的属性名,默认为当前验证的属性 39 | const argsObj = args.object as any; 40 | if (!condition.entity) return false; 41 | 42 | try { 43 | // 获取repository 44 | const repo = this.dataSource.getTreeRepository(condition.entity); 45 | 46 | if (isNil(value)) return true; 47 | const collection = await repo.find({ 48 | where: { 49 | parent: !argsObj[condition.parentKey] 50 | ? null 51 | : { id: argsObj[condition.parentKey] }, 52 | }, 53 | withDeleted: true, 54 | }); 55 | // 对比每个子分类的queryProperty值是否与当前验证的dto属性相同,如果有相同的则验证失败 56 | return collection.every((item) => item[condition.property] !== value); 57 | } catch (err) { 58 | console.log(`UniqueTreeConstraint failed, err=${err}`); 59 | return false; 60 | } 61 | } 62 | 63 | defaultMessage(args: ValidationArguments) { 64 | const { entity, property } = args.constraints[0]; 65 | const queryProperty = property ?? args.property; 66 | if (!entity) { 67 | return 'Model not been specified!'; 68 | } 69 | return `${queryProperty} of ${entity.name} must been unique with siblings element!`; 70 | } 71 | } 72 | 73 | /** 74 | * 树形模型下同父节点同级别某个字段的唯一性验证 75 | * @param params 76 | * @param validationOptions 77 | */ 78 | export function IsTreeUnique( 79 | params: ObjectType | Condition, 80 | validationOptions?: ValidationOptions, 81 | ) { 82 | return (object: Record, propertyName: string) => { 83 | registerDecorator({ 84 | target: object.constructor, 85 | propertyName, 86 | options: validationOptions, 87 | constraints: [params], 88 | validator: UniqueTreeConstraint, 89 | }); 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/modules/database/constraints/tree.unique.exist.constraint.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | registerDecorator, 4 | ValidationArguments, 5 | ValidationOptions, 6 | ValidatorConstraint, 7 | ValidatorConstraintInterface, 8 | } from 'class-validator'; 9 | import merge from 'deepmerge'; 10 | import { DataSource, ObjectType } from 'typeorm'; 11 | 12 | type Condition = { 13 | entity: ObjectType; 14 | /** 15 | * 默认忽略字段为id 16 | */ 17 | ignore?: string; 18 | /** 19 | * 查询条件字段,默认为指定的ignore 20 | */ 21 | findKey?: string; 22 | /** 23 | * 需要查询的属性名,默认为当前验证的属性 24 | */ 25 | property?: string; 26 | }; 27 | 28 | /** 29 | * 在更新时验证树形数据同父节点同级别某个字段的唯一性,通过ignore指定忽略的字段 30 | */ 31 | @Injectable() 32 | @ValidatorConstraint({ name: 'treeDataUniqueExist', async: true }) 33 | export class UniqueTreeExistConstraint implements ValidatorConstraintInterface { 34 | constructor(private dataSource: DataSource) {} 35 | 36 | async validate(value: any, args: ValidationArguments) { 37 | const config: Omit = { 38 | ignore: 'id', 39 | property: args.property, 40 | }; 41 | const condition = ('entity' in args.constraints[0] 42 | ? merge(config, args.constraints[0]) 43 | : { 44 | ...config, 45 | entity: args.constraints[0], 46 | }) as unknown as Required; 47 | if (!condition.findKey) { 48 | condition.findKey = condition.ignore; 49 | } 50 | if (!condition.entity) return false; 51 | // 在传入的dto数据中获取需要忽略的字段的值 52 | const ignoreValue = (args.object as any)[condition.ignore]; 53 | // 查询条件字段的值 54 | const keyValue = (args.object as any)[condition.findKey]; 55 | if (!ignoreValue || !keyValue) return false; 56 | const repo = this.dataSource.getTreeRepository(condition.entity); 57 | // 根据查询条件查询出当前验证的数据 58 | const item = await repo.findOne({ 59 | where: { [condition.findKey]: keyValue }, 60 | relations: ['parent'], 61 | }); 62 | // 没有此数据则验证失败 63 | if (!item) return false; 64 | // 如果验证数据没有parent则把所有顶级分类作为验证数据否则就把同一个父分类下的子分类作为验证数据 65 | const rows: any[] = await repo.find({ 66 | where: { 67 | parent: !item.parent ? null : { id: item.parent.id }, 68 | }, 69 | withDeleted: true, 70 | }); 71 | // 在忽略本身数据后如果同级别其它数据与验证的queryProperty的值相同则验证失败 72 | return !rows.find( 73 | (row) => row[condition.property] === value && row[condition.ignore] !== ignoreValue, 74 | ); 75 | } 76 | 77 | defaultMessage(args: ValidationArguments) { 78 | const { entity, property } = args.constraints[0]; 79 | const queryProperty = property ?? args.property; 80 | if (!entity) { 81 | return 'Model not been specified!'; 82 | } 83 | return `${queryProperty} of ${entity.name} must been unique with siblings element!`; 84 | } 85 | } 86 | 87 | /** 88 | * 树形数据同父节点同级别某个字段的唯一性验证 89 | * @param params 90 | * @param validationOptions 91 | */ 92 | export function IsTreeUniqueExist( 93 | params: ObjectType | Condition, 94 | validationOptions?: ValidationOptions, 95 | ) { 96 | return (object: Record, propertyName: string) => { 97 | registerDecorator({ 98 | target: object.constructor, 99 | propertyName, 100 | options: validationOptions, 101 | constraints: [params], 102 | validator: UniqueTreeExistConstraint, 103 | }); 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /src/modules/database/constraints/unique.constraint.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | registerDecorator, 4 | ValidationArguments, 5 | ValidationOptions, 6 | ValidatorConstraint, 7 | ValidatorConstraintInterface, 8 | } from 'class-validator'; 9 | import merge from 'deepmerge'; 10 | import { isNil } from 'lodash'; 11 | import { DataSource, ObjectType } from 'typeorm'; 12 | 13 | type Condition = { 14 | entity: ObjectType; 15 | /** 16 | * 如果没有指定字段则使用当前验证的属性作为查询依据 17 | */ 18 | property?: string; 19 | }; 20 | 21 | /** 22 | * 验证某个字段的唯一性 23 | */ 24 | @ValidatorConstraint({ name: 'dataUnique', async: true }) 25 | @Injectable() 26 | export class UniqueConstraint implements ValidatorConstraintInterface { 27 | constructor(private dataSource: DataSource) {} 28 | 29 | async validate(value: any, args: ValidationArguments) { 30 | // 获取要验证的模型和字段 31 | const config: Omit = { 32 | property: args.property, 33 | }; 34 | const condition = ('entity' in args.constraints[0] 35 | ? merge(config, args.constraints[0]) 36 | : { 37 | ...config, 38 | entity: args.constraints[0], 39 | }) as unknown as Required; 40 | if (!condition.entity) return false; 41 | try { 42 | // 查询是否存在数据,如果已经存在则验证失败 43 | const repo = this.dataSource.getRepository(condition.entity); 44 | return isNil( 45 | await repo.findOne({ where: { [condition.property]: value }, withDeleted: true }), 46 | ); 47 | } catch (err) { 48 | // 如果数据库操作异常则验证失败 49 | console.log(`UniqueConstraint failed, err=${err}`); 50 | return false; 51 | } 52 | } 53 | 54 | defaultMessage(args: ValidationArguments) { 55 | const { entity, property } = args.constraints[0]; 56 | const queryProperty = property ?? args.property; 57 | if (!(args.object as any).getManager) { 58 | return 'getManager function not been found!'; 59 | } 60 | if (!entity) { 61 | return 'Model not been specified!'; 62 | } 63 | return `${queryProperty} of ${entity.name} must been unique!`; 64 | } 65 | } 66 | 67 | /** 68 | * 数据唯一性验证 69 | * @param params Entity类或验证条件对象 70 | * @param validationOptions 71 | */ 72 | export function IsUnique( 73 | params: ObjectType | Condition, 74 | validationOptions?: ValidationOptions, 75 | ) { 76 | return (object: Record, propertyName: string) => { 77 | registerDecorator({ 78 | target: object.constructor, 79 | propertyName, 80 | options: validationOptions, 81 | constraints: [params], 82 | validator: UniqueConstraint, 83 | }); 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /src/modules/database/constraints/unique.exist.constraint.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | registerDecorator, 4 | ValidationArguments, 5 | ValidationOptions, 6 | ValidatorConstraint, 7 | ValidatorConstraintInterface, 8 | } from 'class-validator'; 9 | import merge from 'deepmerge'; 10 | import { isNil } from 'lodash'; 11 | import { DataSource, Not, ObjectType } from 'typeorm'; 12 | 13 | type Condition = { 14 | entity: ObjectType; 15 | /** 16 | * 默认忽略字段为id 17 | */ 18 | ignore?: string; 19 | /** 20 | * 如果没有指定字段则使用当前验证的属性作为查询依据 21 | */ 22 | property?: string; 23 | }; 24 | 25 | /** 26 | * 在更新时验证唯一性,通过指定ignore忽略忽略的字段 27 | */ 28 | @ValidatorConstraint({ name: 'dataUniqueExist', async: true }) 29 | @Injectable() 30 | export class UniqueExistContraint implements ValidatorConstraintInterface { 31 | constructor(private dataSource: DataSource) {} 32 | 33 | async validate(value: any, args: ValidationArguments) { 34 | const config: Omit = { 35 | ignore: 'id', 36 | property: args.property, 37 | }; 38 | const condition = ('entity' in args.constraints[0] 39 | ? merge(config, args.constraints[0]) 40 | : { 41 | ...config, 42 | entity: args.constraints[0], 43 | }) as unknown as Required; 44 | if (!condition.entity) return false; 45 | // 在传入的dto数据中获取需要忽略的字段的值 46 | const ignoreValue = (args.object as any)[condition.ignore]; 47 | // 如果忽略字段不存在则验证失败 48 | if (ignoreValue === undefined) return false; 49 | // 通过entity获取repository 50 | const repo = this.dataSource.getRepository(condition.entity); 51 | // 查询忽略字段之外的数据是否对queryProperty的值唯一 52 | return isNil( 53 | await repo.findOne({ 54 | where: { 55 | [condition.property]: value, 56 | [condition.ignore]: Not(ignoreValue), 57 | }, 58 | withDeleted: true, 59 | }), 60 | ); 61 | } 62 | 63 | defaultMessage(args: ValidationArguments) { 64 | const { entity, property } = args.constraints[0]; 65 | const queryProperty = property ?? args.property; 66 | if (!(args.object as any).getManager) { 67 | return 'getManager function not been found!'; 68 | } 69 | if (!entity) { 70 | return 'Model not been specified!'; 71 | } 72 | return `${queryProperty} of ${entity.name} must been unique!`; 73 | } 74 | } 75 | 76 | /** 77 | * 更新数据时的唯一性验证 78 | * @param params Entity类或验证条件对象 79 | * @param validationOptions 80 | */ 81 | export function IsUniqueExist( 82 | params: ObjectType | Condition, 83 | validationOptions?: ValidationOptions, 84 | ) { 85 | return (object: Record, propertyName: string) => { 86 | registerDecorator({ 87 | target: object.constructor, 88 | propertyName, 89 | options: validationOptions, 90 | constraints: [params], 91 | validator: UniqueExistContraint, 92 | }); 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/modules/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module, Provider, Type } from '@nestjs/common'; 2 | import { getDataSourceToken, TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; 3 | import { DataSource, ObjectType } from 'typeorm'; 4 | 5 | import { 6 | DataExistConstraint, 7 | UniqueConstraint, 8 | UniqueExistContraint, 9 | UniqueTreeConstraint, 10 | UniqueTreeExistConstraint, 11 | } from '@/modules/database/constraints'; 12 | 13 | import { CUSTOM_REPOSITORY_METADATA } from './constants'; 14 | 15 | @Module({}) 16 | export class DatabaseModule { 17 | static forRoot(configRegister: () => TypeOrmModuleOptions): DynamicModule { 18 | return { 19 | global: true, 20 | module: DatabaseModule, 21 | imports: [TypeOrmModule.forRoot(configRegister())], 22 | providers: [ 23 | DataExistConstraint, 24 | UniqueConstraint, 25 | UniqueExistContraint, 26 | UniqueTreeConstraint, 27 | UniqueTreeExistConstraint, 28 | ], 29 | }; 30 | } 31 | 32 | /** 33 | * 注册自定义Repository 34 | * @param repositories 需要注册的自定义类列表 35 | * @param dataSourceName 数据池名称,默认为默认连接 36 | */ 37 | static forRepository>( 38 | repositories: T[], 39 | dataSourceName?: string, 40 | ): DynamicModule { 41 | const providers: Provider[] = []; 42 | 43 | for (const Repo of repositories) { 44 | const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repo); 45 | 46 | if (!entity) { 47 | continue; 48 | } 49 | 50 | providers.push({ 51 | inject: [getDataSourceToken(dataSourceName)], 52 | provide: Repo, 53 | useFactory: (dataSource: DataSource): InstanceType => { 54 | const base = dataSource.getRepository>(entity); 55 | return new Repo(base.target, base.manager, base.queryRunner); 56 | }, 57 | }); 58 | } 59 | 60 | return { 61 | exports: providers, 62 | module: DatabaseModule, 63 | providers, 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/modules/database/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './repository.decorator'; 2 | -------------------------------------------------------------------------------- /src/modules/database/decorators/repository.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { ObjectType } from 'typeorm'; 3 | 4 | import { CUSTOM_REPOSITORY_METADATA } from '../constants'; 5 | 6 | /** 7 | * 自定义Repository 8 | * @param entity 关联的模型 9 | */ 10 | export const CustomRepository = (entity: ObjectType): ClassDecorator => 11 | SetMetadata(CUSTOM_REPOSITORY_METADATA, entity); 12 | -------------------------------------------------------------------------------- /src/modules/database/helpers.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from 'lodash'; 2 | import { 3 | DataSource, 4 | EntityManager, 5 | ObjectLiteral, 6 | ObjectType, 7 | Repository, 8 | SelectQueryBuilder, 9 | } from 'typeorm'; 10 | 11 | import { CUSTOM_REPOSITORY_METADATA } from '@/modules/database/constants'; 12 | 13 | import { OrderQueryType, PaginateOptions, PaginateReturn } from './types'; 14 | 15 | /** 16 | * 分页函数 17 | * @param queryBuilder 18 | * @param options 分页选项 19 | */ 20 | export const paginate = async ( 21 | queryBuilder: SelectQueryBuilder, 22 | options: PaginateOptions, 23 | ): Promise> => { 24 | // 查询数据总条数 25 | const totalItems = await queryBuilder.getCount(); 26 | // 调用分页处理的公共函数 27 | return extractedPaginate(options, queryBuilder, totalItems); 28 | }; 29 | 30 | /** 31 | * 按类型分组的分页函数 32 | * @param manager Entity管理器,用于写原生SQL语句 33 | * @param queryBuilder 34 | * @param options 分页选项 35 | */ 36 | export const paginateType = async ( 37 | manager: EntityManager, 38 | queryBuilder: SelectQueryBuilder, 39 | options: PaginateOptions, 40 | ): Promise> => { 41 | // 查询数据总条数(因为「select count(1) from (select * from) t」语句在TypeORM中不好弄,所以改成了拼接的方式) 42 | const newVar: [{ count: number }] = await manager.query( 43 | `select count(1) as count from (${queryBuilder.getQuery()})t`, 44 | ); 45 | const totalItems = newVar[0].count; 46 | // 调用分页处理的公共函数 47 | return extractedPaginate(options, queryBuilder, totalItems); 48 | }; 49 | 50 | /** 51 | * 抽取分页处理的公共代码 52 | */ 53 | async function extractedPaginate( 54 | options: PaginateOptions, 55 | queryBuilder: SelectQueryBuilder, 56 | totalItems: number, 57 | ) { 58 | // 计算take和skip的值,并查询分页数据 59 | const start = options.page > 0 ? options.page - 1 : 0; 60 | queryBuilder.take(options.limit).skip(start * options.limit); 61 | const items = await queryBuilder.getMany(); 62 | // 计算总页数 63 | const totalPages = Math.ceil(totalItems / options.limit); 64 | // 计算当前页项目数量 65 | const remainder = totalItems % options.limit !== 0 ? totalItems % options.limit : options.limit; 66 | const itemCount = options.page < totalPages ? options.limit : remainder; 67 | return { 68 | items, 69 | meta: { 70 | totalItems, 71 | itemCount, 72 | perPage: options.limit, 73 | totalPages, 74 | currentPage: options.page, 75 | }, 76 | }; 77 | } 78 | 79 | /** 80 | * 为查询添加排序,默认排序规则为DESC 81 | * @param qb 原查询 82 | * @param alias 别名 83 | * @param orderBy 查询排序 84 | */ 85 | export const getOrderByQuery = ( 86 | qb: SelectQueryBuilder, 87 | alias: string, 88 | orderBy?: OrderQueryType, 89 | ) => { 90 | if (isNil(orderBy)) return qb; 91 | if (typeof orderBy === 'string') return qb.orderBy(`${alias}.${orderBy}`, 'DESC'); 92 | if (Array.isArray(orderBy)) { 93 | const i = 0; 94 | for (const item of orderBy) { 95 | if (i === 0) { 96 | typeof item === 'string' 97 | ? qb.orderBy(`${alias}.${item}`, 'DESC') 98 | : qb.orderBy(`${alias}.${item}`, item.order); 99 | } else { 100 | typeof item === 'string' 101 | ? qb.addOrderBy(`${alias}.${item}`, 'DESC') 102 | : qb.addOrderBy(`${alias}.${item}`, item.order); 103 | } 104 | } 105 | return qb; 106 | } 107 | return qb.orderBy(`${alias}.${(orderBy as any).name}`, (orderBy as any).order); 108 | }; 109 | 110 | /** 111 | * 获取自定义Repository的实例 112 | * @param dataSource 数据连接池 113 | * @param Repo repository类 114 | */ 115 | export const getCustomRepository = , E extends ObjectLiteral>( 116 | dataSource: DataSource, 117 | Repo: ClassType, 118 | ): T => { 119 | if (isNil(Repo)) return null; 120 | const entity = Reflect.getMetadata(CUSTOM_REPOSITORY_METADATA, Repo); 121 | if (!entity) return null; 122 | const base = dataSource.getRepository>(entity); 123 | return new Repo(base.target, base.manager, base.queryRunner) as T; 124 | }; 125 | 126 | /** 127 | * 数据手动分页函数 128 | * @param options 分页选项 129 | * @param data 数据列表 130 | */ 131 | export function manualPaginate( 132 | options: PaginateOptions, 133 | data: E[], 134 | ): PaginateReturn { 135 | const { page, limit } = options; 136 | let items: E[] = []; 137 | const totalItems = data.length; 138 | const totalRst = totalItems / limit; 139 | const totalPages = 140 | totalRst > Math.floor(totalRst) ? Math.floor(totalRst) + 1 : Math.floor(totalRst); 141 | let itemCount = 0; 142 | if (page <= totalPages) { 143 | itemCount = page === totalPages ? totalItems - (totalPages - 1) * limit : limit; 144 | const start = (page - 1) * limit; 145 | items = data.slice(start, start + itemCount); 146 | } 147 | return { 148 | meta: { 149 | itemCount, 150 | totalItems, 151 | perPage: limit, 152 | totalPages, 153 | currentPage: page, 154 | }, 155 | items, 156 | }; 157 | } 158 | -------------------------------------------------------------------------------- /src/modules/database/types.ts: -------------------------------------------------------------------------------- 1 | import { SelectQueryBuilder, ObjectLiteral, FindTreeOptions, Repository } from 'typeorm'; 2 | 3 | import { BaseRepository } from '@/modules/database/base'; 4 | import { OrderType, SelectTrashMode } from '@/modules/database/constants'; 5 | 6 | /** 7 | * 分页原数据 8 | */ 9 | export interface PaginateMeta { 10 | /** 11 | * 当前页项目数量 12 | */ 13 | itemCount: number; 14 | /** 15 | * 项目总数量 16 | */ 17 | totalItems?: number; 18 | /** 19 | * 每页显示数量 20 | */ 21 | perPage: number; 22 | /** 23 | * 总页数 24 | */ 25 | totalPages?: number; 26 | /** 27 | * 当前页数 28 | */ 29 | currentPage: number; 30 | } 31 | /** 32 | * 分页选项 33 | */ 34 | export interface PaginateOptions { 35 | /** 36 | * 当前页数 37 | */ 38 | page: number; 39 | /** 40 | * 每页显示数量 41 | */ 42 | limit: number; 43 | } 44 | 45 | /** 46 | * 分页返回数据 47 | */ 48 | export interface PaginateReturn { 49 | meta: PaginateMeta; 50 | items: E[]; 51 | } 52 | 53 | /** 54 | * 为queryBuilder添加查询的回调函数接口 55 | */ 56 | export type QueryHook = ( 57 | qb: SelectQueryBuilder, 58 | ) => Promise>; 59 | 60 | /** 61 | * 排序类型,{字段名称: 排序方法} 62 | * 如果多个值则传入数组即可 63 | * 排序方法不设置,默认DESC 64 | */ 65 | export type OrderQueryType = 66 | | string 67 | | { name: string; order: `${OrderType}` } 68 | | Array<{ name: string; order: `${OrderType}` } | string>; 69 | 70 | /** 71 | * 数据列表查询类型 72 | */ 73 | export interface QueryParams { 74 | addQuery?: QueryHook; 75 | orderBy?: OrderQueryType; 76 | withTrashed?: boolean; 77 | onlyTrashed?: boolean; 78 | } 79 | 80 | /** 81 | * 服务类数据列表查询类型 82 | */ 83 | export type ServiceListQueryOption = 84 | | ServiceListQueryOptionWithTrashed 85 | | ServiceListQueryOptionNotWithTrashed; 86 | 87 | /** 88 | * 带有软删除的服务类数据列表查询类型 89 | */ 90 | type ServiceListQueryOptionWithTrashed = Omit< 91 | FindTreeOptions & QueryParams, 92 | 'withTrashed' 93 | > & { 94 | trashed?: `${SelectTrashMode}`; 95 | } & Record; 96 | 97 | /** 98 | * 不带软删除的服务类数据列表查询类型 99 | */ 100 | type ServiceListQueryOptionNotWithTrashed = Omit< 101 | ServiceListQueryOptionWithTrashed, 102 | 'trashed' 103 | >; 104 | 105 | /** 106 | * Repository类型 107 | */ 108 | export type RepositoryType = Repository | BaseRepository; 109 | 110 | /** 111 | * 软删除选项 112 | */ 113 | export interface TrashedOptions { 114 | trashed?: SelectTrashMode; 115 | } 116 | -------------------------------------------------------------------------------- /src/modules/org/constants.ts: -------------------------------------------------------------------------------- 1 | export const AUTHORITY_TYPE_MENU = 'MENU'; 2 | export const AUTHORITY_TYPE_RESOURCE = 'RESOURCE'; 3 | export const LIST_MAX_LIMIT = 10000; 4 | -------------------------------------------------------------------------------- /src/modules/org/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './org.controller'; 2 | export * from './station.controller'; 3 | export * from './user.controller'; 4 | export * from './role.controller'; 5 | export * from './user-role.controller'; 6 | export * from './role-authority.controller'; 7 | -------------------------------------------------------------------------------- /src/modules/org/controllers/org.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateOrgDto, QueryOrgTreeDto, UpdateOrgDto } from '../dtos'; 8 | import { OrgService } from '../services'; 9 | 10 | @Crud({ 11 | id: 'org', 12 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 13 | dtos: { 14 | store: CreateOrgDto, 15 | update: UpdateOrgDto, 16 | list: QueryOrgTreeDto, 17 | }, 18 | preAuth: 'org:org:', 19 | }) 20 | @Controller('org') 21 | @RequireLogin() 22 | export class OrgController extends BaseController { 23 | constructor(protected service: OrgService) { 24 | super(service); 25 | } 26 | 27 | @Get('tree') 28 | @RequireAuthority('org:org:tree') 29 | tree(@Query() options: QueryOrgTreeDto) { 30 | return this.service.findTrees(options); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/org/controllers/role-authority.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { AUTHORITY_TYPE_MENU } from '../constants'; 8 | import { CreateRoleAuthorityDto, QueryRoleAuthorityDto } from '../dtos'; 9 | 10 | import { RoleAuthorityService } from '../services'; 11 | 12 | @Crud({ 13 | id: 'role', 14 | enabled: ['list', 'detail', 'store', 'delete', 'restore'], 15 | dtos: { 16 | store: CreateRoleAuthorityDto, 17 | list: QueryRoleAuthorityDto, 18 | }, 19 | preAuth: 'org:roleAuthority:', 20 | }) 21 | @Controller('role-authority') 22 | @RequireLogin() 23 | export class RoleAuthorityController extends BaseController { 24 | constructor(protected service: RoleAuthorityService) { 25 | super(service); 26 | } 27 | 28 | @Get('listRoleAuthorityIdByRoleId') 29 | @RequireAuthority('org:roleAuthority:listRoleAuthorityIdByRoleId') 30 | listRoleAuthorityIdByRoleId(@Query() options: QueryRoleAuthorityDto) { 31 | return this.service.listRoleAuthorityIdByRoleId(options); 32 | } 33 | 34 | @Post('listRoleMenuByRoleIds') 35 | @RequireAuthority('org:roleAuthority:listRoleMenuByRoleIds') 36 | listRoleMenuByRoleIds(@Body() options: QueryRoleAuthorityDto) { 37 | options.authorityType = AUTHORITY_TYPE_MENU; 38 | return this.service.listRoleAuthorityByRoleIds(options); 39 | } 40 | 41 | @Post('saveBatchRoleAutority') 42 | @RequireAuthority('org:roleAuthority:saveBatchRoleAutority') 43 | saveBatchRoleAutority(@Body() options: CreateRoleAuthorityDto) { 44 | return this.service.saveBatchRoleAutority(options); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/org/controllers/role.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateRoleDto, QueryRoleDto, UpdateRoleDto } from '../dtos'; 8 | 9 | import { RoleService } from '../services'; 10 | 11 | @Crud({ 12 | id: 'role', 13 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 14 | dtos: { 15 | store: CreateRoleDto, 16 | update: UpdateRoleDto, 17 | list: QueryRoleDto, 18 | }, 19 | preAuth: 'org:role:', 20 | }) 21 | @Controller('role') 22 | @RequireLogin() 23 | export class RoleController extends BaseController { 24 | constructor(protected service: RoleService) { 25 | super(service); 26 | } 27 | 28 | @Get('listRelate') 29 | @RequireAuthority('org:role:listRelate') 30 | listRelate(@Query() options: QueryRoleDto) { 31 | return this.service.listRelate(options); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/org/controllers/station.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Query, 6 | Req, 7 | Res, 8 | UploadedFile, 9 | UseInterceptors, 10 | } from '@nestjs/common'; 11 | import { Request } from 'express'; 12 | import { FastifyReply } from 'fastify'; 13 | 14 | import { diskStorage } from 'multer'; 15 | 16 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 17 | import { BaseController } from '@/modules/restful/base'; 18 | import { UPLOAD_FOLDER } from '@/modules/restful/constants'; 19 | import { Crud } from '@/modules/restful/decorators'; 20 | import { FastifyFileInterceptor } from '@/modules/restful/upload/fastify-file-interceptor'; 21 | 22 | import { fileMapper } from '@/modules/restful/upload/file-mappter'; 23 | import { editFileName } from '@/modules/restful/upload/file-upload-util'; 24 | 25 | import { CreateStationDto, QueryStationDto, UpdateStationDto } from '../dtos'; 26 | 27 | import { StationService } from '../services'; 28 | 29 | @Crud({ 30 | id: 'station', 31 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 32 | dtos: { 33 | store: CreateStationDto, 34 | update: UpdateStationDto, 35 | list: QueryStationDto, 36 | }, 37 | preAuth: 'org:station:', 38 | }) 39 | @Controller('station') 40 | @RequireLogin() 41 | export class StationController extends BaseController { 42 | constructor(protected service: StationService) { 43 | super(service); 44 | } 45 | 46 | @Get('listRelate') 47 | @RequireAuthority('org:station:listRelate') 48 | listRelate(@Query() options: QueryStationDto) { 49 | return this.service.listRelate(options); 50 | } 51 | 52 | @Get('exportExcel') 53 | @RequireAuthority('org:station:exportExcel') 54 | exportExcel(@Query() options: QueryStationDto, @Res() response: FastifyReply) { 55 | return this.service.exportExcel(options, response); 56 | } 57 | 58 | @Post('importExcel') 59 | @RequireAuthority('org:station:importExcel') 60 | @UseInterceptors( 61 | FastifyFileInterceptor('file', { 62 | storage: diskStorage({ 63 | destination: UPLOAD_FOLDER, 64 | filename: editFileName, 65 | }), 66 | }), 67 | ) 68 | importExcel(@UploadedFile() file: Express.Multer.File, @Req() req: Request) { 69 | this.service.importExcel(file); 70 | return { file: fileMapper({ file, req }) }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/modules/org/controllers/user-role.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateUserRoleDto, QueryUserRoleDto, UpdateUserRoleDto } from '../dtos'; 8 | 9 | import { UserRoleService } from '../services'; 10 | 11 | @Crud({ 12 | id: 'user-role', 13 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 14 | dtos: { 15 | store: CreateUserRoleDto, 16 | update: UpdateUserRoleDto, 17 | list: QueryUserRoleDto, 18 | }, 19 | preAuth: 'org:userRole:', 20 | }) 21 | @Controller('user-role') 22 | @RequireLogin() 23 | export class UserRoleController extends BaseController { 24 | constructor(protected service: UserRoleService) { 25 | super(service); 26 | } 27 | 28 | @Get('listUserRoleByRoleId') 29 | @RequireAuthority('org:userRole:listUserRoleByRoleId') 30 | listUserRoleByRoleId(@Query() options: QueryUserRoleDto) { 31 | return this.service.listUserRoleByRoleId(options); 32 | } 33 | 34 | @Post('saveListAfterDelete') 35 | @RequireAuthority('org:userRole:saveListAfterDelete') 36 | saveListAfterDelete(@Body() options: CreateUserRoleDto) { 37 | return this.service.saveListAfterDelete(options); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/org/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateUserDto, LoginUserDto, QueryUserDto, UpdateUserDto } from '../dtos'; 8 | 9 | import { UserService } from '../services'; 10 | 11 | @Crud({ 12 | id: 'user', 13 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 14 | dtos: { 15 | store: CreateUserDto, 16 | update: UpdateUserDto, 17 | list: QueryUserDto, 18 | }, 19 | preAuth: 'org:user:', 20 | }) 21 | @Controller('user') 22 | @RequireLogin() 23 | export class UserController extends BaseController { 24 | constructor(protected service: UserService) { 25 | super(service); 26 | } 27 | 28 | @Get('listRelate') 29 | @RequireAuthority('org:user:listRelate') 30 | async listRelate(@Query() options: QueryUserDto) { 31 | return this.service.listRelate(options); 32 | } 33 | 34 | @Get('getUserSigninAfterInfo') 35 | @RequireAuthority('org:user:getUserSigninAfterInfo') 36 | async getUserSigninAfterInfo(@Query() options: LoginUserDto) { 37 | return this.service.getUserSigninAfterInfo(options); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/org/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './org.dto'; 2 | export * from './station.dto'; 3 | export * from './user.dto'; 4 | export * from './role.dto'; 5 | export * from './user-role.dto'; 6 | export * from './role-authority.dto'; 7 | -------------------------------------------------------------------------------- /src/modules/org/dtos/org.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { 4 | IsDefined, 5 | IsEnum, 6 | IsNotEmpty, 7 | IsNumber, 8 | IsNumberString, 9 | IsOptional, 10 | Min, 11 | ValidateIf, 12 | } from 'class-validator'; 13 | 14 | import { toNumber } from 'lodash'; 15 | 16 | import { DtoValidation } from '@/modules/core/decorators'; 17 | import { SelectTrashMode } from '@/modules/database/constants'; 18 | import { IsDataExist, IsTreeUnique, IsTreeUniqueExist } from '@/modules/database/constraints'; 19 | 20 | import { ListQueryDto } from '@/modules/restful/dtos'; 21 | 22 | import { OrgEntity } from '../entities'; 23 | 24 | /** 25 | * 机构分页查询验证 26 | */ 27 | @DtoValidation({ type: 'query' }) 28 | export class QueryOrgTreeDto extends ListQueryDto { 29 | @IsEnum(SelectTrashMode) 30 | @IsOptional() 31 | trashed?: SelectTrashMode; 32 | } 33 | 34 | /** 35 | * 机构创建验证 36 | */ 37 | @DtoValidation({ groups: ['create'] }) 38 | export class CreateOrgDto { 39 | @IsTreeUnique(OrgEntity, { 40 | groups: ['create'], 41 | message: '该机构/部门已经存在', 42 | }) 43 | @IsTreeUniqueExist(OrgEntity, { 44 | groups: ['update'], 45 | message: '该机构/部门已经存在', 46 | }) 47 | @IsNotEmpty({ groups: ['create'], message: '机构/部门名称不能为空' }) 48 | @IsOptional({ groups: ['update'] }) 49 | label!: string; 50 | 51 | @IsDataExist(OrgEntity, { always: true, message: '父机构/部门不存在' }) 52 | @IsNumberString(undefined, { always: true, message: '父机构/部门ID格式不正确' }) 53 | @ValidateIf((value) => value.parent !== null && value.parent) 54 | @IsOptional({ always: true }) 55 | parent?: string; 56 | 57 | @Transform(({ value }) => toNumber(value)) 58 | @Min(1, { always: true, message: '排序值必须大于1' }) 59 | @IsNumber(undefined, { always: true }) 60 | @IsOptional({ always: true }) 61 | sortValue = 1; 62 | 63 | @IsOptional() 64 | abbreviation: string | null; 65 | 66 | @IsOptional() 67 | type: string | null; 68 | 69 | @IsOptional() 70 | describe: string | null; 71 | 72 | @IsOptional() 73 | state: boolean | null; 74 | } 75 | 76 | /** 77 | * 机构更新验证 78 | */ 79 | @DtoValidation({ groups: ['update'] }) 80 | export class UpdateOrgDto extends PartialType(CreateOrgDto) { 81 | @IsNumberString(undefined, { groups: ['update'], message: '机构ID格式错误' }) 82 | @IsDefined({ groups: ['update'], message: '机构ID必须指定' }) 83 | id!: string; 84 | } 85 | -------------------------------------------------------------------------------- /src/modules/org/dtos/role-authority.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum, IsNotEmpty, IsOptional } from 'class-validator'; 2 | 3 | import { DtoValidation } from '@/modules/core/decorators'; 4 | 5 | import { ListWithTrashedQueryDto } from '@/modules/restful/dtos'; 6 | import { PublicOrderType } from '@/modules/system/constants'; 7 | 8 | /** 9 | * 分页查询验证 10 | */ 11 | @DtoValidation({ type: 'query' }) 12 | export class QueryRoleAuthorityDto extends ListWithTrashedQueryDto { 13 | @IsEnum(PublicOrderType, { 14 | message: `排序规则必须是${Object.values(PublicOrderType).join(',')}其中一项`, 15 | }) 16 | @IsOptional() 17 | orderBy?: PublicOrderType; 18 | 19 | @IsOptional() 20 | roleId?: string; 21 | 22 | @IsOptional() 23 | roleIds?: string[]; 24 | 25 | @IsOptional() 26 | authorityType?: string; 27 | } 28 | 29 | /** 30 | * 创建验证 31 | */ 32 | @DtoValidation({ groups: ['create'] }) 33 | export class CreateRoleAuthorityDto { 34 | @IsOptional() 35 | @IsNotEmpty({ groups: ['create', 'update'], message: '角色ID不能为空' }) 36 | roleId?: string; 37 | 38 | @IsOptional() 39 | menuIdList?: string[]; 40 | 41 | @IsOptional() 42 | resourceIdList?: string[]; 43 | 44 | authorityId!: string; 45 | 46 | authorityType!: string; 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/org/dtos/role.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { IsDefined, IsEnum, IsNotEmpty, IsNumberString, IsOptional } from 'class-validator'; 4 | 5 | import { DtoValidation } from '@/modules/core/decorators'; 6 | 7 | import { IsUnique, IsUniqueExist } from '@/modules/database/constraints'; 8 | import { RoleEntity } from '@/modules/org/entities'; 9 | import { ListWithTrashedQueryDto } from '@/modules/restful/dtos'; 10 | import { PublicOrderType } from '@/modules/system/constants'; 11 | 12 | /** 13 | * 角色分页查询验证 14 | */ 15 | @DtoValidation({ type: 'query' }) 16 | export class QueryRoleDto extends ListWithTrashedQueryDto { 17 | @IsEnum(PublicOrderType, { 18 | message: `排序规则必须是${Object.values(PublicOrderType).join(',')}其中一项`, 19 | }) 20 | @IsOptional() 21 | orderBy?: PublicOrderType; 22 | 23 | @IsOptional() 24 | name?: string; 25 | 26 | @IsOptional() 27 | code?: string; 28 | 29 | @Transform(({ value }) => value.split(',')) 30 | @IsOptional() 31 | timeRange?: string[]; 32 | } 33 | 34 | /** 35 | * 角色创建验证 36 | */ 37 | @DtoValidation({ groups: ['create'] }) 38 | export class CreateRoleDto { 39 | @IsUnique(RoleEntity, { 40 | groups: ['create'], 41 | message: '角色编码重复', 42 | }) 43 | @IsUniqueExist(RoleEntity, { 44 | groups: ['update'], 45 | message: '角色编码重复', 46 | }) 47 | @IsNotEmpty({ groups: ['create', 'update'], message: '角色编码不能为空' }) 48 | code!: string; 49 | 50 | @IsNotEmpty({ groups: ['create', 'update'], message: '角色名称不能为空' }) 51 | name!: string; 52 | } 53 | 54 | /** 55 | * 角色更新验证 56 | */ 57 | @DtoValidation({ groups: ['update'] }) 58 | export class UpdateRoleDto extends PartialType(CreateRoleDto) { 59 | @IsNumberString(undefined, { groups: ['update'], message: '角色ID格式错误' }) 60 | @IsDefined({ groups: ['update'], message: '角色ID必须指定' }) 61 | id!: string; 62 | } 63 | 64 | /** 65 | * 翻译成字典值的DTO 66 | */ 67 | export class RoleEchoDto { 68 | category: string; 69 | } 70 | -------------------------------------------------------------------------------- /src/modules/org/dtos/station.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { IsDefined, IsEnum, IsNotEmpty, IsNumberString, IsOptional } from 'class-validator'; 4 | 5 | import { DtoValidation } from '@/modules/core/decorators'; 6 | 7 | import { IsUnique, IsUniqueExist } from '@/modules/database/constraints'; 8 | import { StationEntity } from '@/modules/org/entities'; 9 | import { ListWithTrashedQueryDto } from '@/modules/restful/dtos'; 10 | import { PublicOrderType } from '@/modules/system/constants'; 11 | 12 | /** 13 | * 岗位分页查询验证 14 | */ 15 | @DtoValidation({ type: 'query' }) 16 | export class QueryStationDto extends ListWithTrashedQueryDto { 17 | @IsEnum(PublicOrderType, { 18 | message: `排序规则必须是${Object.values(PublicOrderType).join(',')}其中一项`, 19 | }) 20 | @IsOptional() 21 | orderBy?: PublicOrderType; 22 | 23 | @IsOptional() 24 | name?: string; 25 | 26 | @IsOptional() 27 | orgId?: string; 28 | 29 | @Transform(({ value }) => value.split(',')) 30 | @IsOptional() 31 | timeRange?: string[]; 32 | } 33 | 34 | /** 35 | * 岗位创建验证 36 | */ 37 | @DtoValidation({ groups: ['create'] }) 38 | export class CreateStationDto { 39 | @IsUnique(StationEntity, { 40 | groups: ['create'], 41 | message: '岗位名称重复', 42 | }) 43 | @IsUniqueExist(StationEntity, { 44 | groups: ['update'], 45 | message: '岗位名称重复', 46 | }) 47 | @IsNotEmpty({ groups: ['create'], message: '岗位名称不能为空' }) 48 | @IsOptional({ groups: ['update'] }) 49 | name!: string; 50 | 51 | @IsOptional() 52 | orgId?: string; 53 | 54 | @IsOptional() 55 | state?: boolean; 56 | 57 | @IsOptional() 58 | describe?: string; 59 | } 60 | 61 | /** 62 | * 岗位更新验证 63 | */ 64 | @DtoValidation({ groups: ['update'] }) 65 | export class UpdateStationDto extends PartialType(CreateStationDto) { 66 | @IsNumberString(undefined, { groups: ['update'], message: '岗位ID格式错误' }) 67 | @IsDefined({ groups: ['update'], message: '岗位ID必须指定' }) 68 | id!: string; 69 | } 70 | -------------------------------------------------------------------------------- /src/modules/org/dtos/user-role.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { IsDefined, IsEnum, IsNotEmpty, IsNumberString, IsOptional } from 'class-validator'; 3 | 4 | import { DtoValidation } from '@/modules/core/decorators'; 5 | 6 | import { ListWithTrashedQueryDto } from '@/modules/restful/dtos'; 7 | import { PublicOrderType } from '@/modules/system/constants'; 8 | 9 | /** 10 | * 分页查询验证 11 | */ 12 | @DtoValidation({ type: 'query' }) 13 | export class QueryUserRoleDto extends ListWithTrashedQueryDto { 14 | @IsEnum(PublicOrderType, { 15 | message: `排序规则必须是${Object.values(PublicOrderType).join(',')}其中一项`, 16 | }) 17 | @IsOptional() 18 | orderBy?: PublicOrderType; 19 | 20 | @IsOptional() 21 | roleId?: string; 22 | } 23 | 24 | /** 25 | * 创建验证 26 | */ 27 | @DtoValidation({ groups: ['create'] }) 28 | export class CreateUserRoleDto { 29 | @IsOptional() 30 | id?: string; 31 | 32 | @IsNotEmpty({ groups: ['create', 'update'], message: '角色ID不能为空' }) 33 | roleId!: string; 34 | 35 | @IsOptional() 36 | userId?: string; 37 | 38 | @IsOptional() 39 | userIdList?: string[]; 40 | } 41 | 42 | /** 43 | * 更新验证 44 | */ 45 | @DtoValidation({ groups: ['update'] }) 46 | export class UpdateUserRoleDto extends PartialType(CreateUserRoleDto) { 47 | @IsNumberString(undefined, { groups: ['update'], message: '用户角色关联ID格式错误' }) 48 | @IsDefined({ groups: ['update'], message: '用户角色关联ID必须指定' }) 49 | id!: string; 50 | } 51 | -------------------------------------------------------------------------------- /src/modules/org/dtos/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { IsDefined, IsEnum, IsNotEmpty, IsNumberString, IsOptional } from 'class-validator'; 4 | 5 | import { DtoValidation } from '@/modules/core/decorators'; 6 | 7 | import { ListWithTrashedQueryDto } from '@/modules/restful/dtos'; 8 | import { PublicOrderType } from '@/modules/system/constants'; 9 | 10 | /** 11 | * 用户分页查询验证 12 | */ 13 | @DtoValidation({ type: 'query' }) 14 | export class QueryUserDto extends ListWithTrashedQueryDto { 15 | @IsEnum(PublicOrderType, { 16 | message: `排序规则必须是${Object.values(PublicOrderType).join(',')}其中一项`, 17 | }) 18 | @IsOptional() 19 | orderBy?: PublicOrderType; 20 | 21 | @IsOptional() 22 | id?: string; 23 | 24 | @IsOptional() 25 | account?: string; 26 | 27 | @Transform(({ value }) => value.split(',')) 28 | nation?: string[]; 29 | 30 | @IsOptional() 31 | orgId?: string; 32 | 33 | @Transform(({ value }) => value.split(',')) 34 | @IsOptional() 35 | timeRange?: string[]; 36 | } 37 | 38 | /** 39 | * 用户创建验证 40 | */ 41 | @DtoValidation({ groups: ['create'] }) 42 | export class CreateUserDto { 43 | // 单就用户的constraint有问题,先不用了 44 | // @IsUnique(UserEntity, { 45 | // groups: ['create'], 46 | // message: '账号名称重复', 47 | // }) 48 | // @IsUniqueExist(UserEntity, { 49 | // groups: ['update'], 50 | // message: '账号名称重复', 51 | // }) 52 | @IsNotEmpty({ groups: ['create', 'update'], message: '账号名称不能为空' }) 53 | @IsOptional({ groups: ['update'] }) 54 | account!: string; 55 | 56 | @IsNotEmpty({ groups: ['create'], message: '姓名不能为空' }) 57 | name?: string; 58 | 59 | @IsOptional() 60 | state?: boolean; 61 | 62 | @IsNotEmpty({ groups: ['create'], message: '密码不能为空' }) 63 | password?: string; 64 | } 65 | 66 | /** 67 | * 用户更新验证 68 | */ 69 | @DtoValidation({ groups: ['update'] }) 70 | export class UpdateUserDto extends PartialType(CreateUserDto) { 71 | @IsNumberString(undefined, { groups: ['update'], message: '用户ID格式错误' }) 72 | @IsDefined({ groups: ['update'], message: '用户ID必须指定' }) 73 | id!: string; 74 | 75 | @IsNotEmpty({ groups: ['resetPwd'], message: '确认密码不能为空' }) 76 | @IsOptional() 77 | confirmPassword?: string; 78 | } 79 | 80 | /** 81 | * 用户登录验证 82 | */ 83 | @DtoValidation({ groups: ['login'] }) 84 | export class LoginUserDto { 85 | @IsNotEmpty({ groups: ['login'], message: '用户名不能为空' }) 86 | account?: string; 87 | 88 | @IsNotEmpty({ groups: ['login'], message: '密码不能为空' }) 89 | password?: string; 90 | 91 | @IsOptional() 92 | userId?: string; 93 | } 94 | 95 | /** 96 | * 翻译成字典值的DTO 97 | */ 98 | export class UserEchoDto { 99 | nation: string; 100 | 101 | education: string; 102 | 103 | positionStatus: string; 104 | } 105 | -------------------------------------------------------------------------------- /src/modules/org/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './org.entity'; 2 | export * from './station.entity'; 3 | export * from './user.entity'; 4 | export * from './role.entity'; 5 | export * from './user-role.entity'; 6 | export * from './role-authority.entity'; 7 | export * from './user-role-relation.entity'; 8 | -------------------------------------------------------------------------------- /src/modules/org/entities/org.entity.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | Index, 8 | Tree, 9 | TreeChildren, 10 | TreeParent, 11 | UpdateDateColumn, 12 | } from 'typeorm'; 13 | 14 | import { BoolBitTransformer } from '@/modules/core/helpers/utils'; 15 | import { BaseEntity } from '@/modules/database/base'; 16 | 17 | @Tree('materialized-path') 18 | @Index('uk_name', ['label'], { unique: true }) 19 | @Index('fu_path', ['mpath'], { fulltext: true }) 20 | @Entity('c_org') 21 | export class OrgEntity extends BaseEntity { 22 | @Type(() => OrgEntity) 23 | @TreeParent({ onDelete: 'CASCADE' }) 24 | parent: OrgEntity | null; 25 | 26 | @Type(() => OrgEntity) 27 | @TreeChildren({ cascade: true }) 28 | children: OrgEntity[]; 29 | 30 | depth = 0; 31 | 32 | @Column('varchar', { 33 | name: 'label', 34 | unique: true, 35 | comment: '名称', 36 | length: 255, 37 | }) 38 | label: string; 39 | 40 | @Column('char', { 41 | name: 'type_', 42 | nullable: true, 43 | comment: '类型: dictType = ORG_TYPE', 44 | length: 2, 45 | }) 46 | type: string | null; 47 | 48 | @Column('varchar', { 49 | name: 'abbreviation', 50 | nullable: true, 51 | comment: '简称', 52 | length: 255, 53 | }) 54 | abbreviation: string | null; 55 | 56 | @Column('varchar', { 57 | name: 'parentId', 58 | nullable: true, 59 | comment: '父ID', 60 | length: 20, 61 | }) 62 | parentId: string | null; 63 | 64 | @Column('int', { 65 | name: 'sort_value', 66 | nullable: true, 67 | comment: '排序', 68 | default: () => "'1'", 69 | }) 70 | sortValue: number | null; 71 | 72 | @Column('bit', { 73 | name: 'state', 74 | nullable: true, 75 | comment: '状态', 76 | default: () => "'b'1''", 77 | transformer: new BoolBitTransformer(), 78 | }) 79 | state: boolean | null; 80 | 81 | @Column('varchar', { 82 | name: 'describe_', 83 | nullable: true, 84 | comment: '描述', 85 | length: 255, 86 | }) 87 | describe: string | null; 88 | 89 | @DeleteDateColumn({ 90 | name: 'deleted_at', 91 | nullable: true, 92 | comment: '删除时间', 93 | }) 94 | deletedAt: Date | null; 95 | 96 | @CreateDateColumn({ 97 | name: 'created_at', 98 | nullable: true, 99 | comment: '创建时间', 100 | }) 101 | createdAt: Date | null; 102 | 103 | @Column('bigint', { 104 | name: 'created_by', 105 | nullable: true, 106 | comment: '创建人', 107 | }) 108 | createdBy: number | null; 109 | 110 | @UpdateDateColumn({ 111 | name: 'updated_at', 112 | nullable: true, 113 | comment: '修改时间', 114 | }) 115 | updatedAt: Date | null; 116 | 117 | @Column('bigint', { 118 | name: 'updated_by', 119 | nullable: true, 120 | comment: '修改人', 121 | }) 122 | updatedBy: number | null; 123 | } 124 | -------------------------------------------------------------------------------- /src/modules/org/entities/role-authority.entity.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { Column, CreateDateColumn, Entity, Index } from 'typeorm'; 3 | 4 | import { BaseEntity } from '@/modules/database/base'; 5 | import { ResourceEntity } from '@/modules/system/entities'; 6 | 7 | @Index('uk_role_authority', ['authorityId', 'authorityType', 'roleId'], { 8 | unique: true, 9 | }) 10 | @Entity('c_role_authority') 11 | export class RoleAuthorityEntity extends BaseEntity { 12 | @Type() 13 | resource: ResourceEntity; 14 | 15 | @Column('varchar', { 16 | name: 'authority_id', 17 | comment: '资源id \n#c_resource #c_menu', 18 | length: 20, 19 | }) 20 | authorityId: string; 21 | 22 | @Column('varchar', { 23 | name: 'authority_type', 24 | comment: '权限类型 \n#AuthorizeType{MENU:菜单;RESOURCE:资源;}', 25 | length: 10, 26 | }) 27 | authorityType: string; 28 | 29 | @Column('varchar', { name: 'role_id', comment: '角色id \n#c_role', length: 20 }) 30 | roleId: string; 31 | 32 | @CreateDateColumn({ 33 | name: 'created_at', 34 | nullable: true, 35 | comment: '创建时间', 36 | }) 37 | createdAt: Date | null; 38 | 39 | @Column('bigint', { name: 'created_by', nullable: true, comment: '创建人' }) 40 | createdBy: number | null; 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/org/entities/role.entity.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | Index, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | 11 | import { BoolBitTransformer } from '@/modules/core/helpers'; 12 | import { BaseEntity } from '@/modules/database/base'; 13 | 14 | import { RoleEchoDto } from '../dtos'; 15 | 16 | @Index('uk_code', ['code'], { unique: true }) 17 | @Entity('c_role') 18 | export class RoleEntity extends BaseEntity { 19 | @Type() 20 | roleEchoDto: RoleEchoDto; 21 | 22 | @Column('char', { 23 | name: 'category', 24 | nullable: true, 25 | comment: '角色类别;[10-功能角色 20-桌面角色 30-数据角色]', 26 | length: 2, 27 | }) 28 | category: string | null; 29 | 30 | @Column('varchar', { name: 'name', comment: '名称', length: 30 }) 31 | name: string; 32 | 33 | @Column('varchar', { 34 | name: 'code', 35 | nullable: true, 36 | unique: true, 37 | comment: '编码', 38 | length: 20, 39 | }) 40 | code: string | null; 41 | 42 | @Column('varchar', { 43 | name: 'describe_', 44 | nullable: true, 45 | comment: '描述', 46 | length: 100, 47 | }) 48 | describe: string | null; 49 | 50 | @Column('bit', { 51 | name: 'state', 52 | nullable: true, 53 | comment: '状态', 54 | default: () => "'b'1''", 55 | transformer: new BoolBitTransformer(), 56 | }) 57 | state: boolean | null; 58 | 59 | @Column('bit', { 60 | name: 'readonly_', 61 | nullable: true, 62 | comment: '内置角色', 63 | default: () => "'b'0''", 64 | transformer: new BoolBitTransformer(), 65 | }) 66 | readonly: boolean | null; 67 | 68 | @DeleteDateColumn({ 69 | name: 'deleted_at', 70 | nullable: true, 71 | comment: '删除时间', 72 | }) 73 | deletedAt: Date | null; 74 | 75 | @Column('bigint', { name: 'created_by', nullable: true, comment: '创建人id' }) 76 | createdBy: number | null; 77 | 78 | @CreateDateColumn({ 79 | name: 'created_at', 80 | nullable: true, 81 | comment: '创建时间', 82 | }) 83 | createdAt: Date | null; 84 | 85 | @Column('bigint', { name: 'updated_by', nullable: true, comment: '更新人id' }) 86 | updatedBy: number | null; 87 | 88 | @UpdateDateColumn({ 89 | name: 'updated_at', 90 | nullable: true, 91 | comment: '更新时间', 92 | }) 93 | updatedAt: Date | null; 94 | } 95 | -------------------------------------------------------------------------------- /src/modules/org/entities/station.entity.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | Index, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | 11 | import { BoolBitTransformer } from '@/modules/core/helpers/utils'; 12 | import { BaseEntity } from '@/modules/database/base'; 13 | import { OrgEntity } from '@/modules/org/entities/org.entity'; 14 | 15 | @Index('uk_name', ['name'], { unique: true }) 16 | @Entity('c_station') 17 | export class StationEntity extends BaseEntity { 18 | @Column('varchar', { 19 | name: 'name', 20 | unique: true, 21 | comment: '名称', 22 | length: 255, 23 | }) 24 | name: string; 25 | 26 | @Column('varchar', { 27 | name: 'org_id', 28 | nullable: true, 29 | comment: '机构', 30 | length: 20, 31 | }) 32 | orgId: string | null; 33 | 34 | @Type() 35 | orgMap: OrgEntity; 36 | 37 | @Column('bit', { 38 | name: 'state', 39 | nullable: true, 40 | comment: '状态', 41 | default: () => "'b'1''", 42 | transformer: new BoolBitTransformer(), 43 | }) 44 | state: boolean | null; 45 | 46 | @Column('varchar', { 47 | name: 'describe_', 48 | nullable: true, 49 | comment: '描述', 50 | length: 255, 51 | }) 52 | describe: string | null; 53 | 54 | @DeleteDateColumn({ 55 | name: 'deleted_at', 56 | nullable: true, 57 | comment: '删除时间', 58 | }) 59 | deletedAt: Date | null; 60 | 61 | @CreateDateColumn({ 62 | name: 'created_at', 63 | nullable: true, 64 | comment: '创建时间', 65 | }) 66 | createdAt: Date | null; 67 | 68 | @Column('bigint', { 69 | name: 'created_by', 70 | nullable: true, 71 | comment: '创建人', 72 | }) 73 | createdBy: number | null; 74 | 75 | @UpdateDateColumn({ 76 | name: 'updated_at', 77 | nullable: true, 78 | comment: '修改时间', 79 | }) 80 | updatedAt: Date | null; 81 | 82 | @Column('bigint', { 83 | name: 'updated_by', 84 | nullable: true, 85 | comment: '修改人', 86 | }) 87 | updatedBy: number | null; 88 | 89 | @Column('bigint', { 90 | name: 'created_org_id', 91 | nullable: true, 92 | comment: '创建者所属机构', 93 | }) 94 | createdOrgId: number | null; 95 | } 96 | -------------------------------------------------------------------------------- /src/modules/org/entities/user-role-relation.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'typeorm'; 2 | 3 | import { BaseEntity } from '@/modules/database/base'; 4 | 5 | import { RoleEntity } from './role.entity'; 6 | 7 | @Entity('c_user_role') 8 | export class UserRoleRelationEntity extends BaseEntity { 9 | role: RoleEntity; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/org/entities/user-role.entity.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { Column, CreateDateColumn, DeleteDateColumn, Entity, Index } from 'typeorm'; 3 | 4 | import { BaseEntity } from '@/modules/database/base'; 5 | 6 | import { RoleEntity } from './role.entity'; 7 | 8 | @Index('uk_user_role', ['roleId', 'userId'], { unique: true }) 9 | @Entity('c_user_role') 10 | export class UserRoleEntity extends BaseEntity { 11 | @Type() 12 | role: RoleEntity; 13 | 14 | @Column('varchar', { name: 'role_id', comment: '角色\n#c_role', length: 20 }) 15 | roleId: string; 16 | 17 | @Column('varchar', { name: 'user_id', comment: '用户\n#c_user', length: 20 }) 18 | userId: string; 19 | 20 | @DeleteDateColumn({ 21 | name: 'deleted_at', 22 | nullable: true, 23 | comment: '删除时间', 24 | }) 25 | deletedAt: Date | null; 26 | 27 | @Column('bigint', { name: 'created_by', nullable: true, comment: '创建人ID' }) 28 | createdBy: number | null; 29 | 30 | @CreateDateColumn({ 31 | name: 'created_at', 32 | nullable: true, 33 | comment: '创建时间', 34 | }) 35 | createdAt: Date | null; 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/org/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | Index, 8 | UpdateDateColumn, 9 | } from 'typeorm'; 10 | 11 | import { BoolBitTransformer } from '@/modules/core/helpers/utils'; 12 | import { BaseEntity } from '@/modules/database/base'; 13 | import { UserEchoDto } from '@/modules/org/dtos'; 14 | import { OrgEntity } from '@/modules/org/entities/org.entity'; 15 | import { StationEntity } from '@/modules/org/entities/station.entity'; 16 | 17 | import { RoleEntity } from './role.entity'; 18 | import { UserRoleEntity } from './user-role.entity'; 19 | 20 | @Index('uk_account', ['account'], { unique: true }) 21 | @Entity('c_user') 22 | export class UserEntity extends BaseEntity { 23 | @Type() 24 | orgMap: OrgEntity; 25 | 26 | @Type() 27 | stationMap: StationEntity; 28 | 29 | @Type() 30 | userEchoDto: UserEchoDto; 31 | 32 | @Type() 33 | userRoles: UserRoleEntity[]; 34 | 35 | @Type() 36 | roles: RoleEntity[]; 37 | 38 | @Type() 39 | permissions: any; 40 | 41 | @Column('varchar', { 42 | name: 'account', 43 | unique: true, 44 | comment: '账号', 45 | length: 30, 46 | }) 47 | account: string; 48 | 49 | @Column('varchar', { name: 'name', comment: '姓名', length: 50 }) 50 | name: string; 51 | 52 | @Column('varchar', { name: 'org_id', nullable: true, comment: '机构', length: 20 }) 53 | orgId: string | null; 54 | 55 | @Column('varchar', { name: 'station_id', nullable: true, comment: '岗位', length: 20 }) 56 | stationId: string | null; 57 | 58 | @Column('bit', { 59 | name: 'readonly', 60 | comment: '内置', 61 | default: () => "'b'0''", 62 | transformer: new BoolBitTransformer(), 63 | }) 64 | readonly: boolean; 65 | 66 | @Column('varchar', { 67 | name: 'email', 68 | nullable: true, 69 | comment: '邮箱', 70 | length: 255, 71 | }) 72 | email: string | null; 73 | 74 | @Column('varchar', { 75 | name: 'mobile', 76 | nullable: true, 77 | comment: '手机', 78 | length: 20, 79 | }) 80 | mobile: string | null; 81 | 82 | @Column('varchar', { 83 | name: 'sex', 84 | nullable: true, 85 | comment: '性别 \n#Sex{W:女;M:男;N:未知}', 86 | length: 1, 87 | default: () => "'M'", 88 | }) 89 | sex: string | null; 90 | 91 | @Column('bit', { 92 | name: 'state', 93 | nullable: true, 94 | comment: '状态', 95 | default: () => "'b'1''", 96 | transformer: new BoolBitTransformer(), 97 | }) 98 | state: boolean | null; 99 | 100 | @Column('varchar', { 101 | name: 'avatar', 102 | nullable: true, 103 | comment: '头像', 104 | length: 255, 105 | }) 106 | avatar: string | null; 107 | 108 | @Column('char', { 109 | name: 'nation', 110 | nullable: true, 111 | comment: '民族: dictType = NATION', 112 | length: 2, 113 | }) 114 | nation: string | null; 115 | 116 | @Column('char', { 117 | name: 'education', 118 | nullable: true, 119 | comment: '学历: dictType = EDUCATION', 120 | length: 2, 121 | }) 122 | education: string | null; 123 | 124 | @Column('char', { 125 | name: 'position_status', 126 | nullable: true, 127 | comment: '职位状态: dictType = POSITION_STATUS', 128 | length: 2, 129 | }) 130 | positionStatus: string | null; 131 | 132 | @Column('varchar', { 133 | name: 'work_describe', 134 | nullable: true, 135 | comment: '工作描述', 136 | length: 255, 137 | }) 138 | workDescribe: string | null; 139 | 140 | @Column('datetime', { 141 | name: 'password_error_last_time', 142 | nullable: true, 143 | comment: '最后一次输错密码时间', 144 | }) 145 | passwordErrorLastTime: Date | null; 146 | 147 | @Column('int', { 148 | name: 'password_error_num', 149 | nullable: true, 150 | comment: '密码错误次数', 151 | default: () => "'0'", 152 | }) 153 | passwordErrorNum: number | null; 154 | 155 | @Column('datetime', { 156 | name: 'password_expire_time', 157 | nullable: true, 158 | comment: '密码过期时间', 159 | }) 160 | passwordExpireTime: Date | null; 161 | 162 | @Column('varchar', { name: 'password', comment: '密码', length: 64 }) 163 | password: string; 164 | 165 | @Column('varchar', { 166 | name: 'salt', 167 | nullable: true, 168 | comment: '密码盐', 169 | length: 20, 170 | }) 171 | salt: string; 172 | 173 | @Column('datetime', { 174 | name: 'last_login_time', 175 | nullable: true, 176 | comment: '最后登录时间', 177 | }) 178 | lastLoginTime: Date | null; 179 | 180 | @DeleteDateColumn({ 181 | name: 'deleted_at', 182 | nullable: true, 183 | comment: '删除时间', 184 | }) 185 | deletedAt: Date | null; 186 | 187 | @Column('bigint', { name: 'created_by', nullable: true, comment: '创建人id' }) 188 | createdBy: number | null; 189 | 190 | @CreateDateColumn({ 191 | name: 'created_at', 192 | nullable: true, 193 | comment: '创建时间', 194 | }) 195 | createdAt: Date | null; 196 | 197 | @Column('bigint', { name: 'updated_by', nullable: true, comment: '更新人id' }) 198 | updatedBy: number | null; 199 | 200 | @UpdateDateColumn({ 201 | name: 'updated_at', 202 | nullable: true, 203 | comment: '更新时间', 204 | }) 205 | updatedAt: Date | null; 206 | 207 | @Column('bigint', { 208 | name: 'created_org_id', 209 | nullable: true, 210 | comment: '创建者所属机构', 211 | }) 212 | createdOrgId: number | null; 213 | } 214 | -------------------------------------------------------------------------------- /src/modules/org/org.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | 5 | import { AuthController } from '../auth/auth.controller'; 6 | import { AuthService } from '../auth/auth.service'; 7 | import { DatabaseModule } from '../database/database.module'; 8 | 9 | import * as systemRepositories from '../system/repositories'; 10 | import * as systemServices from '../system/services'; 11 | 12 | import * as controllers from './controllers'; 13 | import * as entities from './entities'; 14 | import * as repositories from './repositories'; 15 | import * as services from './services'; 16 | 17 | @Module({ 18 | imports: [ 19 | JwtModule.registerAsync({ 20 | async useFactory() { 21 | return { 22 | // 这里sign起作用,verify不起作用 23 | secret: process.env.JWT_SECRET, 24 | }; 25 | }, 26 | }), 27 | TypeOrmModule.forFeature(Object.values(entities)), 28 | DatabaseModule.forRepository(Object.values(repositories)), 29 | DatabaseModule.forRepository(Object.values(systemRepositories)), 30 | ], 31 | controllers: [...Object.values(controllers), AuthController], 32 | providers: [ 33 | ...Object.values(services), 34 | ...Object.values(systemServices), 35 | AuthService, 36 | // JwtStrategy, 37 | ], 38 | exports: [ 39 | ...Object.values(services), 40 | ...Object.values(systemServices), 41 | DatabaseModule.forRepository(Object.values(repositories)), 42 | DatabaseModule.forRepository(Object.values(systemRepositories)), 43 | ], 44 | }) 45 | export class OrgModule {} 46 | -------------------------------------------------------------------------------- /src/modules/org/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './org.repository'; 2 | export * from './station.repository'; 3 | export * from './user.repository'; 4 | export * from './role.repository'; 5 | export * from './user-role.repository'; 6 | export * from './role-authority.repository'; 7 | -------------------------------------------------------------------------------- /src/modules/org/repositories/org.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseTreeRepository } from '@/modules/database/base/tree.repository'; 2 | import { OrderType, TreeChildrenResolve } from '@/modules/database/constants'; 3 | import { CustomRepository } from '@/modules/database/decorators'; 4 | 5 | import { OrgEntity } from '../entities'; 6 | 7 | @CustomRepository(OrgEntity) 8 | export class OrgRepository extends BaseTreeRepository { 9 | protected _qbName = 'org'; 10 | 11 | protected orderBy = { name: 'sortValue', order: OrderType.ASC }; 12 | 13 | protected _childrenResolve = TreeChildrenResolve.UP; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/org/repositories/role-authority.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '@/modules/database/base'; 2 | import { CustomRepository } from '@/modules/database/decorators'; 3 | 4 | import { RoleAuthorityEntity } from '../entities/role-authority.entity'; 5 | 6 | @CustomRepository(RoleAuthorityEntity) 7 | export class RoleAuthorityRepository extends BaseRepository { 8 | protected _qbName = 'role_authority'; 9 | 10 | buildBaseQB() { 11 | return this.createQueryBuilder(this.qbName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/org/repositories/role.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '@/modules/database/base'; 2 | import { CustomRepository } from '@/modules/database/decorators'; 3 | 4 | import { RoleEntity } from '../entities'; 5 | 6 | @CustomRepository(RoleEntity) 7 | export class RoleRepository extends BaseRepository { 8 | protected _qbName = 'role'; 9 | 10 | buildBaseQB() { 11 | return this.createQueryBuilder(this.qbName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/org/repositories/station.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '@/modules/database/base'; 2 | import { CustomRepository } from '@/modules/database/decorators'; 3 | 4 | import { StationEntity } from '../entities'; 5 | 6 | @CustomRepository(StationEntity) 7 | export class StationRepository extends BaseRepository { 8 | protected _qbName = 'station'; 9 | 10 | buildBaseQB() { 11 | return this.createQueryBuilder(this.qbName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/org/repositories/user-role.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '@/modules/database/base'; 2 | import { CustomRepository } from '@/modules/database/decorators'; 3 | 4 | import { UserRoleEntity } from '../entities/user-role.entity'; 5 | 6 | @CustomRepository(UserRoleEntity) 7 | export class UserRoleRepository extends BaseRepository { 8 | protected _qbName = 'user_role'; 9 | 10 | buildBaseQB() { 11 | return this.createQueryBuilder(this.qbName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/org/repositories/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '@/modules/database/base'; 2 | import { CustomRepository } from '@/modules/database/decorators'; 3 | 4 | import { UserEntity } from '../entities'; 5 | 6 | @CustomRepository(UserEntity) 7 | export class UserRepository extends BaseRepository { 8 | protected _qbName = 'user'; 9 | 10 | buildBaseQB() { 11 | return this.createQueryBuilder(this.qbName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/org/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './org.service'; 2 | export * from './station.service'; 3 | export * from './user.service'; 4 | export * from './role.service'; 5 | export * from './user-role.service'; 6 | export * from './role-authority.service'; 7 | -------------------------------------------------------------------------------- /src/modules/org/services/org.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { isNil, omit } from 'lodash'; 4 | import { SelectQueryBuilder, EntityNotFoundError } from 'typeorm'; 5 | 6 | import { BaseService } from '@/modules/database/base'; 7 | 8 | import { SelectTrashMode } from '@/modules/database/constants'; 9 | import { PublicOrderType } from '@/modules/system/constants'; 10 | 11 | import { QueryOrgTreeDto, CreateOrgDto, UpdateOrgDto } from '../dtos'; 12 | import { OrgEntity } from '../entities'; 13 | import { OrgRepository } from '../repositories'; 14 | 15 | /** 16 | * 机构数据操作 17 | */ 18 | @Injectable() 19 | export class OrgService extends BaseService { 20 | protected enableTrash = false; 21 | 22 | constructor(protected repository: OrgRepository) { 23 | super(repository); 24 | } 25 | 26 | /** 27 | * 查询机构树 28 | */ 29 | async findTrees(options: QueryOrgTreeDto) { 30 | const { trashed = SelectTrashMode.NONE } = options; 31 | return this.repository.findTrees({ 32 | withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY, 33 | onlyTrashed: trashed === SelectTrashMode.ONLY, 34 | }); 35 | } 36 | 37 | /** 38 | * 新建机构 39 | * @param data 40 | */ 41 | async create(data: CreateOrgDto) { 42 | const createParams = await super.create(data); 43 | // 执行插入 44 | return this.repository.save({ 45 | ...createParams, 46 | // 传入父机构 47 | parent: await this.getParent(undefined, data.parent), 48 | }); 49 | } 50 | 51 | /** 52 | * 更新机构 53 | * @param data 54 | */ 55 | async update(data: UpdateOrgDto) { 56 | const parent = await this.getParent(data.id, data.parent); 57 | const querySet = omit(data, ['id', 'parent']); 58 | if (Object.keys(querySet).length > 0) { 59 | await this.repository.update(data.id, querySet); 60 | } 61 | const cat = await this.detail(data.id); 62 | const shouldUpdateParent = 63 | (!isNil(cat.parent) && !isNil(parent) && cat.parent.id !== parent.id) || 64 | (isNil(cat.parent) && !isNil(parent)) || 65 | (!isNil(cat.parent) && isNil(parent)); 66 | // 父分类单独更新 67 | if (parent !== undefined && shouldUpdateParent) { 68 | cat.parent = parent; 69 | await this.repository.save(cat); 70 | } 71 | return cat; 72 | } 73 | 74 | /** 75 | * 获取请求传入的父机构 76 | * @param current 当前机构的ID 77 | * @param id 78 | */ 79 | protected async getParent(current?: string, id?: string) { 80 | if (current === id) return undefined; 81 | let parent: OrgEntity | undefined; 82 | if (id !== undefined) { 83 | if (id === null) return null; 84 | parent = await this.repository.findOne({ where: { id } }); 85 | if (!parent) 86 | throw new EntityNotFoundError(OrgEntity, `Parent category ${id} not exists!`); 87 | } 88 | return parent; 89 | } 90 | 91 | /** 92 | * 对机构进行排序的Query构建 93 | * @param qb 94 | * @param orderBy 排序方式 95 | */ 96 | protected addOrderByQuery(qb: SelectQueryBuilder, orderBy?: PublicOrderType) { 97 | const queryName = this.repository.qbName; 98 | switch (orderBy) { 99 | case PublicOrderType.CREATED: 100 | return qb.orderBy(`${queryName}.created_at`, 'DESC'); 101 | case PublicOrderType.UPDATED: 102 | return qb.orderBy(`${queryName}.updated_at`, 'DESC'); 103 | default: 104 | return qb.orderBy(`${queryName}.id`, 'ASC'); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/modules/org/services/role.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { isEmpty, isNil, omit } from 'lodash'; 4 | import { SelectQueryBuilder } from 'typeorm'; 5 | 6 | import { BaseService } from '@/modules/database/base'; 7 | 8 | import { paginate } from '@/modules/database/helpers'; 9 | import { PaginateReturn, QueryHook } from '@/modules/database/types'; 10 | import { DictionaryType, PublicOrderType } from '@/modules/system/constants'; 11 | 12 | import { QueryDictionaryDto } from '@/modules/system/dtos'; 13 | 14 | import { DictionaryService } from '@/modules/system/services'; 15 | 16 | import { CreateRoleDto, QueryRoleDto, RoleEchoDto, UpdateRoleDto } from '../dtos'; 17 | import { RoleEntity } from '../entities'; 18 | import { RoleRepository } from '../repositories'; 19 | 20 | /** 21 | * 角色数据操作 22 | */ 23 | @Injectable() 24 | export class RoleService extends BaseService { 25 | protected enableTrash = true; 26 | 27 | constructor( 28 | protected repository: RoleRepository, 29 | protected dictionaryService: DictionaryService, 30 | ) { 31 | super(repository); 32 | } 33 | 34 | /** 35 | * 新建 36 | * @param data 37 | */ 38 | async create(data: CreateRoleDto) { 39 | // 获取通用参数 40 | const createParams = await super.create(data); 41 | // 执行插入 42 | return this.repository.save(createParams); 43 | } 44 | 45 | /** 46 | * 更新 47 | * @param data 48 | */ 49 | async update(data: UpdateRoleDto) { 50 | await this.repository.update(data.id, omit(data, ['id', 'code'])); 51 | return this.detail(data.id); 52 | } 53 | 54 | /** 55 | * 调用关联查询并分页 56 | */ 57 | async listRelate( 58 | options?: QueryRoleDto, 59 | callback?: QueryHook, 60 | ): Promise> { 61 | // 调用重写的qb处理方法 62 | const qb = await this.buildListQB(this.repository.buildBaseQB(), options, callback); 63 | // 调用分页函数,得到返回的数据 64 | const roleEntityPaginateReturn = await paginate(qb, options); 65 | const { items } = roleEntityPaginateReturn; 66 | // 查询字典列表按类型筛选 67 | const queryDictionaryDto = new QueryDictionaryDto(); 68 | queryDictionaryDto.type = `('${DictionaryType.ROLE_CATEGORY}')`; 69 | queryDictionaryDto.trashed = options.trashed; 70 | const dictionaryEntities = await this.dictionaryService.listWhereType(queryDictionaryDto); 71 | // 查询完之后挨个参数替换翻译 72 | for (const item of items) { 73 | item.roleEchoDto = new RoleEchoDto(); 74 | for (const dictionaryEntity of dictionaryEntities) { 75 | // 翻译角色种类 76 | if (dictionaryEntity.type === DictionaryType.ROLE_CATEGORY) { 77 | if (dictionaryEntity.code === item.category) { 78 | item.roleEchoDto.category = dictionaryEntity.name; 79 | } 80 | } 81 | } 82 | } 83 | return roleEntityPaginateReturn; 84 | } 85 | 86 | /** 87 | * 构建列表查询器 88 | * @param queryBuilder 初始查询构造器 89 | * @param options 排查分页选项后的查询选项 90 | * @param callback 添加额外的查询 91 | */ 92 | protected async buildListQB( 93 | queryBuilder: SelectQueryBuilder, 94 | options: QueryRoleDto, 95 | callback?: QueryHook, 96 | ) { 97 | // 调用父类通用qb处理方法 98 | const qb = await super.buildListQB(queryBuilder, options, callback); 99 | // 子类自我实现 100 | const { orderBy, name, code, timeRange } = options; 101 | const queryName = this.repository.qbName; 102 | // 对几个可选参数的where判断 103 | if (!isEmpty(name)) { 104 | qb.andWhere(`${queryName}.name like '%${name}%'`); 105 | } 106 | if (!isEmpty(code)) { 107 | qb.andWhere(`${queryName}.code like '%${code}%'`); 108 | } 109 | if (!isNil(timeRange)) { 110 | qb.andWhere(`${queryName}.created_at between ${timeRange[0]} and ${timeRange[1]}`); 111 | } 112 | // 排序 113 | this.addOrderByQuery(qb, orderBy); 114 | return qb; 115 | } 116 | 117 | /** 118 | * 进行排序的Query构建 119 | * @param qb 120 | * @param orderBy 排序方式 121 | */ 122 | protected addOrderByQuery(qb: SelectQueryBuilder, orderBy?: PublicOrderType) { 123 | const queryName = this.repository.qbName; 124 | switch (orderBy) { 125 | case PublicOrderType.CREATED: 126 | return qb.orderBy(`${queryName}.created_at`, 'DESC'); 127 | case PublicOrderType.UPDATED: 128 | return qb.orderBy(`${queryName}.updated_at`, 'DESC'); 129 | default: 130 | return qb.orderBy(`${queryName}.id`, 'ASC'); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/modules/org/services/user-role.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { isEmpty, isNil } from 'lodash'; 4 | import { SelectQueryBuilder } from 'typeorm'; 5 | 6 | import { BaseService } from '@/modules/database/base'; 7 | 8 | import { PublicOrderType } from '@/modules/system/constants'; 9 | 10 | import { CreateUserRoleDto, QueryUserRoleDto } from '../dtos'; 11 | import { UserRoleEntity } from '../entities'; 12 | import { UserRoleRepository } from '../repositories'; 13 | 14 | /** 15 | * 用户角色关联数据操作 16 | */ 17 | @Injectable() 18 | export class UserRoleService extends BaseService { 19 | protected enableTrash = false; 20 | 21 | constructor(protected repository: UserRoleRepository) { 22 | super(repository); 23 | } 24 | 25 | /** 26 | * 删除并新建 27 | */ 28 | async saveListAfterDelete(data: CreateUserRoleDto) { 29 | // 先删除角色数据 30 | if (!isNil(data.roleId)) { 31 | await this.repository.delete({ roleId: data.roleId }); 32 | } 33 | // 再处理成插入的对象集合 34 | const saveList: UserRoleEntity[] = []; 35 | if (!isEmpty(data.userIdList)) { 36 | for (const userId of data.userIdList) { 37 | data.userId = userId; 38 | saveList.push(await super.create(data)); 39 | } 40 | } 41 | // 执行插入 42 | return this.repository.save(saveList); 43 | } 44 | 45 | /** 46 | * 根据角色ID查询用户角色关联 47 | */ 48 | async listUserRoleByRoleId(options: QueryUserRoleDto): Promise { 49 | const qb = await super.buildListQB(this.repository.buildBaseQB(), options); 50 | if (!isEmpty(options.roleId)) { 51 | qb.where(`${this.repository.qbName}.role_id = ${options.roleId}`); 52 | } 53 | return qb.getMany(); 54 | } 55 | 56 | /** 57 | * 对机构进行排序的Query构建 58 | * @param qb 59 | * @param orderBy 排序方式 60 | */ 61 | protected addOrderByQuery(qb: SelectQueryBuilder, orderBy?: PublicOrderType) { 62 | const queryName = this.repository.qbName; 63 | switch (orderBy) { 64 | case PublicOrderType.CREATED: 65 | return qb.orderBy(`${queryName}.created_at`, 'DESC'); 66 | case PublicOrderType.UPDATED: 67 | return qb.orderBy(`${queryName}.updated_at`, 'DESC'); 68 | default: 69 | return qb.orderBy(`${queryName}.id`, 'ASC'); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/modules/resource/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './oss.controller'; 2 | -------------------------------------------------------------------------------- /src/modules/resource/controllers/oss.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateOssDto, QueryOssDto, UpdateOssDto } from '../dtos'; 8 | 9 | import { OssService } from '../services'; 10 | 11 | @Crud({ 12 | id: 'oss', 13 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 14 | dtos: { 15 | store: CreateOssDto, 16 | update: UpdateOssDto, 17 | list: QueryOssDto, 18 | }, 19 | preAuth: 'resource:oss:', 20 | }) 21 | @Controller('oss') 22 | @RequireLogin() 23 | export class OssController extends BaseController { 24 | constructor(protected service: OssService) { 25 | super(service); 26 | } 27 | 28 | @Get('listRelate') 29 | @RequireAuthority('resource:oss:listRelate') 30 | async listRelate(@Query() options: QueryOssDto) { 31 | return this.service.listRelate(options); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/resource/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './oss.dto'; 2 | -------------------------------------------------------------------------------- /src/modules/resource/dtos/oss.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { IsDefined, IsEnum, IsNotEmpty, IsNumberString, IsOptional } from 'class-validator'; 3 | 4 | import { DtoValidation } from '@/modules/core/decorators'; 5 | 6 | import { IsUnique, IsUniqueExist } from '@/modules/database/constraints'; 7 | import { ListWithTrashedQueryDto } from '@/modules/restful/dtos'; 8 | import { PublicOrderType } from '@/modules/system/constants'; 9 | 10 | import { OssEntity } from '../entities'; 11 | 12 | /** 13 | * 分页查询验证 14 | */ 15 | @DtoValidation({ type: 'query' }) 16 | export class QueryOssDto extends ListWithTrashedQueryDto { 17 | @IsEnum(PublicOrderType, { 18 | message: `排序规则必须是${Object.values(PublicOrderType).join(',')}其中一项`, 19 | }) 20 | @IsOptional() 21 | orderBy?: PublicOrderType; 22 | 23 | @IsOptional() 24 | category?: string; 25 | 26 | @IsOptional() 27 | code?: string; 28 | 29 | @IsOptional() 30 | accessKey?: string; 31 | } 32 | 33 | /** 34 | * 创建验证 35 | */ 36 | @DtoValidation({ groups: ['create'] }) 37 | export class CreateOssDto { 38 | @IsUnique(OssEntity, { 39 | groups: ['create'], 40 | message: '资源编码名称重复', 41 | }) 42 | @IsUniqueExist(OssEntity, { 43 | groups: ['update'], 44 | message: '资源编码重复', 45 | }) 46 | @IsNotEmpty({ groups: ['create', 'update'], message: '资源编码不能为空' }) 47 | code!: string; 48 | } 49 | 50 | /** 51 | * 更新验证 52 | */ 53 | @DtoValidation({ groups: ['update'] }) 54 | export class UpdateOssDto extends PartialType(CreateOssDto) { 55 | @IsNumberString(undefined, { groups: ['update'], message: '对象存储ID格式错误' }) 56 | @IsDefined({ groups: ['update'], message: 'ID必须指定' }) 57 | id!: string; 58 | } 59 | 60 | /** 61 | * 翻译成字典值的DTO 62 | */ 63 | export class OssEchoDto { 64 | category: string; 65 | } 66 | -------------------------------------------------------------------------------- /src/modules/resource/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './oss.entity'; 2 | -------------------------------------------------------------------------------- /src/modules/resource/entities/oss.entity.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { Column, CreateDateColumn, DeleteDateColumn, Entity, UpdateDateColumn } from 'typeorm'; 3 | 4 | import { BoolBitTransformer } from '@/modules/core/helpers/utils'; 5 | import { BaseEntity } from '@/modules/database/base'; 6 | 7 | import { OssEchoDto } from '../dtos'; 8 | 9 | @Entity('c_oss', { schema: 'prune' }) 10 | export class OssEntity extends BaseEntity { 11 | @Type() 12 | ossEchoDto: OssEchoDto; 13 | 14 | @Column('varchar', { 15 | name: 'code', 16 | nullable: true, 17 | comment: '资源编码', 18 | length: 32, 19 | }) 20 | code: string | null; 21 | 22 | @Column('varchar', { name: 'category', nullable: true, comment: '种类', length: 2 }) 23 | category: string | null; 24 | 25 | @Column('varchar', { name: 'bucket_name', nullable: true, comment: '空间名', length: 64 }) 26 | bucketName: string | null; 27 | 28 | @Column('varchar', { 29 | name: 'access_key', 30 | nullable: true, 31 | comment: 'ak', 32 | length: 128, 33 | }) 34 | accessKey: string | null; 35 | 36 | @Column('varchar', { 37 | name: 'secret_key', 38 | nullable: true, 39 | comment: 'sk', 40 | length: 255, 41 | }) 42 | secretKey: string | null; 43 | 44 | @Column('varchar', { 45 | name: 'endpoint', 46 | nullable: true, 47 | comment: '资源地址', 48 | length: 128, 49 | }) 50 | endpoint: string | null; 51 | 52 | @Column('varchar', { 53 | name: 'describe', 54 | nullable: true, 55 | comment: '描述', 56 | length: 128, 57 | }) 58 | describe: string | null; 59 | 60 | @Column('bit', { 61 | name: 'state', 62 | nullable: true, 63 | comment: '状态', 64 | default: () => "'b'1''", 65 | transformer: new BoolBitTransformer(), 66 | }) 67 | state: boolean | null; 68 | 69 | @DeleteDateColumn({ 70 | name: 'deleted_at', 71 | nullable: true, 72 | comment: '删除时间', 73 | }) 74 | deletedAt: Date | null; 75 | 76 | @CreateDateColumn({ 77 | name: 'created_at', 78 | nullable: true, 79 | comment: '创建时间', 80 | }) 81 | createdAt: Date | null; 82 | 83 | @Column('bigint', { 84 | name: 'created_by', 85 | nullable: true, 86 | comment: '创建人', 87 | }) 88 | createdBy: number | null; 89 | 90 | @UpdateDateColumn({ 91 | name: 'updated_at', 92 | nullable: true, 93 | comment: '修改时间', 94 | }) 95 | updatedAt: Date | null; 96 | 97 | @Column('bigint', { 98 | name: 'updated_by', 99 | nullable: true, 100 | comment: '修改人', 101 | }) 102 | updatedBy: number | null; 103 | } 104 | -------------------------------------------------------------------------------- /src/modules/resource/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './oss.repository'; 2 | -------------------------------------------------------------------------------- /src/modules/resource/repositories/oss.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '@/modules/database/base'; 2 | import { CustomRepository } from '@/modules/database/decorators'; 3 | 4 | import { OssEntity } from '../entities'; 5 | 6 | @CustomRepository(OssEntity) 7 | export class OssRepository extends BaseRepository { 8 | protected _qbName = 'oss'; 9 | 10 | buildBaseQB() { 11 | return this.createQueryBuilder(this.qbName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/resource/resource.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { DatabaseModule } from '../database/database.module'; 5 | 6 | import { OrgModule } from '../org/org.module'; 7 | 8 | import * as controllers from './controllers'; 9 | import * as entities from './entities'; 10 | import * as repositories from './repositories'; 11 | import * as services from './services'; 12 | 13 | @Module({ 14 | imports: [ 15 | OrgModule, 16 | TypeOrmModule.forFeature(Object.values(entities)), 17 | DatabaseModule.forRepository(Object.values(repositories)), 18 | ], 19 | controllers: Object.values(controllers), 20 | providers: [...Object.values(services)], 21 | exports: [ 22 | ...Object.values(services), 23 | DatabaseModule.forRepository(Object.values(repositories)), 24 | ], 25 | }) 26 | export class ResourceModule {} 27 | -------------------------------------------------------------------------------- /src/modules/resource/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './oss.service'; 2 | -------------------------------------------------------------------------------- /src/modules/resource/services/oss.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { isEmpty, omit } from 'lodash'; 3 | 4 | import { SelectQueryBuilder } from 'typeorm'; 5 | 6 | import { BaseService } from '@/modules/database/base'; 7 | 8 | import { paginate } from '@/modules/database/helpers'; 9 | import { PaginateReturn, QueryHook } from '@/modules/database/types'; 10 | import { DictionaryType, PublicOrderType } from '@/modules/system/constants'; 11 | 12 | import { QueryDictionaryDto } from '@/modules/system/dtos'; 13 | 14 | import { DictionaryService } from '@/modules/system/services'; 15 | 16 | import { CreateOssDto, OssEchoDto, QueryOssDto, UpdateOssDto } from '../dtos'; 17 | import { OssEntity } from '../entities'; 18 | import { OssRepository } from '../repositories'; 19 | 20 | // 用户查询接口 21 | type FindParams = { 22 | [key in keyof Omit]: QueryOssDto[key]; 23 | }; 24 | 25 | /** 26 | * 用户数据操作 27 | */ 28 | @Injectable() 29 | export class OssService extends BaseService { 30 | constructor( 31 | protected repository: OssRepository, 32 | protected dictionaryService: DictionaryService, 33 | ) { 34 | super(repository); 35 | } 36 | 37 | /** 38 | * 新建用户 39 | * @param data 40 | */ 41 | async create(data: CreateOssDto) { 42 | // 获取通用参数 43 | const createParams = await super.create(data); 44 | // 执行插入 45 | return this.repository.save(createParams); 46 | } 47 | 48 | /** 49 | * 更新用户 50 | * @param data 51 | */ 52 | async update(data: UpdateOssDto) { 53 | await this.repository.update(data.id, omit(data, ['id'])); 54 | return this.detail(data.id); 55 | } 56 | 57 | /** 58 | * 调用关联查询并分页 59 | */ 60 | async listRelate( 61 | options?: QueryOssDto, 62 | callback?: QueryHook, 63 | ): Promise> { 64 | // 调用 buildListQB 65 | const qb = await this.buildListQB(this.repository.buildBaseQB(), options, callback); 66 | // 调用分页函数,得到返回的数据 67 | const ossEntityPaginateReturn = await paginate(qb, options); 68 | const { items } = ossEntityPaginateReturn; 69 | // 查询字典列表按类型筛选 70 | const queryDictionaryDto = new QueryDictionaryDto(); 71 | queryDictionaryDto.type = `('${DictionaryType.OSSC_CATEGORY}')`; 72 | queryDictionaryDto.trashed = options.trashed; 73 | const dictionaryEntities = await this.dictionaryService.listWhereType(queryDictionaryDto); 74 | // 查询完之后挨个参数替换翻译 75 | for (const item of items) { 76 | item.ossEchoDto = new OssEchoDto(); 77 | for (const dictionaryEntity of dictionaryEntities) { 78 | // 翻译种类 79 | if (dictionaryEntity.type === DictionaryType.OSSC_CATEGORY) { 80 | if (dictionaryEntity.code === item.category) { 81 | item.ossEchoDto.category = dictionaryEntity.name; 82 | } 83 | } 84 | } 85 | } 86 | return ossEntityPaginateReturn; 87 | } 88 | 89 | /** 90 | * 构建岗位列表查询器 91 | * @param queryBuilder 初始查询构造器 92 | * @param options 排查分页选项后的查询选项 93 | * @param callback 添加额外的查询 94 | */ 95 | protected async buildListQB( 96 | queryBuilder: SelectQueryBuilder, 97 | options: FindParams, 98 | callback?: QueryHook, 99 | ) { 100 | // 调用父类通用qb处理方法 101 | const qb = await super.buildListQB(queryBuilder, options, callback); 102 | // 子类自我实现 103 | const { category, code, accessKey, orderBy } = options; 104 | const queryName = this.repository.qbName; 105 | // 对几个可选参数的where判断 106 | if (!isEmpty(category)) { 107 | qb.andWhere(`${queryName}.category = ${category}`); 108 | } 109 | if (!isEmpty(code)) { 110 | qb.andWhere(`${queryName}.code like '%${code}%'`); 111 | } 112 | if (!isEmpty(accessKey)) { 113 | qb.andWhere(`${queryName}.access_key like '%${accessKey}%'`); 114 | } 115 | // 排序 116 | this.addOrderByQuery(qb, orderBy); 117 | return qb; 118 | } 119 | 120 | /** 121 | * 对用户进行排序的Query构建 122 | * @param qb 123 | * @param orderBy 排序方式 124 | */ 125 | protected addOrderByQuery(qb: SelectQueryBuilder, orderBy?: PublicOrderType) { 126 | const queryName = this.repository.qbName; 127 | switch (orderBy) { 128 | // 按时间倒序 129 | case PublicOrderType.CREATED: 130 | return qb.orderBy(`${queryName}.created_at`, 'DESC'); 131 | case PublicOrderType.UPDATED: 132 | return qb.orderBy(`${queryName}.updated_at`, 'DESC'); 133 | // 默认按id正序 134 | default: 135 | return qb.orderBy(`${queryName}.id`, 'ASC'); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/modules/restful/base/code.constants.ts: -------------------------------------------------------------------------------- 1 | export const SUCCESS = 0; 2 | export const CODE_NOT_EXPIRE = 10001; 3 | export const UPDATE_ERROR = 10002; 4 | export const ACCOUNT_NOT_EXIST = 10003; 5 | export const CODE_NOT_EXIST = 10004; 6 | export const CODE_EXPIRE = 10005; 7 | export const LOGIN_ERROR = 10006; 8 | export const CODE_SEND_ERROR = 10007; 9 | export const ACCOUNT_EXIST = 10008; 10 | export const REGISTER_ERROR = 10009; 11 | export const NOT_EMPTY = 10010; 12 | export const VALIDATE_ERROR = 10011; 13 | export const STUDENT_NOT_EXIST = 10012; 14 | export const ORG_NOT_EXIST = 10013; 15 | export const ORG_FAIL = 10014; 16 | export const ORG_DEL_FAIL = 10015; 17 | export const COURSE_DEL_FAIL = 10016; 18 | export const COURSE_NOT_EXIST = 10017; 19 | export const COURSE_FAIL = 10018; 20 | export const COURSE_CREATE_FAIL = 10019; 21 | export const COURSE_UPDATE_FAIL = 10020; 22 | export const CARD_CREATE_FAIL = 10021; 23 | export const CARD_DEL_FAIL = 10022; 24 | export const CARD_NOT_EXIST = 10023; 25 | export const CARD_UPDATE_FAIL = 10024; 26 | export const PRODUCT_CREATE_FAIL = 10025; 27 | export const PRODUCT_DEL_FAIL = 10026; 28 | export const PRODUCT_NOT_EXIST = 10027; 29 | export const PRODUCT_UPDATE_FAIL = 10028; 30 | export const NOT_OPENID = 10029; 31 | export const SCHEDULE_CREATE_FAIL = 10030; 32 | export const ORDER_LIMIT = 10031; 33 | export const STACK_NOT_ENOUGH = 10032; 34 | export const CARD_RECORD_EXIST = 10033; 35 | export const CARD_EXPIRED = 10034; 36 | export const CARD_DEPLETE = 10035; 37 | export const SCHEDULE_NOT_EXIST = 10036; 38 | export const SUBSCRIBE_FAIL = 10037; 39 | export const SCHEDULE_HAD_SUBSCRIBE = 10038; 40 | export const SCHEDULE_RECORD_NOT_EXIST = 10039; 41 | export const CANCEL_SCHEDULE_FAIL = 10040; 42 | -------------------------------------------------------------------------------- /src/modules/restful/base/controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Param, Query } from '@nestjs/common'; 2 | 3 | import { DeleteWithTrashDto, ListWithTrashedQueryDto, RestoreDto } from '../dtos'; 4 | 5 | /** 6 | * 基础控制器 7 | */ 8 | export abstract class BaseController { 9 | protected service: S; 10 | 11 | constructor(service: S) { 12 | this.setService(service); 13 | } 14 | 15 | private setService(service: S) { 16 | this.service = service; 17 | } 18 | 19 | async list(@Query() options: ListWithTrashedQueryDto, ...args: any[]) { 20 | return (this.service as any).paginate(options); 21 | } 22 | 23 | async detail( 24 | @Param('id') 25 | id: number, 26 | ...args: any[] 27 | ) { 28 | return (this.service as any).detail(id); 29 | } 30 | 31 | async store( 32 | @Body() 33 | data: any, 34 | ...args: any[] 35 | ) { 36 | return (this.service as any).create(data); 37 | } 38 | 39 | async update( 40 | @Body() 41 | data: any, 42 | ...args: any[] 43 | ) { 44 | return (this.service as any).update(data); 45 | } 46 | 47 | async delete( 48 | @Body() 49 | { ids, trash }: DeleteWithTrashDto, 50 | ...args: any[] 51 | ) { 52 | return (this.service as any).delete(ids, trash); 53 | } 54 | 55 | async restore( 56 | @Body() 57 | { ids }: RestoreDto, 58 | ...args: any[] 59 | ) { 60 | return (this.service as any).restore(ids); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/restful/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controller'; 2 | -------------------------------------------------------------------------------- /src/modules/restful/base/result.type.ts: -------------------------------------------------------------------------------- 1 | export interface IResult { 2 | code: number; 3 | message: string; 4 | result?: T; 5 | } 6 | 7 | export interface IResults { 8 | code: number; 9 | message: string; 10 | result?: T[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/restful/constants.ts: -------------------------------------------------------------------------------- 1 | export const CRUD_OPTIONS = 'crud_options'; 2 | export const UPLOAD_FOLDER = './upload'; 3 | -------------------------------------------------------------------------------- /src/modules/restful/decorators/crud.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Get, Type, Post, Patch, Delete, SerializeOptions } from '@nestjs/common'; 2 | import { ClassTransformOptions } from 'class-transformer'; 3 | import { isNil } from 'lodash'; 4 | 5 | import { RequireAuthority } from '@/modules/auth/auth.decorator'; 6 | 7 | import { BaseController } from '../base'; 8 | 9 | import { CRUD_OPTIONS } from '../constants'; 10 | 11 | import { CrudItem, CrudOptions } from '../types'; 12 | 13 | /** 14 | * 控制器上的CRUD装饰器 15 | * @param options 16 | */ 17 | export const Crud = 18 | (options: CrudOptions) => 19 | >(Target: Type) => { 20 | Reflect.defineMetadata(CRUD_OPTIONS, options, Target); 21 | 22 | const { id, enabled, dtos, preAuth } = Reflect.getMetadata( 23 | CRUD_OPTIONS, 24 | Target, 25 | ) as CrudOptions; 26 | const methods: CrudItem[] = []; 27 | // 添加启用的CRUD方法 28 | for (const value of enabled) { 29 | const item = (typeof value === 'string' ? { name: value } : value) as CrudItem; 30 | if ( 31 | methods.map(({ name }) => name).includes(item.name) || 32 | !isNil(Object.getOwnPropertyDescriptor(Target.prototype, item.name)) 33 | ) 34 | continue; 35 | methods.push(item); 36 | } 37 | // 添加控制器方法的具体实现,参数的DTO类型,方法及路径装饰器,序列化选项,是否允许匿名访问等metadata 38 | // 添加其它回调函数 39 | for (const { name, option = {} } of methods) { 40 | if (isNil(Object.getOwnPropertyDescriptor(Target.prototype, name))) { 41 | const descriptor = Object.getOwnPropertyDescriptor(BaseController.prototype, name); 42 | Object.defineProperty(Target.prototype, name, { 43 | ...descriptor, 44 | async value(...args: any[]) { 45 | return descriptor.value.apply(this, args); 46 | }, 47 | }); 48 | } 49 | 50 | const descriptor = Object.getOwnPropertyDescriptor(Target.prototype, name); 51 | 52 | const [, ...params] = Reflect.getMetadata('design:paramtypes', Target.prototype, name); 53 | 54 | if (name === 'store' && !isNil(dtos.store)) { 55 | Reflect.defineMetadata( 56 | 'design:paramtypes', 57 | [dtos.store, ...params], 58 | Target.prototype, 59 | name, 60 | ); 61 | } else if (name === 'update' && !isNil(dtos.update)) { 62 | Reflect.defineMetadata( 63 | 'design:paramtypes', 64 | [dtos.update, ...params], 65 | Target.prototype, 66 | name, 67 | ); 68 | } else if (name === 'list' && !isNil(dtos.list)) { 69 | Reflect.defineMetadata( 70 | 'design:paramtypes', 71 | [dtos.list, ...params], 72 | Target.prototype, 73 | name, 74 | ); 75 | } 76 | 77 | let serialize: ClassTransformOptions = {}; 78 | if (isNil(option.serialize)) { 79 | if (['detail', 'store', 'update', 'delete', 'restore'].includes(name)) { 80 | serialize = { groups: [`${id}-detail`] }; 81 | } else if (['list'].includes(name)) { 82 | serialize = { groups: [`${id}-list`] }; 83 | } 84 | } else if (option.serialize === 'noGroup') { 85 | serialize = {}; 86 | } else { 87 | serialize = option.serialize; 88 | } 89 | SerializeOptions(serialize)(Target, name, descriptor); 90 | // 识别控制层固定的几个方法名,加请求方式装饰器 91 | switch (name) { 92 | case 'list': 93 | Get()(Target, name, descriptor); 94 | break; 95 | case 'detail': 96 | Get(':id')(Target, name, descriptor); 97 | break; 98 | case 'store': 99 | Post()(Target, name, descriptor); 100 | break; 101 | case 'update': 102 | Patch()(Target, name, descriptor); 103 | break; 104 | case 'delete': 105 | Delete()(Target, name, descriptor); 106 | break; 107 | case 'restore': 108 | Patch('restore')(Target, name, descriptor); 109 | break; 110 | default: 111 | break; 112 | } 113 | // 给控制层enabled中定义的API,加资源权限装饰器 114 | RequireAuthority(`${preAuth}${name}`)(Target, name, descriptor); 115 | 116 | if (!isNil(option.hook)) option.hook(Target, name); 117 | } 118 | 119 | return Target; 120 | }; 121 | -------------------------------------------------------------------------------- /src/modules/restful/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud.decorator'; 2 | -------------------------------------------------------------------------------- /src/modules/restful/dtos/delete-with-trash.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsBoolean, IsOptional } from 'class-validator'; 3 | 4 | import { DtoValidation } from '@/modules/core/decorators'; 5 | import { toBoolean } from '@/modules/core/helpers'; 6 | 7 | import { DeleteDto } from './delete.dto'; 8 | 9 | /** 10 | * 带软删除的批量删除验证 11 | */ 12 | @DtoValidation() 13 | export class DeleteWithTrashDto extends DeleteDto { 14 | @Transform(({ value }) => toBoolean(value)) 15 | @IsBoolean() 16 | @IsOptional() 17 | trash?: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/restful/dtos/delete.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDefined, IsNumberString } from 'class-validator'; 2 | 3 | import { DtoValidation } from '@/modules/core/decorators'; 4 | 5 | /** 6 | * 批量删除验证 7 | */ 8 | @DtoValidation() 9 | export class DeleteDto { 10 | @IsNumberString(undefined, { 11 | each: true, 12 | message: 'ID格式错误', 13 | }) 14 | @IsDefined({ 15 | each: true, 16 | message: 'ID必须指定', 17 | }) 18 | ids: string[] = []; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/restful/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './delete.dto'; 2 | export * from './delete-with-trash.dto'; 3 | export * from './restore.dto'; 4 | export * from './query.dto'; 5 | -------------------------------------------------------------------------------- /src/modules/restful/dtos/query.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsEnum, IsNumber, IsOptional, Min } from 'class-validator'; 3 | 4 | import { toNumber } from 'lodash'; 5 | 6 | import { DtoValidation } from '@/modules/core/decorators'; 7 | import { SelectTrashMode } from '@/modules/database/constants'; 8 | import { PaginateOptions, TrashedOptions } from '@/modules/database/types'; 9 | 10 | @DtoValidation({ type: 'query' }) 11 | export class ListQueryDto implements PaginateOptions { 12 | @Transform(({ value }) => toNumber(value)) 13 | @Min(1, { message: '当前页必须大于1' }) 14 | @IsNumber() 15 | @IsOptional() 16 | page = 1; 17 | 18 | @Transform(({ value }) => toNumber(value)) 19 | @Min(1, { message: '每页显示数据必须大于1' }) 20 | @IsNumber() 21 | @IsOptional() 22 | limit = 10; 23 | } 24 | 25 | @DtoValidation({ type: 'query' }) 26 | export class ListWithTrashedQueryDto extends ListQueryDto implements TrashedOptions { 27 | @IsEnum(SelectTrashMode) 28 | @IsOptional() 29 | trashed?: SelectTrashMode; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/restful/dtos/restore.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDefined, IsNumberString } from 'class-validator'; 2 | 3 | import { DtoValidation } from '@/modules/core/decorators'; 4 | 5 | /** 6 | * 批量恢复验证 7 | */ 8 | @DtoValidation() 9 | export class RestoreDto { 10 | @IsNumberString(undefined, { 11 | each: true, 12 | message: 'ID格式错误', 13 | }) 14 | @IsDefined({ 15 | each: true, 16 | message: 'ID必须指定', 17 | }) 18 | ids: string[] = []; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/restful/oss/oss-middleware.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | 5 | import { OSSMiddlewareService } from './oss-middleware.service'; 6 | import { AliOSSType, QiniuOSSType } from './oss-middleware.type'; 7 | 8 | @Controller('oss-middleware') 9 | @RequireLogin() 10 | export class OSSMiddlewareController { 11 | constructor(protected service: OSSMiddlewareService) {} 12 | 13 | /** 14 | * 获取阿里云 OSS 上传信息 15 | */ 16 | @Get('getAliOSSInfo') 17 | @RequireAuthority('oss-middleware:getAliOSSInfo') 18 | async getAliOSSInfo(): Promise { 19 | return this.service.getAliSignature(); 20 | } 21 | 22 | /** 23 | * 获取七牛云 OSS 上传信息 24 | */ 25 | @Get('getQiniuOSSInfo') 26 | @RequireAuthority('oss-middleware:getQiniuOSSInfo') 27 | async getQiniuOSSInfo(): Promise { 28 | return this.service.getQiniuSignature(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/restful/oss/oss-middleware.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { OSSMiddlewareController } from './oss-middleware.controller'; 4 | import { OSSMiddlewareService } from './oss-middleware.service'; 5 | 6 | @Module({ 7 | imports: [], 8 | controllers: [OSSMiddlewareController], 9 | providers: [OSSMiddlewareService], 10 | exports: [], 11 | }) 12 | export class OSSMiddlewareModule {} 13 | -------------------------------------------------------------------------------- /src/modules/restful/oss/oss-middleware.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import OSS from 'ali-oss'; 4 | import dayjs from 'dayjs'; 5 | import qiniu from 'qiniu'; 6 | 7 | import { AliOSSType, QiniuOSSType } from './oss-middleware.type'; 8 | 9 | @Injectable() 10 | export class OSSMiddlewareService { 11 | /** 12 | * 获取阿里云 OSS 上传签名 13 | */ 14 | async getAliSignature(): Promise { 15 | const config = { 16 | accessKeyId: process.env.ALI_ACCESS_KEY, 17 | accessKeySecret: process.env.ALI_ACCESS_KEY_SECRET, 18 | bucket: 'water-drop-resources', 19 | dir: 'images/', 20 | }; 21 | 22 | const client = new OSS(config); 23 | 24 | const date = new Date(); 25 | date.setDate(date.getDate() + 1); 26 | const policy = { 27 | expiration: date.toISOString(), // 请求有效期 28 | conditions: [ 29 | ['content-length-range', 0, 1048576000], // 设置上传文件的大小限制 30 | ], 31 | }; 32 | 33 | // bucket域名 34 | const host = `https://${config.bucket}.${ 35 | (await client.getBucketLocation(config.bucket)).location 36 | }.aliyuncs.com`.toString(); 37 | // 签名 38 | const formData = client.calculatePostSignature(policy); 39 | // 返回参数 40 | const params = { 41 | expire: dayjs().add(1, 'days').unix().toString(), 42 | policy: formData.policy, 43 | signature: formData.Signature, 44 | accessId: formData.OSSAccessKeyId, 45 | host, 46 | dir: 'images/', 47 | }; 48 | 49 | return params; 50 | } 51 | 52 | /** 53 | * 获取七牛云 OSS 上传签名 54 | */ 55 | async getQiniuSignature(): Promise { 56 | const accessKey = process.env.QINIU_ACCESS_KEY; 57 | const secretKey = process.env.QINIU_SECRET_KEY; 58 | const mac = new qiniu.auth.digest.Mac(accessKey, secretKey); 59 | const options = { 60 | host: 'https://qiniu.panlore.top', 61 | dir: 'project/prune/', 62 | scope: 'kd-figure-bed', 63 | expire: new Date().getTime() + 60 * 60 * 1000, // 和默认值一样,1个小时 64 | }; 65 | const putPolicy = new qiniu.rs.PutPolicy(options); 66 | const uploadToken = putPolicy.uploadToken(mac); 67 | const params = { 68 | host: options.host, 69 | dir: options.dir, 70 | token: uploadToken, 71 | expires: options.expire, 72 | }; 73 | return params; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/modules/restful/oss/oss-middleware.type.ts: -------------------------------------------------------------------------------- 1 | export class AliOSSType { 2 | expire: string; 3 | 4 | policy: string; 5 | 6 | signature: string; 7 | 8 | accessId: string; 9 | 10 | host: string; 11 | 12 | dir: string; 13 | } 14 | 15 | export class QiniuOSSType { 16 | host: string; 17 | 18 | dir: string; 19 | 20 | token: string; 21 | 22 | expires: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/restful/types.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { ClassTransformOptions } from 'class-transformer'; 3 | 4 | /** 5 | * CRUD控制器方法列表 6 | */ 7 | export type CrudMethod = 'detail' | 'delete' | 'restore' | 'list' | 'store' | 'update'; 8 | 9 | /** 10 | * CRUD装饰器的方法选项 11 | */ 12 | export interface CrudMethodOption { 13 | /** 14 | * 序列化选项,如果为`noGroup`则不传参数,否则根据`id`+方法匹配来传参 15 | */ 16 | serialize?: ClassTransformOptions | 'noGroup'; 17 | hook?: (target: Type, method: string) => void; 18 | } 19 | /** 20 | * 每个启用方法的配置 21 | */ 22 | export interface CrudItem { 23 | name: CrudMethod; 24 | option?: CrudMethodOption; 25 | } 26 | 27 | /** 28 | * CRUD装饰器选项 29 | */ 30 | export interface CrudOptions { 31 | id: string; 32 | // 需要启用的方法 33 | enabled: Array; 34 | // 一些方法要使用到的自定义DTO 35 | dtos: { 36 | [key in 'list' | 'store' | 'update']?: Type; 37 | }; 38 | // 资源权限使用的前缀 39 | preAuth: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/restful/upload/fastify-file-interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | mixin, 6 | NestInterceptor, 7 | Optional, 8 | Type, 9 | } from '@nestjs/common'; 10 | import FastifyMulter from 'fastify-multer'; 11 | import { Options, Multer } from 'multer'; 12 | import { Observable } from 'rxjs'; 13 | 14 | type MulterInstance = any; 15 | 16 | /** 17 | * 文件拦截器 18 | */ 19 | export function FastifyFileInterceptor( 20 | fieldName: string, 21 | localOptions: Options, 22 | ): Type { 23 | class MixinInterceptor implements NestInterceptor { 24 | protected multer: MulterInstance; 25 | 26 | constructor( 27 | @Optional() 28 | @Inject('MULTER_MODULE_OPTIONS') 29 | options: Multer, 30 | ) { 31 | this.multer = (FastifyMulter as any)({ ...options, ...localOptions }); 32 | } 33 | 34 | async intercept(context: ExecutionContext, next: CallHandler): Promise> { 35 | const ctx = context.switchToHttp(); 36 | 37 | await new Promise((resolve, reject) => 38 | this.multer.single(fieldName)(ctx.getRequest(), ctx.getResponse(), (error: any) => { 39 | if (error) { 40 | // const error = transformException(err); 41 | return reject(error); 42 | } 43 | resolve(); 44 | }), 45 | ); 46 | 47 | return next.handle(); 48 | } 49 | } 50 | const Interceptor = mixin(MixinInterceptor); 51 | return Interceptor as Type; 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/restful/upload/file-mappter.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | interface FileMapper { 4 | file: Express.Multer.File; 5 | req: Request; 6 | } 7 | 8 | /** 9 | * 返回File对象 10 | */ 11 | export const fileMapper = ({ file, req }: FileMapper) => { 12 | return { 13 | originalName: file.originalname, 14 | fileName: file.filename, 15 | fileUrl: `${req.protocol}://${req.headers.host}/${file.path}`, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/restful/upload/file-upload-util.ts: -------------------------------------------------------------------------------- 1 | import { extname } from 'path'; 2 | 3 | import { Request } from 'express'; 4 | 5 | /** 6 | * 导入文件的名称处理 7 | */ 8 | export const editFileName = (req: Request, file: Express.Multer.File, callback: any) => { 9 | const name = file.originalname.split('.')[0]; 10 | const fileExtName = extname(file.originalname); 11 | const randomName = Array(4) 12 | .fill(null) 13 | .map(() => Math.round(Math.random() * 16).toString(16)) 14 | .join(''); 15 | callback(null, `${name}-${randomName}${fileExtName}`); 16 | }; 17 | 18 | /** 19 | * 图片过滤器 20 | */ 21 | export const imageFileFilter = (req: Request, file: Express.Multer.File, callback: any) => { 22 | if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) { 23 | callback(new Error('Only image files are allowed!'), false); 24 | return; 25 | } 26 | callback(null, true); 27 | }; 28 | -------------------------------------------------------------------------------- /src/modules/system/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 字典排序类型 3 | */ 4 | export enum PublicOrderType { 5 | CREATED = 'createdAt', 6 | UPDATED = 'updatedAt', 7 | } 8 | 9 | export enum DictionaryType { 10 | NATION = 'NATION', 11 | EDUCATION = 'EDUCATION', 12 | POSITION_STATUS = 'POSITION_STATUS', 13 | ROLE_CATEGORY = 'ROLE_CATEGORY', 14 | OSSC_CATEGORY = 'OSSC_CATEGORY', 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/system/controllers/area.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateAreaDto, QueryAreaTreeDto, UpdateAreaDto } from '../dtos'; 8 | 9 | import { AreaService } from '../services'; 10 | 11 | @Crud({ 12 | id: 'area', 13 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 14 | dtos: { 15 | store: CreateAreaDto, 16 | update: UpdateAreaDto, 17 | list: QueryAreaTreeDto, 18 | }, 19 | preAuth: 'system:area:', 20 | }) 21 | @Controller('area') 22 | @RequireLogin() 23 | export class AreaController extends BaseController { 24 | constructor(protected service: AreaService) { 25 | super(service); 26 | } 27 | 28 | @Get('tree') 29 | @RequireAuthority('system:area:tree') 30 | tree(@Query() options: QueryAreaTreeDto) { 31 | return this.service.findTrees(options); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/system/controllers/dictionary.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query, SerializeOptions } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateDictionaryDto, QueryDictionaryDto, UpdateDictionaryDto } from '../dtos'; 8 | 9 | import { DictionaryService } from '../services'; 10 | 11 | @Crud({ 12 | id: 'dict', 13 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 14 | dtos: { 15 | store: CreateDictionaryDto, 16 | update: UpdateDictionaryDto, 17 | list: QueryDictionaryDto, 18 | }, 19 | preAuth: 'system:dict:', 20 | }) 21 | @Controller('dict') 22 | @RequireLogin() 23 | export class DictionaryController extends BaseController { 24 | constructor(protected service: DictionaryService) { 25 | super(service); 26 | } 27 | 28 | @Get('listType') 29 | @RequireAuthority('system:dict:listType') 30 | @SerializeOptions({ groups: ['dict-list'] }) 31 | listType(@Query() dto: QueryDictionaryDto) { 32 | return this.service.paginateType(dto); 33 | } 34 | 35 | @Get('listMultiType') 36 | @RequireAuthority('system:dict:listMultiType') 37 | @SerializeOptions({ groups: ['dict-list'] }) 38 | listMultiType(@Query() dto: QueryDictionaryDto) { 39 | return this.service.listWhereTypes(dto); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/system/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dictionary.controller'; 2 | export * from './parameter.controller'; 3 | export * from './area.controller'; 4 | export * from './menu.controller'; 5 | export * from './resource.controller'; 6 | -------------------------------------------------------------------------------- /src/modules/system/controllers/menu.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | 3 | import { RequireAuthority, RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateMenuDto, QueryMenuTreeDto, UpdateMenuDto } from '../dtos'; 8 | import { MenuService } from '../services'; 9 | 10 | @Crud({ 11 | id: 'menu', 12 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 13 | dtos: { 14 | store: CreateMenuDto, 15 | update: UpdateMenuDto, 16 | list: QueryMenuTreeDto, 17 | }, 18 | preAuth: 'system:menu:', 19 | }) 20 | @Controller('menu') 21 | @RequireLogin() 22 | export class MenuController extends BaseController { 23 | constructor(protected service: MenuService) { 24 | super(service); 25 | } 26 | 27 | @Get('tree') 28 | @RequireAuthority('system:menu:tree') 29 | tree(@Query() options: QueryMenuTreeDto) { 30 | return this.service.findTrees(options); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/system/controllers/parameter.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | import { RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateParameterDto, QueryParameterDto, UpdateParameterDto } from '../dtos'; 8 | 9 | import { ParameterService } from '../services'; 10 | 11 | @Crud({ 12 | id: 'param', 13 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 14 | dtos: { 15 | store: CreateParameterDto, 16 | update: UpdateParameterDto, 17 | list: QueryParameterDto, 18 | }, 19 | preAuth: 'system:param:', 20 | }) 21 | @Controller('param') 22 | @RequireLogin() 23 | export class ParameterController extends BaseController { 24 | constructor(protected service: ParameterService) { 25 | super(service); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/system/controllers/resource.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | 3 | import { RequireLogin } from '@/modules/auth/auth.decorator'; 4 | import { BaseController } from '@/modules/restful/base'; 5 | import { Crud } from '@/modules/restful/decorators'; 6 | 7 | import { CreateResourceDto, QueryResourceDto, UpdateResourceDto } from '../dtos'; 8 | 9 | import { ResourceService } from '../services'; 10 | 11 | @Crud({ 12 | id: 'resource', 13 | enabled: ['list', 'detail', 'store', 'update', 'delete', 'restore'], 14 | dtos: { 15 | store: CreateResourceDto, 16 | update: UpdateResourceDto, 17 | list: QueryResourceDto, 18 | }, 19 | preAuth: 'system:resource:', 20 | }) 21 | @Controller('resource') 22 | @RequireLogin() 23 | export class ResourceController extends BaseController { 24 | constructor(protected service: ResourceService) { 25 | super(service); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/system/dtos/area.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { 4 | IsDefined, 5 | IsEnum, 6 | IsNotEmpty, 7 | IsNumber, 8 | IsNumberString, 9 | IsOptional, 10 | Min, 11 | ValidateIf, 12 | } from 'class-validator'; 13 | 14 | import { toNumber } from 'lodash'; 15 | 16 | import { DtoValidation } from '@/modules/core/decorators'; 17 | import { SelectTrashMode } from '@/modules/database/constants'; 18 | import { IsDataExist, IsTreeUnique, IsTreeUniqueExist } from '@/modules/database/constraints'; 19 | import { ListQueryDto } from '@/modules/restful/dtos'; 20 | import { AreaEntity } from '@/modules/system/entities'; 21 | 22 | /** 23 | * 地区分页查询验证 24 | */ 25 | @DtoValidation({ type: 'query' }) 26 | export class QueryAreaTreeDto extends ListQueryDto { 27 | @IsEnum(SelectTrashMode) 28 | @IsOptional() 29 | trashed?: SelectTrashMode; 30 | } 31 | 32 | /** 33 | * 地区创建验证 34 | */ 35 | @DtoValidation({ groups: ['create'] }) 36 | export class CreateAreaDto { 37 | @IsTreeUnique(AreaEntity, { 38 | groups: ['create'], 39 | message: '地区编码重复', 40 | }) 41 | @IsTreeUniqueExist(AreaEntity, { 42 | groups: ['update'], 43 | message: '地区编码重复', 44 | }) 45 | @IsNotEmpty({ groups: ['create'], message: '地区编码不能为空' }) 46 | @IsOptional({ groups: ['update'] }) 47 | code!: string; 48 | 49 | @IsDataExist(AreaEntity, { always: true, message: '父地区不存在' }) 50 | @IsNumberString(undefined, { always: true, message: '父地区ID格式不正确' }) 51 | @ValidateIf((value) => value.parent !== null && value.parent) 52 | @IsOptional({ always: true }) 53 | parent?: string; 54 | 55 | @Transform(({ value }) => toNumber(value)) 56 | @Min(1, { always: true, message: '排序值必须大于1' }) 57 | @IsNumber(undefined, { always: true }) 58 | @IsOptional({ always: true }) 59 | sortValue = 1; 60 | 61 | @IsNotEmpty({ groups: ['create'], message: '地区名称不能为空' }) 62 | @IsOptional({ groups: ['update'] }) 63 | label: string; 64 | 65 | @IsOptional() 66 | fullName: string | null; 67 | 68 | @IsOptional() 69 | level: string | null; 70 | 71 | @IsOptional() 72 | source: string | null; 73 | } 74 | 75 | /** 76 | * 地区更新验证 77 | */ 78 | @DtoValidation({ groups: ['update'] }) 79 | export class UpdateAreaDto extends PartialType(CreateAreaDto) { 80 | @IsNumberString(undefined, { groups: ['update'], message: '地区ID格式错误' }) 81 | @IsDefined({ groups: ['update'], message: '地区ID必须指定' }) 82 | id!: string; 83 | } 84 | -------------------------------------------------------------------------------- /src/modules/system/dtos/dictionary.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { IsDefined, IsEnum, IsNotEmpty, IsNumberString, IsOptional } from 'class-validator'; 3 | 4 | import { DtoValidation } from '@/modules/core/decorators'; 5 | 6 | import { ListWithTrashedQueryDto } from '@/modules/restful/dtos'; 7 | import { PublicOrderType } from '@/modules/system/constants'; 8 | 9 | /** 10 | * 字典分页查询验证 11 | */ 12 | @DtoValidation({ type: 'query' }) 13 | export class QueryDictionaryDto extends ListWithTrashedQueryDto { 14 | @IsEnum(PublicOrderType, { 15 | message: `排序规则必须是${Object.values(PublicOrderType).join(',')}其中一项`, 16 | }) 17 | @IsOptional() 18 | orderBy?: PublicOrderType; 19 | 20 | @IsOptional() 21 | label?: string; 22 | 23 | @IsOptional() 24 | type?: string; 25 | 26 | @IsOptional() 27 | code?: string; 28 | 29 | @IsOptional() 30 | name?: string; 31 | } 32 | 33 | /** 34 | * 字典创建验证 35 | */ 36 | @DtoValidation({ groups: ['create'] }) 37 | export class CreateDictionaryDto { 38 | @IsNotEmpty({ groups: ['create'], message: '类型必须传递' }) 39 | @IsOptional({ groups: ['update'] }) 40 | type!: string; 41 | 42 | @IsNotEmpty({ groups: ['create'], message: '类型标签必须传递' }) 43 | @IsOptional({ groups: ['update'] }) 44 | label!: string; 45 | 46 | @IsNotEmpty({ groups: ['create'], message: '编码必须填写' }) 47 | @IsOptional({ groups: ['update'] }) 48 | code!: string; 49 | } 50 | 51 | /** 52 | * 字典更新验证 53 | */ 54 | @DtoValidation({ groups: ['update'] }) 55 | export class UpdateDictionaryDto extends PartialType(CreateDictionaryDto) { 56 | @IsNumberString(undefined, { groups: ['update'], message: '字典ID格式错误' }) 57 | @IsDefined({ groups: ['update'], message: '字典ID必须指定' }) 58 | id!: string; 59 | } 60 | -------------------------------------------------------------------------------- /src/modules/system/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dictionary.dto'; 2 | export * from './parameter.dto'; 3 | export * from './area.dto'; 4 | export * from './menu.dto'; 5 | export * from './resource.dto'; 6 | -------------------------------------------------------------------------------- /src/modules/system/dtos/menu.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { 4 | IsDefined, 5 | IsEnum, 6 | IsNotEmpty, 7 | IsNumber, 8 | IsNumberString, 9 | IsOptional, 10 | Min, 11 | ValidateIf, 12 | } from 'class-validator'; 13 | 14 | import { toNumber } from 'lodash'; 15 | 16 | import { DtoValidation } from '@/modules/core/decorators'; 17 | import { SelectTrashMode } from '@/modules/database/constants'; 18 | import { IsDataExist, IsTreeUnique, IsTreeUniqueExist } from '@/modules/database/constraints'; 19 | 20 | import { ListQueryDto } from '@/modules/restful/dtos'; 21 | 22 | import { MenuEntity } from '../entities'; 23 | 24 | /** 25 | * 分页查询验证 26 | */ 27 | @DtoValidation({ type: 'query' }) 28 | export class QueryMenuTreeDto extends ListQueryDto { 29 | @IsEnum(SelectTrashMode) 30 | @IsOptional() 31 | trashed?: SelectTrashMode; 32 | } 33 | 34 | /** 35 | * 新建验证 36 | */ 37 | @DtoValidation({ groups: ['create'] }) 38 | export class CreateMenuDto { 39 | @IsTreeUnique(MenuEntity, { 40 | groups: ['create'], 41 | message: '该菜单名称已经存在', 42 | }) 43 | @IsTreeUniqueExist(MenuEntity, { 44 | groups: ['update'], 45 | message: '该菜单名称已经存在', 46 | }) 47 | @IsNotEmpty({ groups: ['create'], message: '菜单名称不能为空' }) 48 | @IsOptional({ groups: ['update'] }) 49 | name!: string; 50 | 51 | @IsTreeUnique(MenuEntity, { 52 | groups: ['create'], 53 | message: '该菜单标签已经存在', 54 | }) 55 | @IsTreeUniqueExist(MenuEntity, { 56 | groups: ['update'], 57 | message: '该菜单标签已经存在', 58 | }) 59 | @IsNotEmpty({ groups: ['create'], message: '菜单标签不能为空' }) 60 | @IsOptional({ groups: ['update'] }) 61 | label!: string; 62 | 63 | @IsDataExist(MenuEntity, { always: true, message: '父菜单不存在' }) 64 | @IsNumberString(undefined, { always: true, message: '父菜单ID格式不正确' }) 65 | @ValidateIf((value) => value.parent !== null && value.parent) 66 | @IsOptional({ always: true }) 67 | parent?: string; 68 | 69 | @Transform(({ value }) => toNumber(value)) 70 | @Min(1, { always: true, message: '排序值必须大于1' }) 71 | @IsNumber(undefined, { always: true }) 72 | @IsOptional({ always: true }) 73 | sortValue = 1; 74 | 75 | @IsOptional() 76 | abbreviation: string | null; 77 | 78 | @IsOptional() 79 | type: string | null; 80 | 81 | @IsOptional() 82 | describe: string | null; 83 | 84 | @IsOptional() 85 | state: boolean | null; 86 | } 87 | 88 | /** 89 | * 更新验证 90 | */ 91 | @DtoValidation({ groups: ['update'] }) 92 | export class UpdateMenuDto extends PartialType(CreateMenuDto) { 93 | @IsNumberString(undefined, { groups: ['update'], message: '机构ID格式错误' }) 94 | @IsDefined({ groups: ['update'], message: '机构ID必须指定' }) 95 | id!: string; 96 | } 97 | -------------------------------------------------------------------------------- /src/modules/system/dtos/parameter.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { IsDefined, IsEnum, IsNotEmpty, IsNumberString, IsOptional } from 'class-validator'; 4 | 5 | import { DtoValidation } from '@/modules/core/decorators'; 6 | 7 | import { ListWithTrashedQueryDto } from '@/modules/restful/dtos'; 8 | import { PublicOrderType } from '@/modules/system/constants'; 9 | 10 | /** 11 | * 系统参数分页查询验证 12 | */ 13 | @DtoValidation({ type: 'query' }) 14 | export class QueryParameterDto extends ListWithTrashedQueryDto { 15 | @IsEnum(PublicOrderType, { 16 | message: `排序规则必须是${Object.values(PublicOrderType).join(',')}其中一项`, 17 | }) 18 | @IsOptional() 19 | orderBy?: PublicOrderType; 20 | 21 | @IsOptional() 22 | key?: string; 23 | 24 | @IsOptional() 25 | name?: string; 26 | 27 | @IsOptional() 28 | value?: string; 29 | 30 | @Transform(({ value }) => value.split(',')) 31 | @IsOptional() 32 | timeRange?: string[]; 33 | } 34 | 35 | /** 36 | * 系统参数创建验证 37 | */ 38 | @DtoValidation({ groups: ['create'] }) 39 | export class CreateParameterDto { 40 | @IsNotEmpty({ groups: ['create', 'update'], message: '状态值必须传递' }) 41 | state!: boolean; 42 | 43 | @IsNotEmpty({ groups: ['create', 'update'], message: '内置值必须传递' }) 44 | readonly!: boolean; 45 | 46 | @IsNotEmpty({ groups: ['create'], message: '参数键不能为空' }) 47 | @IsOptional({ groups: ['update'] }) 48 | key!: string; 49 | 50 | @IsNotEmpty({ groups: ['create', 'update'], message: '参数名称不能为空' }) 51 | name!: string; 52 | 53 | @IsNotEmpty({ groups: ['create', 'update'], message: '参数值不能为空' }) 54 | value!: string; 55 | 56 | @IsOptional() 57 | describe?: string; 58 | } 59 | 60 | /** 61 | * 系统参数更新验证 62 | */ 63 | @DtoValidation({ groups: ['update'] }) 64 | export class UpdateParameterDto extends PartialType(CreateParameterDto) { 65 | @IsNumberString(undefined, { groups: ['update'], message: '参数ID格式错误' }) 66 | @IsDefined({ groups: ['update'], message: '参数ID必须指定' }) 67 | id!: string; 68 | } 69 | -------------------------------------------------------------------------------- /src/modules/system/dtos/resource.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { IsDefined, IsEnum, IsNotEmpty, IsNumberString, IsOptional } from 'class-validator'; 4 | 5 | import { DtoValidation } from '@/modules/core/decorators'; 6 | 7 | import { IsUnique, IsUniqueExist } from '@/modules/database/constraints'; 8 | import { ListWithTrashedQueryDto } from '@/modules/restful/dtos'; 9 | import { PublicOrderType } from '@/modules/system/constants'; 10 | 11 | import { ResourceEntity } from '../entities'; 12 | 13 | /** 14 | * 分页查询验证 15 | */ 16 | @DtoValidation({ type: 'query' }) 17 | export class QueryResourceDto extends ListWithTrashedQueryDto { 18 | @IsEnum(PublicOrderType, { 19 | message: `排序规则必须是${Object.values(PublicOrderType).join(',')}其中一项`, 20 | }) 21 | @IsOptional() 22 | orderBy?: PublicOrderType; 23 | 24 | @IsOptional() 25 | menuId?: string; 26 | 27 | @IsOptional() 28 | code?: string; 29 | 30 | @IsOptional() 31 | name?: string; 32 | 33 | @Transform(({ value }) => value.split(',')) 34 | @IsOptional() 35 | timeRange?: string[]; 36 | } 37 | 38 | /** 39 | * 创建验证 40 | */ 41 | @DtoValidation({ groups: ['create'] }) 42 | export class CreateResourceDto { 43 | @IsUnique(ResourceEntity, { 44 | groups: ['create'], 45 | message: '资源编码重复', 46 | }) 47 | @IsUniqueExist(ResourceEntity, { 48 | groups: ['update'], 49 | message: '资源编码重复', 50 | }) 51 | @IsNotEmpty({ groups: ['create', 'update'], message: '资源编码不能为空' }) 52 | code!: string; 53 | 54 | @IsNotEmpty({ groups: ['create', 'update'], message: '资源名称不能为空' }) 55 | name!: string; 56 | 57 | @IsOptional() 58 | describe?: string; 59 | } 60 | 61 | /** 62 | * 更新验证 63 | */ 64 | @DtoValidation({ groups: ['update'] }) 65 | export class UpdateResourceDto extends PartialType(CreateResourceDto) { 66 | @IsNumberString(undefined, { groups: ['update'], message: '资源ID格式错误' }) 67 | @IsDefined({ groups: ['update'], message: '资源ID必须指定' }) 68 | id!: string; 69 | } 70 | -------------------------------------------------------------------------------- /src/modules/system/entities/area.entity.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | DeleteDateColumn, 6 | Entity, 7 | Index, 8 | Tree, 9 | TreeChildren, 10 | TreeParent, 11 | UpdateDateColumn, 12 | } from 'typeorm'; 13 | 14 | import { BoolBitTransformer } from '@/modules/core/helpers/utils'; 15 | import { BaseEntity } from '@/modules/database/base'; 16 | 17 | @Tree('materialized-path') 18 | @Index('uk_code', ['code'], { unique: true }) 19 | @Entity('c_area') 20 | export class AreaEntity extends BaseEntity { 21 | @Type(() => AreaEntity) 22 | @TreeParent({ onDelete: 'CASCADE' }) 23 | parent: AreaEntity | null; 24 | 25 | @Type(() => AreaEntity) 26 | @TreeChildren({ cascade: true }) 27 | children: AreaEntity[]; 28 | 29 | depth = 0; 30 | 31 | @Column('varchar', { 32 | name: 'code', 33 | unique: true, 34 | comment: '编码', 35 | length: 64, 36 | }) 37 | code: string; 38 | 39 | @Column('varchar', { name: 'label', comment: '名称', length: 255 }) 40 | label: string; 41 | 42 | @Column('varchar', { 43 | name: 'full_name', 44 | nullable: true, 45 | comment: '全名', 46 | length: 255, 47 | }) 48 | fullName: string | null; 49 | 50 | @Column('int', { 51 | name: 'sort_value', 52 | nullable: true, 53 | comment: '排序', 54 | default: () => "'1'", 55 | }) 56 | sortValue: number | null; 57 | 58 | @Column('varchar', { 59 | name: 'longitude', 60 | nullable: true, 61 | comment: '经度', 62 | length: 255, 63 | }) 64 | longitude: string | null; 65 | 66 | @Column('varchar', { 67 | name: 'latitude', 68 | nullable: true, 69 | comment: '维度', 70 | length: 255, 71 | }) 72 | latitude: string | null; 73 | 74 | @Column('varchar', { 75 | name: 'level_', 76 | nullable: true, 77 | comment: '行政区级: dictType = AREA_LEVEL', 78 | length: 10, 79 | }) 80 | level: string | null; 81 | 82 | @Column('varchar', { 83 | name: 'source_', 84 | nullable: true, 85 | comment: '数据来源', 86 | length: 255, 87 | }) 88 | source: string | null; 89 | 90 | @Column('bit', { 91 | name: 'state', 92 | nullable: true, 93 | comment: '状态', 94 | default: () => "'b'0''", 95 | transformer: new BoolBitTransformer(), 96 | }) 97 | state: boolean | null; 98 | 99 | @Column('varchar', { 100 | name: 'parentId', 101 | nullable: true, 102 | comment: '父ID', 103 | length: 20, 104 | }) 105 | parentId: string | null; 106 | 107 | @DeleteDateColumn({ 108 | name: 'deleted_at', 109 | nullable: true, 110 | comment: '删除时间', 111 | }) 112 | deletedAt: Date | null; 113 | 114 | @Column('bigint', { name: 'created_by', nullable: true, comment: '创建人' }) 115 | createdBy: number | null; 116 | 117 | @CreateDateColumn({ 118 | name: 'created_at', 119 | nullable: true, 120 | comment: '创建时间', 121 | }) 122 | createdAt: Date | null; 123 | 124 | @Column('bigint', { name: 'updated_by', nullable: true, comment: '更新人' }) 125 | updatedBy: number | null; 126 | 127 | @UpdateDateColumn({ 128 | name: 'updated_at', 129 | nullable: true, 130 | comment: '更新时间', 131 | }) 132 | updatedAt: Date | null; 133 | } 134 | -------------------------------------------------------------------------------- /src/modules/system/entities/dictionary.entity.ts: -------------------------------------------------------------------------------- 1 | import { Exclude, Expose } from 'class-transformer'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | Entity, 6 | UpdateDateColumn, 7 | Index, 8 | DeleteDateColumn, 9 | } from 'typeorm'; 10 | 11 | import { BoolBitTransformer } from '@/modules/core/helpers/utils'; 12 | import { BaseEntity } from '@/modules/database/base'; 13 | 14 | @Exclude() 15 | @Index('uk_type_code', ['type', 'code'], { unique: true }) 16 | @Entity('c_dictionary') 17 | export class DictionaryEntity extends BaseEntity { 18 | @Expose() 19 | @Column('varchar', { name: 'type', comment: '类型', length: 255 }) 20 | type: string; 21 | 22 | @Expose() 23 | @Column('varchar', { name: 'label', comment: '类型标签', length: 255 }) 24 | label: string; 25 | 26 | @Expose() 27 | @Column('varchar', { name: 'code', comment: '编码', length: 64 }) 28 | code: string; 29 | 30 | @Expose() 31 | @Column('varchar', { name: 'name', comment: '名称', length: 64 }) 32 | name: string; 33 | 34 | @Expose() 35 | @Column('bit', { 36 | name: 'state', 37 | nullable: true, 38 | comment: '状态', 39 | default: () => "'b'1''", 40 | transformer: new BoolBitTransformer(), 41 | }) 42 | state: boolean | null; 43 | 44 | @Expose() 45 | @Column('varchar', { 46 | name: 'describe_', 47 | nullable: true, 48 | comment: '描述', 49 | length: 255, 50 | }) 51 | describe: string | null; 52 | 53 | @Expose({ groups: ['dict-list'] }) 54 | @Column('int', { 55 | name: 'sort_value', 56 | nullable: true, 57 | comment: '排序', 58 | default: () => "'1'", 59 | }) 60 | sortValue: number | null; 61 | 62 | @Expose() 63 | @Column('varchar', { 64 | name: 'icon', 65 | nullable: true, 66 | comment: '图标', 67 | length: 255, 68 | }) 69 | icon: string | null; 70 | 71 | @Expose() 72 | @Column('varchar', { 73 | name: 'css_style', 74 | nullable: true, 75 | comment: 'css样式', 76 | length: 255, 77 | }) 78 | cssStyle: string | null; 79 | 80 | @Expose() 81 | @Column('varchar', { 82 | name: 'css_class', 83 | nullable: true, 84 | comment: 'css class', 85 | length: 255, 86 | }) 87 | cssClass: string | null; 88 | 89 | @Expose() 90 | @Column('bit', { 91 | name: 'readonly_', 92 | nullable: true, 93 | comment: '内置', 94 | default: () => "'b'0''", 95 | transformer: new BoolBitTransformer(), 96 | }) 97 | readonly: boolean | null; 98 | 99 | @Expose() 100 | @Column('bigint', { name: 'created_by', nullable: true, comment: '创建人id' }) 101 | createdBy: number | null; 102 | 103 | @Expose() 104 | @Column('bigint', { name: 'updated_by', nullable: true, comment: '更新人id' }) 105 | updatedBy: number | null; 106 | 107 | @Expose() 108 | @CreateDateColumn({ 109 | name: 'created_at', 110 | nullable: true, 111 | comment: '创建时间', 112 | }) 113 | createdAt: Date | null; 114 | 115 | @Expose() 116 | @UpdateDateColumn({ 117 | name: 'updated_at', 118 | nullable: true, 119 | comment: '更新时间', 120 | }) 121 | updatedAt: Date | null; 122 | 123 | @Expose() 124 | @DeleteDateColumn({ name: 'deleted_at', comment: '删除时间', nullable: true }) 125 | deletedAt: Date | null; 126 | } 127 | -------------------------------------------------------------------------------- /src/modules/system/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dictionary.entity'; 2 | export * from './parameter.entity'; 3 | export * from './area.entity'; 4 | export * from './menu.entity'; 5 | export * from './resource.entity'; 6 | -------------------------------------------------------------------------------- /src/modules/system/entities/menu.entity.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { Column, DeleteDateColumn, Entity, Index, Tree, TreeChildren, TreeParent } from 'typeorm'; 3 | 4 | import { BoolBitTransformer } from '@/modules/core/helpers'; 5 | import { BaseEntity } from '@/modules/database/base'; 6 | 7 | @Tree('materialized-path') 8 | @Index('uk_path', ['path'], { unique: true }) 9 | @Entity('c_menu') 10 | export class MenuEntity extends BaseEntity { 11 | @Type(() => MenuEntity) 12 | @TreeParent({ onDelete: 'CASCADE' }) 13 | parent: MenuEntity | null; 14 | 15 | @Type(() => MenuEntity) 16 | @TreeChildren({ cascade: true }) 17 | children: MenuEntity[]; 18 | 19 | depth = 0; 20 | 21 | @Column('varchar', { name: 'name', comment: '名称', length: 32 }) 22 | name: string; 23 | 24 | @Column('varchar', { name: 'label', comment: '标签', length: 64 }) 25 | label: string; 26 | 27 | @Column('char', { 28 | name: 'resource_type', 29 | nullable: true, 30 | comment: '[10-有子级的菜单 20-无子级的菜单 60-按钮]; dictType = RESOURCE_TYPE', 31 | length: 2, 32 | }) 33 | resourceType: string | null; 34 | 35 | @Column('int', { name: 'tree_grade', nullable: true, comment: '树层级' }) 36 | treeGrade: number | null; 37 | 38 | @Column('varchar', { 39 | name: 'describe_', 40 | nullable: true, 41 | comment: '描述', 42 | length: 200, 43 | }) 44 | describe: string | null; 45 | 46 | @Column('bit', { 47 | name: 'is_general', 48 | nullable: true, 49 | comment: '通用菜单 \nTrue表示无需分配所有人就可以访问的', 50 | default: () => "'b'0''", 51 | transformer: new BoolBitTransformer(), 52 | }) 53 | isGeneral: boolean | null; 54 | 55 | @Column('varchar', { 56 | name: 'path', 57 | nullable: true, 58 | unique: true, 59 | comment: '路径', 60 | length: 255, 61 | }) 62 | path: string | null; 63 | 64 | @Column('varchar', { 65 | name: 'component', 66 | nullable: true, 67 | comment: '组件', 68 | length: 255, 69 | }) 70 | component: string | null; 71 | 72 | @Column('bit', { 73 | name: 'state', 74 | nullable: true, 75 | comment: '状态', 76 | default: () => "'b'1''", 77 | transformer: new BoolBitTransformer(), 78 | }) 79 | state: boolean | null; 80 | 81 | @Column('int', { 82 | name: 'sort_value', 83 | nullable: true, 84 | comment: '排序', 85 | default: () => "'1'", 86 | }) 87 | sortValue: number | null; 88 | 89 | @Column('varchar', { 90 | name: 'icon', 91 | nullable: true, 92 | comment: '菜单图标', 93 | length: 255, 94 | }) 95 | icon: string | null; 96 | 97 | @Column('varchar', { 98 | name: 'group_', 99 | nullable: true, 100 | comment: '分组', 101 | length: 20, 102 | }) 103 | group: string | null; 104 | 105 | @Column('char', { 106 | name: 'data_scope', 107 | nullable: true, 108 | comment: 109 | '数据范围;[01-全部 02-本单位及子级 03-本单位 04-本部门 05-本部门及子级 06-个人 07-自定义]', 110 | length: 2, 111 | }) 112 | dataScope: string | null; 113 | 114 | @Column('varchar', { 115 | name: 'custom_class', 116 | nullable: true, 117 | comment: '实现类', 118 | length: 255, 119 | }) 120 | customClass: string | null; 121 | 122 | @Column('bit', { 123 | name: 'is_def', 124 | nullable: true, 125 | comment: '是否默认', 126 | default: () => "'b'0''", 127 | transformer: new BoolBitTransformer(), 128 | }) 129 | isDef: boolean | null; 130 | 131 | @Column('varchar', { 132 | name: 'parentId', 133 | nullable: true, 134 | comment: '父级菜单ID', 135 | length: 20, 136 | }) 137 | parentId: string | null; 138 | 139 | @Column('bit', { 140 | name: 'readonly_', 141 | nullable: true, 142 | comment: '内置', 143 | default: () => "'b'0''", 144 | transformer: new BoolBitTransformer(), 145 | }) 146 | readonly: boolean | null; 147 | 148 | @DeleteDateColumn({ 149 | name: 'deleted_at', 150 | nullable: true, 151 | comment: '删除时间', 152 | }) 153 | deletedAt: Date | null; 154 | 155 | @Column('bigint', { name: 'created_by', nullable: true, comment: '创建人id' }) 156 | createdBy: number | null; 157 | 158 | @Column('datetime', { 159 | name: 'created_at', 160 | nullable: true, 161 | comment: '创建时间', 162 | }) 163 | createdAt: Date | null; 164 | 165 | @Column('bigint', { name: 'updated_by', nullable: true, comment: '更新人id' }) 166 | updatedBy: number | null; 167 | 168 | @Column('datetime', { 169 | name: 'updated_at', 170 | nullable: true, 171 | comment: '更新时间', 172 | }) 173 | updatedAt: Date | null; 174 | } 175 | -------------------------------------------------------------------------------- /src/modules/system/entities/parameter.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | UpdateDateColumn, 6 | Index, 7 | DeleteDateColumn, 8 | } from 'typeorm'; 9 | 10 | import { BoolBitTransformer } from '@/modules/core/helpers/utils'; 11 | import { BaseEntity } from '@/modules/database/base'; 12 | 13 | @Index('uk_key', ['key'], { unique: true }) 14 | @Entity('c_parameter') 15 | export class ParameterEntity extends BaseEntity { 16 | @Column('varchar', { 17 | name: 'key_', 18 | unique: true, 19 | comment: '参数键', 20 | length: 255, 21 | }) 22 | key: string; 23 | 24 | @Column('varchar', { name: 'value', comment: '参数值', length: 255 }) 25 | value: string; 26 | 27 | @Column('varchar', { name: 'name', comment: '参数名称', length: 255 }) 28 | name: string; 29 | 30 | @Column('varchar', { 31 | name: 'describe_', 32 | nullable: true, 33 | comment: '描述', 34 | length: 255, 35 | }) 36 | describe: string | null; 37 | 38 | @Column('bit', { 39 | name: 'state', 40 | nullable: true, 41 | comment: '状态', 42 | default: () => "'b'1''", 43 | transformer: new BoolBitTransformer(), 44 | }) 45 | state: boolean | null; 46 | 47 | @Column('bit', { 48 | name: 'readonly_', 49 | nullable: true, 50 | comment: '内置', 51 | default: () => "'b'0''", 52 | transformer: new BoolBitTransformer(), 53 | }) 54 | readonly: boolean | null; 55 | 56 | @Column('bigint', { name: 'created_by', nullable: true, comment: '创建人id' }) 57 | createdBy: number | null; 58 | 59 | @Column('bigint', { name: 'updated_by', nullable: true, comment: '更新人id' }) 60 | updatedBy: number | null; 61 | 62 | @CreateDateColumn({ 63 | name: 'created_at', 64 | nullable: true, 65 | comment: '创建时间', 66 | }) 67 | createdAt: Date | null; 68 | 69 | @UpdateDateColumn({ 70 | name: 'updated_at', 71 | nullable: true, 72 | comment: '更新时间', 73 | }) 74 | updatedAt: Date | null; 75 | 76 | @DeleteDateColumn({ name: 'deleted_at', comment: '删除时间', nullable: true }) 77 | deletedAt: Date | null; 78 | } 79 | -------------------------------------------------------------------------------- /src/modules/system/entities/resource.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | DeleteDateColumn, 5 | Entity, 6 | Index, 7 | UpdateDateColumn, 8 | } from 'typeorm'; 9 | 10 | import { BoolBitTransformer } from '@/modules/core/helpers'; 11 | import { BaseEntity } from '@/modules/database/base'; 12 | 13 | @Index('uk_code', ['code'], { unique: true }) 14 | @Entity('c_resource') 15 | export class ResourceEntity extends BaseEntity { 16 | @Column('varchar', { 17 | name: 'code', 18 | nullable: true, 19 | unique: true, 20 | comment: '编码', 21 | length: 500, 22 | }) 23 | code: string | null; 24 | 25 | @Column('varchar', { name: 'name', comment: '名称', length: 255 }) 26 | name: string; 27 | 28 | @Column('varchar', { 29 | name: 'menu_id', 30 | nullable: true, 31 | comment: '菜单\n#c_menu', 32 | length: 20, 33 | }) 34 | menuId: string | null; 35 | 36 | @Column('varchar', { 37 | name: 'describe_', 38 | nullable: true, 39 | comment: '描述', 40 | length: 255, 41 | }) 42 | describe: string | null; 43 | 44 | @Column('bit', { 45 | name: 'readonly_', 46 | nullable: true, 47 | comment: '内置', 48 | default: () => "'b'1''", 49 | transformer: new BoolBitTransformer(), 50 | }) 51 | readonly: boolean | null; 52 | 53 | @DeleteDateColumn({ 54 | name: 'deleted_at', 55 | nullable: true, 56 | comment: '删除时间', 57 | }) 58 | deletedAt: Date | null; 59 | 60 | @Column('bigint', { name: 'created_by', nullable: true, comment: '创建人id' }) 61 | createdBy: number | null; 62 | 63 | @CreateDateColumn({ 64 | name: 'created_at', 65 | nullable: true, 66 | comment: '创建时间', 67 | }) 68 | createTime: Date | null; 69 | 70 | @Column('bigint', { name: 'updated_by', nullable: true, comment: '更新人id' }) 71 | updatedBy: number | null; 72 | 73 | @UpdateDateColumn({ 74 | name: 'updated_at', 75 | nullable: true, 76 | comment: '更新时间', 77 | }) 78 | updateTime: Date | null; 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/system/helpers.ts: -------------------------------------------------------------------------------- 1 | import { SnowflakeIdv1 } from 'simple-flakeid'; 2 | 3 | const snowflakeIdv1 = new SnowflakeIdv1({ workerId: 1 }); 4 | 5 | export const getSnowflakeId = () => { 6 | return snowflakeIdv1.NextNumber(); 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/system/repositories/area.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseTreeRepository } from '@/modules/database/base/tree.repository'; 2 | import { OrderType, TreeChildrenResolve } from '@/modules/database/constants'; 3 | import { CustomRepository } from '@/modules/database/decorators'; 4 | 5 | import { AreaEntity } from '../entities'; 6 | 7 | @CustomRepository(AreaEntity) 8 | export class AreaRepository extends BaseTreeRepository { 9 | protected _qbName = 'area'; 10 | 11 | protected orderBy = { name: 'sortValue', order: OrderType.ASC }; 12 | 13 | protected _childrenResolve = TreeChildrenResolve.DELETE; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/system/repositories/dictionary.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '@/modules/database/base'; 2 | import { CustomRepository } from '@/modules/database/decorators'; 3 | 4 | import { DictionaryEntity } from '../entities'; 5 | 6 | @CustomRepository(DictionaryEntity) 7 | export class DictionaryRepository extends BaseRepository { 8 | protected _qbName = 'dict'; 9 | 10 | buildBaseQB() { 11 | return this.createQueryBuilder(this.qbName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/system/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dictionary.repository'; 2 | export * from './parameter.repository'; 3 | export * from './area.repository'; 4 | export * from './menu.repository'; 5 | export * from './resource.repository'; 6 | -------------------------------------------------------------------------------- /src/modules/system/repositories/menu.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseTreeRepository } from '@/modules/database/base/tree.repository'; 2 | import { OrderType, TreeChildrenResolve } from '@/modules/database/constants'; 3 | import { CustomRepository } from '@/modules/database/decorators'; 4 | 5 | import { MenuEntity } from '../entities/menu.entity'; 6 | 7 | @CustomRepository(MenuEntity) 8 | export class MenuRepository extends BaseTreeRepository { 9 | protected _qbName = 'org'; 10 | 11 | protected orderBy = { name: 'sortValue', order: OrderType.ASC }; 12 | 13 | protected _childrenResolve = TreeChildrenResolve.UP; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/system/repositories/parameter.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '@/modules/database/base'; 2 | import { CustomRepository } from '@/modules/database/decorators'; 3 | 4 | import { ParameterEntity } from '../entities'; 5 | 6 | @CustomRepository(ParameterEntity) 7 | export class ParameterRepository extends BaseRepository { 8 | protected _qbName = 'param'; 9 | 10 | buildBaseQB() { 11 | return this.createQueryBuilder(this.qbName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/system/repositories/resource.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '@/modules/database/base'; 2 | import { CustomRepository } from '@/modules/database/decorators'; 3 | 4 | import { ResourceEntity } from '../entities'; 5 | 6 | @CustomRepository(ResourceEntity) 7 | export class ResourceRepository extends BaseRepository { 8 | protected _qbName = 'resource'; 9 | 10 | buildBaseQB() { 11 | return this.createQueryBuilder(this.qbName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/system/services/area.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { isNil, omit } from 'lodash'; 4 | import { SelectQueryBuilder, EntityNotFoundError } from 'typeorm'; 5 | 6 | import { BaseService } from '@/modules/database/base'; 7 | 8 | import { SelectTrashMode } from '@/modules/database/constants'; 9 | import { PublicOrderType } from '@/modules/system/constants'; 10 | 11 | import { QueryAreaTreeDto, CreateAreaDto, UpdateAreaDto } from '../dtos'; 12 | import { AreaEntity } from '../entities'; 13 | import { AreaRepository } from '../repositories'; 14 | 15 | /** 16 | * 地区数据操作 17 | */ 18 | @Injectable() 19 | export class AreaService extends BaseService { 20 | protected enableTrash = false; 21 | 22 | constructor(protected repository: AreaRepository) { 23 | super(repository); 24 | } 25 | 26 | /** 27 | * 查询地区树 28 | */ 29 | async findTrees(options: QueryAreaTreeDto) { 30 | const { trashed = SelectTrashMode.NONE } = options; 31 | return this.repository.findTrees({ 32 | withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY, 33 | onlyTrashed: trashed === SelectTrashMode.ONLY, 34 | }); 35 | } 36 | 37 | /** 38 | * 新建地区 39 | * @param data 40 | */ 41 | async create(data: CreateAreaDto) { 42 | // 获取通用参数 43 | const createParams = await super.create(data); 44 | // 执行插入 45 | return this.repository.save({ 46 | ...createParams, 47 | // 传入父地区 48 | parent: await this.getParent(undefined, data.parent), 49 | }); 50 | } 51 | 52 | /** 53 | * 更新地区 54 | * @param data 55 | */ 56 | async update(data: UpdateAreaDto) { 57 | const parent = await this.getParent(data.id, data.parent); 58 | const querySet = omit(data, ['id', 'parent']); 59 | if (Object.keys(querySet).length > 0) { 60 | await this.repository.update(data.id, querySet); 61 | } 62 | const cat = await this.detail(data.id); 63 | const shouldUpdateParent = 64 | (!isNil(cat.parent) && !isNil(parent) && cat.parent.id !== parent.id) || 65 | (isNil(cat.parent) && !isNil(parent)) || 66 | (!isNil(cat.parent) && isNil(parent)); 67 | // 父分类单独更新 68 | if (parent !== undefined && shouldUpdateParent) { 69 | cat.parent = parent; 70 | await this.repository.save(cat); 71 | } 72 | return cat; 73 | } 74 | 75 | /** 76 | * 获取请求传入的父地区 77 | * @param current 当前地区的ID 78 | * @param id 79 | */ 80 | protected async getParent(current?: string, id?: string) { 81 | if (current === id) return undefined; 82 | let parent: AreaEntity | undefined; 83 | if (id !== undefined) { 84 | if (id === null) return null; 85 | parent = await this.repository.findOne({ where: { id } }); 86 | if (!parent) 87 | throw new EntityNotFoundError(AreaEntity, `Parent category ${id} not exists!`); 88 | } 89 | return parent; 90 | } 91 | 92 | /** 93 | * 对地区进行排序的Query构建 94 | * @param qb 95 | * @param orderBy 排序方式 96 | */ 97 | protected addOrderByQuery(qb: SelectQueryBuilder, orderBy?: PublicOrderType) { 98 | const queryName = this.repository.qbName; 99 | switch (orderBy) { 100 | case PublicOrderType.CREATED: 101 | return qb.orderBy(`${queryName}.created_at`, 'DESC'); 102 | case PublicOrderType.UPDATED: 103 | return qb.orderBy(`${queryName}.updated_at`, 'DESC'); 104 | default: 105 | return qb.orderBy(`${queryName}.id`, 'ASC'); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/modules/system/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dictionary.service'; 2 | export * from './parameter.service'; 3 | export * from './area.service'; 4 | export * from './menu.service'; 5 | export * from './resource.service'; 6 | -------------------------------------------------------------------------------- /src/modules/system/services/menu.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { isNil, omit } from 'lodash'; 4 | import { SelectQueryBuilder, EntityNotFoundError } from 'typeorm'; 5 | 6 | import { BaseService } from '@/modules/database/base'; 7 | 8 | import { SelectTrashMode } from '@/modules/database/constants'; 9 | import { PublicOrderType } from '@/modules/system/constants'; 10 | 11 | import { QueryMenuTreeDto, CreateMenuDto, UpdateMenuDto } from '../dtos'; 12 | import { MenuEntity } from '../entities'; 13 | import { MenuRepository } from '../repositories'; 14 | 15 | /** 16 | * 菜单数据操作 17 | */ 18 | @Injectable() 19 | export class MenuService extends BaseService { 20 | protected enableTrash = false; 21 | 22 | constructor(protected repository: MenuRepository) { 23 | super(repository); 24 | } 25 | 26 | /** 27 | * 树查询 28 | */ 29 | async findTrees(options: QueryMenuTreeDto) { 30 | const { trashed = SelectTrashMode.NONE } = options; 31 | return this.repository.findTrees({ 32 | withTrashed: trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY, 33 | onlyTrashed: trashed === SelectTrashMode.ONLY, 34 | }); 35 | } 36 | 37 | /** 38 | * 新建 39 | */ 40 | async create(data: CreateMenuDto) { 41 | const createParams = await super.create(data); 42 | // 执行插入 43 | return this.repository.save({ 44 | ...createParams, 45 | // 传入父机构 46 | parent: await this.getParent(undefined, data.parent), 47 | }); 48 | } 49 | 50 | /** 51 | * 更新 52 | */ 53 | async update(data: UpdateMenuDto) { 54 | const parent = await this.getParent(data.id, data.parent); 55 | const querySet = omit(data, ['id', 'parent']); 56 | if (Object.keys(querySet).length > 0) { 57 | await this.repository.update(data.id, querySet); 58 | } 59 | const cat = await this.detail(data.id); 60 | const shouldUpdateParent = 61 | (!isNil(cat.parent) && !isNil(parent) && cat.parent.id !== parent.id) || 62 | (isNil(cat.parent) && !isNil(parent)) || 63 | (!isNil(cat.parent) && isNil(parent)); 64 | // 父分类单独更新 65 | if (parent !== undefined && shouldUpdateParent) { 66 | cat.parent = parent; 67 | await this.repository.save(cat); 68 | } 69 | return cat; 70 | } 71 | 72 | /** 73 | * 获取请求传入的父菜单 74 | * @param current 当前菜单的ID 75 | * @param id 76 | */ 77 | protected async getParent(current?: string, id?: string) { 78 | if (current === id) return undefined; 79 | let parent: MenuEntity | undefined; 80 | if (id !== undefined) { 81 | if (id === null) return null; 82 | parent = await this.repository.findOne({ where: { id } }); 83 | if (!parent) 84 | throw new EntityNotFoundError(MenuEntity, `Parent category ${id} not exists!`); 85 | } 86 | return parent; 87 | } 88 | 89 | /** 90 | * 进行排序的Query构建 91 | * @param qb 92 | * @param orderBy 排序方式 93 | */ 94 | protected addOrderByQuery(qb: SelectQueryBuilder, orderBy?: PublicOrderType) { 95 | const queryName = this.repository.qbName; 96 | switch (orderBy) { 97 | case PublicOrderType.CREATED: 98 | return qb.orderBy(`${queryName}.created_at`, 'DESC'); 99 | case PublicOrderType.UPDATED: 100 | return qb.orderBy(`${queryName}.updated_at`, 'DESC'); 101 | default: 102 | return qb.orderBy(`${queryName}.id`, 'ASC'); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/modules/system/services/parameter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotAcceptableException } from '@nestjs/common'; 2 | import { isEmpty, isNil, omit } from 'lodash'; 3 | 4 | import { SelectQueryBuilder } from 'typeorm'; 5 | 6 | import { BaseService } from '@/modules/database/base'; 7 | 8 | import { QueryHook } from '@/modules/database/types'; 9 | import { PublicOrderType } from '@/modules/system/constants'; 10 | 11 | import { QueryParameterDto, CreateParameterDto, UpdateParameterDto } from '../dtos'; 12 | import { ParameterEntity } from '../entities'; 13 | import { ParameterRepository } from '../repositories'; 14 | 15 | // 系统参数查询接口 16 | type FindParams = { 17 | [key in keyof Omit]: QueryParameterDto[key]; 18 | }; 19 | 20 | /** 21 | * 系统参数数据操作 22 | */ 23 | @Injectable() 24 | export class ParameterService extends BaseService< 25 | ParameterEntity, 26 | ParameterRepository, 27 | FindParams 28 | > { 29 | constructor(protected repository: ParameterRepository) { 30 | super(repository); 31 | } 32 | 33 | /** 34 | * 新建系统参数 35 | * @param data 36 | */ 37 | async create(data: CreateParameterDto) { 38 | const createParams = await super.create(data); 39 | // 先判断参数键是否重复 40 | const qb = await super.buildListQB(this.repository.buildBaseQB(), createParams); 41 | const count = await qb.where({ key: createParams.key }).getCount(); 42 | if (count > 0) { 43 | throw new NotAcceptableException(`parameter key [${createParams.key}] is repeated`); 44 | } 45 | // 判断后再执行插入 46 | return this.repository.save(createParams); 47 | } 48 | 49 | /** 50 | * 更新系统参数 51 | * @param data 52 | */ 53 | async update(data: UpdateParameterDto) { 54 | await this.repository.update(data.id, omit(data, ['id', 'key'])); 55 | return this.detail(data.id); 56 | } 57 | 58 | /** 59 | * 构建参数列表查询器 60 | * @param queryBuilder 初始查询构造器 61 | * @param options 排查分页选项后的查询选项 62 | * @param callback 添加额外的查询 63 | */ 64 | protected async buildListQB( 65 | queryBuilder: SelectQueryBuilder, 66 | options: FindParams, 67 | callback?: QueryHook, 68 | ) { 69 | // 调用父类通用qb处理方法 70 | const qb = await super.buildListQB(queryBuilder, options, callback); 71 | // 子类自我实现 72 | const { orderBy, key, value, name, timeRange } = options; 73 | const queryName = this.repository.qbName; 74 | // 对几个可选参数的where判断 75 | if (!isEmpty(key)) { 76 | qb.andWhere(`${queryName}.key like '%${key}%'`); 77 | } 78 | if (!isEmpty(value)) { 79 | qb.andWhere(`${queryName}.value like '%${value}%'`); 80 | } 81 | if (!isEmpty(name)) { 82 | qb.andWhere(`${queryName}.name like '%${name}%'`); 83 | } 84 | if (!isNil(timeRange)) { 85 | qb.andWhere(`${queryName}.created_at between ${timeRange[0]} and ${timeRange[1]}`); 86 | } 87 | // 排序 88 | this.addOrderByQuery(qb, orderBy); 89 | return qb; 90 | } 91 | 92 | /** 93 | * 对系统参数进行排序的Query构建 94 | * @param qb 95 | * @param orderBy 排序方式 96 | */ 97 | protected addOrderByQuery(qb: SelectQueryBuilder, orderBy?: PublicOrderType) { 98 | const queryName = this.repository.qbName; 99 | switch (orderBy) { 100 | case PublicOrderType.CREATED: 101 | return qb.orderBy(`${queryName}.created_at`, 'DESC'); 102 | case PublicOrderType.UPDATED: 103 | return qb.orderBy(`${queryName}.updated_at`, 'DESC'); 104 | default: 105 | return qb.orderBy(`${queryName}.id`, 'ASC'); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/modules/system/services/resource.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { isEmpty, isNil, omit } from 'lodash'; 3 | 4 | import { SelectQueryBuilder } from 'typeorm'; 5 | 6 | import { BaseService } from '@/modules/database/base'; 7 | 8 | import { QueryHook } from '@/modules/database/types'; 9 | import { PublicOrderType } from '@/modules/system/constants'; 10 | 11 | import { CreateResourceDto, QueryResourceDto, UpdateResourceDto } from '../dtos'; 12 | import { ResourceEntity } from '../entities'; 13 | import { ResourceRepository } from '../repositories'; 14 | 15 | /** 16 | * 系统参数数据操作 17 | */ 18 | @Injectable() 19 | export class ResourceService extends BaseService { 20 | constructor(protected repository: ResourceRepository) { 21 | super(repository); 22 | } 23 | 24 | /** 25 | * 新建系统参数 26 | * @param data 27 | */ 28 | async create(data: CreateResourceDto) { 29 | // 获取通用参数 30 | const createParams = await super.create(data); 31 | // 执行插入 32 | return this.repository.save(createParams); 33 | } 34 | 35 | /** 36 | * 更新系统参数 37 | * @param data 38 | */ 39 | async update(data: UpdateResourceDto) { 40 | await this.repository.update(data.id, omit(data, ['id', 'code'])); 41 | return this.detail(data.id); 42 | } 43 | 44 | /** 45 | * 构建参数列表查询器 46 | * @param queryBuilder 初始查询构造器 47 | * @param options 排查分页选项后的查询选项 48 | * @param callback 添加额外的查询 49 | */ 50 | protected async buildListQB( 51 | queryBuilder: SelectQueryBuilder, 52 | options: QueryResourceDto, 53 | callback?: QueryHook, 54 | ) { 55 | // 调用父类通用qb处理方法 56 | const qb = await super.buildListQB(queryBuilder, options, callback); 57 | // 子类自我实现 58 | const { orderBy, menuId, code, name, timeRange } = options; 59 | const queryName = this.repository.qbName; 60 | // 对几个可选参数的where判断 61 | if (!isEmpty(menuId)) { 62 | qb.andWhere(`${queryName}.menu_id = ${menuId}`); 63 | } 64 | if (!isEmpty(code)) { 65 | qb.andWhere(`${queryName}.code like '%${code}%'`); 66 | } 67 | if (!isEmpty(name)) { 68 | qb.andWhere(`${queryName}.name like '%${name}%'`); 69 | } 70 | if (!isNil(timeRange)) { 71 | qb.andWhere(`${queryName}.created_at between ${timeRange[0]} and ${timeRange[1]}`); 72 | } 73 | // 排序 74 | this.addOrderByQuery(qb, orderBy); 75 | return qb; 76 | } 77 | 78 | /** 79 | * 对系统参数进行排序的Query构建 80 | * @param qb 81 | * @param orderBy 排序方式 82 | */ 83 | protected addOrderByQuery(qb: SelectQueryBuilder, orderBy?: PublicOrderType) { 84 | const queryName = this.repository.qbName; 85 | switch (orderBy) { 86 | case PublicOrderType.CREATED: 87 | return qb.orderBy(`${queryName}.created_at`, 'DESC'); 88 | case PublicOrderType.UPDATED: 89 | return qb.orderBy(`${queryName}.updated_at`, 'DESC'); 90 | default: 91 | return qb.orderBy(`${queryName}.id`, 'ASC'); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/modules/system/system.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { DatabaseModule } from '../database/database.module'; 5 | 6 | import * as controllers from './controllers'; 7 | import * as entities from './entities'; 8 | import * as repositories from './repositories'; 9 | import * as services from './services'; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forFeature(Object.values(entities)), 14 | DatabaseModule.forRepository(Object.values(repositories)), 15 | ], 16 | controllers: Object.values(controllers), 17 | providers: [...Object.values(services)], 18 | exports: [ 19 | ...Object.values(services), 20 | DatabaseModule.forRepository(Object.values(repositories)), 21 | ], 22 | }) 23 | export class SystemModule {} 24 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare type RecordAny = Record; 2 | declare type RecordNever = Record; 3 | declare type RecordAnyOrNever = RecordAny | RecordNever; 4 | /** 5 | * 类转义为普通对象后的类型 6 | */ 7 | declare type ClassToPlain = { [key in keyof T]: T[key] }; 8 | 9 | /** 10 | * 一个类的类型 11 | */ 12 | declare type ClassType = { new (...args: any[]): T }; 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "**.js", "*.json"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "declaration": true, 8 | "removeComments": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "alwaysStrict": true, 12 | "sourceMap": true, 13 | "incremental": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "isolatedModules": true, 16 | "esModuleInterop": true, 17 | "noUnusedLocals": true, 18 | "noImplicitReturns": true, 19 | "strictNullChecks": false, 20 | "strictBindCallApply": false, 21 | "noFallthroughCasesInSwitch": true, 22 | "allowSyntheticDefaultImports": true, 23 | "pretty": true, 24 | "resolveJsonModule": true, 25 | "skipLibCheck": true, 26 | "noImplicitAny": true, 27 | "allowJs": true, 28 | "importsNotUsedAsValues": "remove", 29 | "noEmit": false, 30 | "lib": ["esnext", "DOM", "ScriptHost", "WebWorker"], 31 | "baseUrl": ".", 32 | "outDir": "./dist", 33 | "paths": { 34 | "@/*": ["./src/*"] 35 | } 36 | }, 37 | "include": ["src", "test", "typings/**/*.d.ts", "**.js", "*.json"] 38 | } 39 | --------------------------------------------------------------------------------