├── .codebeatignore ├── .codecov.yml ├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .gitpod.yml ├── .mocharc.yml ├── .nycrc.json ├── .prettierrc.js ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── bootstrap.js ├── commitlint.config.js ├── database └── init.sql ├── docker-compose.yml ├── f.yml ├── jest.config.js ├── package.json ├── sonar-project.properties ├── src ├── app │ ├── controller │ │ ├── admin │ │ │ ├── menu.ts │ │ │ ├── permission.ts │ │ │ ├── role.ts │ │ │ └── user.ts │ │ ├── auth.ts │ │ └── home.ts │ ├── dto │ │ ├── admin │ │ │ ├── menu.ts │ │ │ ├── permission.ts │ │ │ ├── role.ts │ │ │ └── user.ts │ │ └── auth.ts │ ├── extend │ │ └── helper.ts │ ├── middleware │ │ ├── error-handler.ts │ │ ├── jwt-auth.ts │ │ └── request-id.ts │ ├── model │ │ ├── admin-menu.ts │ │ ├── admin-permission.ts │ │ ├── admin-role.ts │ │ ├── admin-user.ts │ │ └── base.ts │ ├── service │ │ ├── admin │ │ │ ├── menu.ts │ │ │ ├── permission.ts │ │ │ ├── role.ts │ │ │ └── user.ts │ │ ├── auth.ts │ │ └── rabbitmq.ts │ └── util │ │ ├── common.ts │ │ ├── custom-logger.ts │ │ └── my-error.ts ├── config │ ├── config.default.ts │ ├── config.local.ts │ ├── config.prod.ts │ ├── config.types.ts │ ├── config.unittest.ts │ └── plugin.ts ├── configuration.ts ├── interface.ts └── typings │ └── meeko.ts ├── template.yaml ├── test ├── controller │ ├── admin │ │ ├── menu.test.ts │ │ ├── permission.test.ts │ │ ├── role.test.ts │ │ └── user.test.ts │ ├── auth-invalid.test.ts │ ├── auth-valid.test.ts │ └── home.test.ts ├── middleware │ ├── error-handler.test.ts │ └── request-id.test.ts ├── root.config.ts ├── root.hooks.ts ├── service │ ├── auth.test.ts │ ├── menu.test.ts │ ├── permission.test.ts │ ├── rabbitmq.test.ts │ ├── role.test.ts │ └── user.test.ts └── util │ ├── common.test.ts │ ├── custom-logger.test.ts │ └── util.ts ├── tsconfig.json └── typings ├── app ├── extend │ └── helper.d.ts └── index.d.ts └── config ├── index.d.ts └── plugin.d.ts /.codebeatignore: -------------------------------------------------------------------------------- 1 | src/app/dto/**/* -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | default: 4 | target: 90% 5 | threshold: 3% 6 | if_ci_failed: error 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | coverage/ 8 | dist/ 9 | .idea/ 10 | run/ 11 | .DS_Store 12 | *.sw* 13 | *.un~ 14 | .tsbuildinfo 15 | .tsbuildinfo.* 16 | docs 17 | .docker/ 18 | .serverless 19 | .git/ 20 | database/ 21 | template.yaml -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 🎨 editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./node_modules/mwts/"], 3 | "plugins": ["unicorn", "import"], 4 | "ignorePatterns": [ 5 | "node_modules", 6 | "dist", 7 | "test", 8 | "jest.config.js", 9 | "typings" 10 | ], 11 | "env": { 12 | "jest": true 13 | }, 14 | "rules": { 15 | "node/no-extraneous-import": [ 16 | "error", 17 | { 18 | "allowModules": ["typeorm", "ioredis", "@midwayjs/core"] 19 | } 20 | ], 21 | "unicorn/filename-case": 2, 22 | "import/order": [ 23 | 1, 24 | { 25 | "groups": [ 26 | "builtin", 27 | "external", 28 | "internal", 29 | "parent", 30 | "sibling", 31 | "index" 32 | ], 33 | "pathGroups": [ 34 | { 35 | "pattern": "@/**", 36 | "group": "external", 37 | "position": "after" 38 | } 39 | ], 40 | "pathGroupsExcludedImportTypes": ["builtin"], 41 | "newlines-between": "always" 42 | } 43 | ], 44 | "no-console": [2, { "allow": ["warn", "error", "info"] } ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | # Docker Hub image that `container-job` executes in 18 | container: node:10.18-jessie 19 | 20 | # Service containers to run with `container-job` 21 | services: 22 | # Label used to access the service container 23 | redis: 24 | # Docker Hub image 25 | image: redis 26 | # Set health checks to wait until redis has started 27 | options: >- 28 | --health-cmd "redis-cli ping" 29 | --health-interval 10s 30 | --health-timeout 5s 31 | --health-retries 5 32 | mysql: 33 | image: mysql:5.7 34 | options: >- 35 | --health-cmd="mysqladmin ping" 36 | --health-interval=10s 37 | --health-timeout=5s 38 | --health-retries=3 39 | env: 40 | MYSQL_ROOT_PASSWORD: password 41 | MYSQL_DATABASE: shop 42 | jaeger: 43 | image: jaegertracing/all-in-one 44 | ports: 45 | - 6832:6832/udp 46 | - 16686:16686 47 | rabbitmq: 48 | image: rabbitmq:3.8.18-beta.1 49 | ports: 50 | - 5672:5672 51 | - 15672:15672 52 | strategy: 53 | matrix: 54 | node-version: [14.x] 55 | os: [ubuntu-latest] 56 | 57 | steps: 58 | - uses: actions/checkout@v2 59 | 60 | - name: Use Node.js ${{ matrix.node-version }} 61 | uses: actions/setup-node@v1 62 | with: 63 | node-version: ${{ matrix.node-version }} 64 | 65 | - uses: gtadam/mysql-deploy-action@v1 66 | with: 67 | DATABASE_HOST: mysql 68 | DATABASE_PORT: 3306 69 | DATABASE_NAME: shop 70 | DATABASE_USERNAME: root 71 | DATABASE_PASSWORD: password 72 | DATABASE_SCRIPT: ./database/init.sql 73 | 74 | - name: Cache node modules 75 | uses: actions/cache@v2 76 | env: 77 | cache-name: cache-node-modules 78 | with: 79 | # npm cache files are stored in `~/.npm` on Linux/macOS 80 | path: ~/.npm 81 | key: ${{ runner.os }}-${{ matrix.node-version }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} 82 | restore-keys: | 83 | ${{ runner.os }}-${{ matrix.node-version }}-build-${{ env.cache-name }}- 84 | ${{ runner.os }}-${{ matrix.node-version }}-build- 85 | ${{ runner.os }}-${{ matrix.node-version }}- 86 | 87 | - name: Install Dependencies 88 | run: npm install 89 | 90 | - name: Run Tests 91 | # run: npm run ci 92 | run: npm run cov:mocha 93 | env: 94 | CI: true 95 | # mysql 96 | MYSQL_HOST: mysql 97 | MYSQL_PORT: 3306 98 | MYSQL_USER: root 99 | MYSQL_PASSWORD: password 100 | MYSQL_DATABASE: shop 101 | # redis 102 | REDIS_HOST: redis 103 | REDIS_PORT: 6379 104 | - name: Upload coverage to Codecov 105 | uses: codecov/codecov-action@v1 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | coverage/ 8 | dist/ 9 | .idea/ 10 | run/ 11 | .DS_Store 12 | *.sw* 13 | *.un~ 14 | .tsbuildinfo 15 | .tsbuildinfo.* 16 | docs 17 | .docker/ 18 | .serverless 19 | .fun -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | github: 2 | prebuilds: 3 | # enable for the master/default branch (defaults to true) 4 | master: true 5 | # enable for all branches in this repo (defaults to false) 6 | branches: true 7 | # enable for pull requests coming from this repo (defaults to true) 8 | pullRequests: true 9 | # enable for pull requests coming from forks (defaults to false) 10 | pullRequestsFromForks: true 11 | # add a "Review in Gitpod" button as a comment to pull requests (defaults to true) 12 | addComment: true 13 | # add a "Review in Gitpod" button to pull requests (defaults to false) 14 | addBadge: false 15 | # add a label once the prebuild is ready to pull requests (defaults to false) 16 | addLabel: prebuilt-in-gitpod 17 | tasks: 18 | - init: docker-compose up -d && npm install -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | 2 | bail: true 3 | full-trace: true 4 | require: 5 | - intelli-espower-loader 6 | - espower-typescript/guess 7 | - test/root.hooks.ts 8 | spec: 9 | - test/**/*.test.ts 10 | timeout: 60000 11 | ui: bdd 12 | parallel: true 13 | exit: true 14 | 15 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/*.ts", 4 | "src/**/*.ts" 5 | ], 6 | "exclude": [ 7 | ".githooks", 8 | "node_modules*", 9 | "**/src/bin", 10 | "**/src/domain.ts", 11 | "**/src/interface.ts", 12 | "**/dist", 13 | "**/node_modules*", 14 | "**/test", 15 | "**/test*", 16 | "**/*.d.ts", 17 | "**/*.js" 18 | ], 19 | "reporter": [ 20 | "html", 21 | "json", 22 | "text", 23 | "text-summary" 24 | ], 25 | "all": true 26 | } 27 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('mwts/.prettierrc.json') 3 | } 4 | -------------------------------------------------------------------------------- /.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": "Midway Debug", 9 | "type": "node", 10 | "autoAttachChildProcesses": true, 11 | "console": "integratedTerminal", 12 | "env": { 13 | "NODE_ENV": "local" 14 | }, 15 | "port": 9229, 16 | // "preLaunchTask": "TypeScript compile", 17 | "protocol": "auto", 18 | "request": "launch", 19 | "restart": true, 20 | "runtimeArgs": [ 21 | "run", 22 | "debug", 23 | "--", 24 | "--inspect-brk" 25 | ], 26 | "runtimeExecutable": "npm", 27 | "skipFiles": [ 28 | // "${workspaceFolder}/node_modules/**/*.js", 29 | "${workspaceFolder}/node_modules/rxjs/**/*.js", 30 | "/**/*.js" 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "logs": true, 4 | "run": true, 5 | "package-lock.json": false, 6 | "node_modules": false, 7 | "coverage": true, 8 | "dist": false 9 | }, 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": true 12 | }, 13 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.3.0](https://github.com/fsd-nodejs/service-mw2/compare/v1.2.0...v1.3.0) (2021-09-13) 6 | 7 | 8 | ### Features 9 | 10 | * **jwt:** update jwt plugin ([#93](https://github.com/fsd-nodejs/service-mw2/issues/93)) ([6d378b0](https://github.com/fsd-nodejs/service-mw2/commit/6d378b0af383c5889f67adfac53f872390547688)) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * rename HOST to PORT ([#101](https://github.com/fsd-nodejs/service-mw2/issues/101)) ([1c1e8ec](https://github.com/fsd-nodejs/service-mw2/commit/1c1e8ecf2a2d96868cd047e21b890b31ff1579f1)) 16 | 17 | ## [1.2.0](https://github.com/fsd-nodejs/service-mw2/compare/v1.1.0...v1.2.0) (2021-06-23) 18 | 19 | 20 | ### Features 21 | 22 | * 增加 rabbitmq 生产者支持 ([8cab1ea](https://github.com/fsd-nodejs/service-mw2/commit/8cab1eae8b9be556d51b1de85f758d36f76137db)) 23 | * add docker support for development dependencies ([67379ab](https://github.com/fsd-nodejs/service-mw2/commit/67379abd891c8dfa1780248b5403e73377b976b4)) 24 | * add gitpod dockerfile config ([bdb6523](https://github.com/fsd-nodejs/service-mw2/commit/bdb65232ad6bf6c166c98317cbd59726fdc3673f)) 25 | * support on rabbitmq ([55bf7cb](https://github.com/fsd-nodejs/service-mw2/commit/55bf7cb8b67829896b08cf04b9c87433ec3287d6)) 26 | * **koid:** using midway-component-koid instead of egg plugin of koid ([b60d029](https://github.com/fsd-nodejs/service-mw2/commit/b60d029244200f9102d2e5611197255fa18f3a7e)) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * config.unittest.ts of jaeger config ([7e1cdf4](https://github.com/fsd-nodejs/service-mw2/commit/7e1cdf44964a22292ff3ca0eb5a7fa32480e92e3)) 32 | * deamon exec gitpod ([af062a2](https://github.com/fsd-nodejs/service-mw2/commit/af062a21b7f1e3d667613d1f473578b742ad7e35)) 33 | 34 | ## 1.1.0 (2021-06-08) 35 | 36 | 37 | ### Features 38 | 39 | * **apm:** change ReturnType of processPriority() to number | undefined ([d785614](https://github.com/fsd-nodejs/service-mw2/commit/d785614a77faae83fb117c1909cafa3847037788)) 40 | * **apm:** define const TraceHeaderKey ([dad082e](https://github.com/fsd-nodejs/service-mw2/commit/dad082e3ad2c2bdfde571dc4ea4567a8db60beeb)) 41 | * **apm:** sample with more information ([81d93ce](https://github.com/fsd-nodejs/service-mw2/commit/81d93ce747faefeab109fbdc5af3341c5af586a1)) 42 | * **apm:** update default sample configuration ([74e5b7f](https://github.com/fsd-nodejs/service-mw2/commit/74e5b7f1565904a1e6897240851c7b254f4638b3)) 43 | * **apm:** update property TracerManager ([aab487b](https://github.com/fsd-nodejs/service-mw2/commit/aab487bc73cdc0dde124144cabac332ef486f919)) 44 | * **apm:** update TracerConfig['whiteList'] accepting RegExp ([3b1a635](https://github.com/fsd-nodejs/service-mw2/commit/3b1a6350f2b173014c26fb54d0687ad99e4e7ca1)) 45 | * **apm:** update types ([442cb52](https://github.com/fsd-nodejs/service-mw2/commit/442cb52ad6e3301069ac0bc205b82c09e450b76a)) 46 | * **apm:** use pkg.name as serviceName ([47b629b](https://github.com/fsd-nodejs/service-mw2/commit/47b629bf6af91f817a99ce52fd284d3a537f55c9)) 47 | * **boilerplate:** custom-logger ([41a474a](https://github.com/fsd-nodejs/service-mw2/commit/41a474a50e5abfbc2dd3807db99c48012a11bc86)) 48 | * **deps:** update egg-jwt to v6.0.0 ([7defc6f](https://github.com/fsd-nodejs/service-mw2/commit/7defc6f55ccd059b7195d6fab61ae81541529103)) 49 | * **jaeger:** using midway-component-jaeger ([02f62db](https://github.com/fsd-nodejs/service-mw2/commit/02f62db677b92e94a01bf10aac2a843cb8f4d9be)) 50 | * **koid:** add api `/genid` and more ([03962a0](https://github.com/fsd-nodejs/service-mw2/commit/03962a083ddc45a0204cd49e471bd956b47a706f)) 51 | * **koid:** add snowflake id generator ([272ba28](https://github.com/fsd-nodejs/service-mw2/commit/272ba289011727c6b7ba09e019ab40232d696d16)) 52 | * **koid:** generate bitint id via koid.nextBigint ([8788623](https://github.com/fsd-nodejs/service-mw2/commit/87886231e00544fcd3b39c95bddc84a4cadc1a5b)) 53 | * **middleware, ctx:** jaeger链路追踪 ([4705333](https://github.com/fsd-nodejs/service-mw2/commit/4705333562971f4d9441bce6c45caace133d7bda)) 54 | * **types:** add config.modal.ts ([9a5e64d](https://github.com/fsd-nodejs/service-mw2/commit/9a5e64ddda27342a9783e169d8e02c18da527234)) 55 | * check 'ValidationError' also wit message ([8580b5a](https://github.com/fsd-nodejs/service-mw2/commit/8580b5aba25246585d32f9c7dbf8d61be9e5fe31)) 56 | * create file .gitpod.yml ([6feb1b4](https://github.com/fsd-nodejs/service-mw2/commit/6feb1b48dcc3c8807327dd7afc70f9c31cbec7d2)) 57 | * github workflows ([b2f5bce](https://github.com/fsd-nodejs/service-mw2/commit/b2f5bce4ef9376c7f91c844ae122a469565089a2)) 58 | * jeager链路追踪 ([46843c5](https://github.com/fsd-nodejs/service-mw2/commit/46843c533dd341f60ac53ac261ef7b9ed2977a95)) 59 | * migrate app.ts to configuration.ts ([f923cc6](https://github.com/fsd-nodejs/service-mw2/commit/f923cc60cc8d80b75bde8f91801dd9287d791be8)) 60 | * set response header x-request-id by parseRequestId() ([46ed3a8](https://github.com/fsd-nodejs/service-mw2/commit/46ed3a82ce5eea79b8883ee537f65b419404fb7a)) 61 | * store error in ctx._internalError ([025ec2b](https://github.com/fsd-nodejs/service-mw2/commit/025ec2b43569531b6056a6363171bff1a1229fdb)) 62 | * update git commitlint configurations ([26e6163](https://github.com/fsd-nodejs/service-mw2/commit/26e61632d9327f263c2ef5cda9ff1b7710d51383)) 63 | * update jwt-auth.ts ([2e917fa](https://github.com/fsd-nodejs/service-mw2/commit/2e917fa54da90ea0f6857af7697b4fe769aad85b)) 64 | * **types:** update DefaultConfig ([83c0c82](https://github.com/fsd-nodejs/service-mw2/commit/83c0c824268d0eff10fa0249ee44cf26c2ad0d53)) 65 | * **util:** add retrieveExternalNetWorkInfo() ([d669cc6](https://github.com/fsd-nodejs/service-mw2/commit/d669cc661af4b27d0a594669b8bab5327a7aa5ac)) 66 | * use midway-logger instead of egg-logger ([4c66de5](https://github.com/fsd-nodejs/service-mw2/commit/4c66de53be79ddb03245b704a588ccf860462427)) 67 | * use slice() instead of substring() ([79e3dcc](https://github.com/fsd-nodejs/service-mw2/commit/79e3dcc747410281b9020e20062dd1f568bee96a)) 68 | * 增加commit校验依赖 ([09d52a0](https://github.com/fsd-nodejs/service-mw2/commit/09d52a0b47670161dc0e619f76b20f070ab18757)) 69 | * 增加husky ([f31263d](https://github.com/fsd-nodejs/service-mw2/commit/f31263d91b1e9fbd7528cb92d78fbdf968f1b2a7)) 70 | * 更新依赖版本 ([46b898c](https://github.com/fsd-nodejs/service-mw2/commit/46b898c12a14e732e6515980d7881643fdf47b7f)) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * remove Node.js 10.x version ([a53b4d2](https://github.com/fsd-nodejs/service-mw2/commit/a53b4d2ad0605c7a9294b9b21229bfb942a70c32)) 76 | * scripts.debug ([c9a5f74](https://github.com/fsd-nodejs/service-mw2/commit/c9a5f7451acb91614a82839fe2ae13b1b6741f4f)) 77 | * update trace condition ([3a0b94e](https://github.com/fsd-nodejs/service-mw2/commit/3a0b94e49b2cb96af99e4084ec22977dd9c55f06)) 78 | * var assignment of MyError.status ([0599bc8](https://github.com/fsd-nodejs/service-mw2/commit/0599bc8e565e31a872fdec735a268745ea9a0af2)) 79 | * 修复husky问题 ([e861541](https://github.com/fsd-nodejs/service-mw2/commit/e8615419d1946e16272665e1b8ef9613ef7ca8dd)) 80 | * 修复测测试用例方法使用以及定义 helper 的 this 属性 ([7d4179c](https://github.com/fsd-nodejs/service-mw2/commit/7d4179c6be0f94bbcffe35c83590ce255264caca)) 81 | * 修复测试用例取值问题 ([0db6e72](https://github.com/fsd-nodejs/service-mw2/commit/0db6e724af6ba63f421029a251c70e9cc4996f4f)) 82 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.17.3-buster-slim AS BUILD_IMAGE 2 | 3 | WORKDIR /home 4 | 5 | COPY . . 6 | 7 | RUN npm install --registry=https://registry.npm.taobao.org && \ 8 | npm run build && \ 9 | npm prune --production 10 | 11 | FROM node:14.17.3-alpine3.14 12 | 13 | WORKDIR /home 14 | 15 | COPY --from=BUILD_IMAGE /home /home 16 | 17 | EXPOSE 9000 18 | 19 | ENTRYPOINT ["node", "bootstrap.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 FSD Node.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FSD Service 2 |

3 | FSD Logo 4 |

5 |

FSD (Full Stack Develop) Service - Midway.js 的最佳实践

6 |

7 | codecov 8 | GitHub Actions status 9 | 10 | codebeat badge 11 | Codacy Badge 12 | GitHub license 13 | PRs Welcome 14 | 15 | Gitpod Ready-to-Code 16 |

17 |

18 | Sonar Cloud 19 |

20 | 21 | ## 项目导览 22 | 在这个项目中,你会看到以下基于 Midway 的实践案例 (上层使用 egg.js ) 23 | 24 | 我们正在做以下工程实践例子,大家你遇到什么问题,或者希望追加什么功能,或者学习内部实现。 25 | 26 | 可以关注我们的仓库(点赞,分享...三连)在 issue 留言,我们会征集你的意见,带来最干货的案例。 27 | 28 | 帮你扫清学习障碍,让你用起 Midway 来更加得心应手,提升能效,找回编码的乐趣。 29 | 30 | **所有代码均已经过工程师的测试请放心使用!!!** 31 | 32 | 33 | | 能力栏目 | 名称 | 进度 | 34 | | :------------- | -------------------------------------- | :---: | 35 | | **概述** | | | 36 | | | 控制器(Controller) | ✓ | 37 | | | 服务和注入 | ✓ | 38 | | | 请求、响应和应用 | ✓ | 39 | | | Web 中间件 | ✓ | 40 | | | 启动和部署 | ✓ | 41 | | **基础能力** | | | 42 | | | 依赖注入 | ✓ | 43 | | | 运行环境 | ✓ | 44 | | | 多环境配置 | ✓ | 45 | | | 参数校验和转换 | ✓ | 46 | | | 生命周期 | ✓ | 47 | | | 使用组件 | ✓ | 48 | | | 日志 | ✓ | 49 | | | 本地调试 | ✓ | 50 | | | 测试 | ✓ | 51 | | **增强** | | | 52 | | | 代码流程控制 | | 53 | | | 方法拦截器(切面) | | 54 | | | 缓存(Cache)(目前直接使用 Redis) | ✓ | 55 | | | Database(TypeORM) | ✓ | 56 | | | MongoDB | | 57 | | | Swagger | ✓ | 58 | | **一体化研发** | | | 59 | | | 开发一体化项目 | | 60 | | | 非 Serverless 环境使用一体化 | | 61 | | **Web技术** | | | 62 | | | Cookies | | 63 | | | Session | | 64 | | | 跨域 CORS | ✓ | 65 | | **微服务** | | | 66 | | | gRPC | | 67 | | | RabbitMQ (生产者) | ✓ | 68 | | | RabbitMQ (消费者) | | 69 | | | Consul | | 70 | | **WebSocket** | | | 71 | | | SocketIO | | 72 | | **常用能力** | | | 73 | | | Admin 登录 | ✓ | 74 | | | 普通用户登录-账户密码 | | 75 | | | OAuth 2.0 | | 76 | | | 日志监控 | | 77 | | | 本地上传文件服务 | | 78 | | | 鉴权中间件 | ✓ | 79 | | | 接口响应统计中间件 | ✓ | 80 | | | 统一错误处理 | ✓ | 81 | | | SnowFlake 雪花算法生成分布式ID | ✓ | 82 | | | Jaeger 链路追踪 | ✓ | 83 | | **业务能力** | | | 84 | | | 权限管理 | ✓ | 85 | | | 角色管理 | ✓ | 86 | | | 管理员管理 | ✓ | 87 | | | 菜单管理 | ✓ | 88 | | | 日志(操作日志,记录管理用户的实际操作) | ✓ | 89 | 90 | 91 | 92 | ## 使用项目 93 | 94 | 查看 [Midway docs](https://midwayjs.org/) 获取更多信息. 95 | 96 | 运行该项目需要以下环境支持 97 | - Mysql 98 | - Redis 99 | - Jeager 100 | 101 | 目前该项目已经集成 Docker 环境,按以下步骤可以自动配置以上依赖。[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/) 相关文档请查看这里 102 | 103 | - 1.确保机器已经安装 Docker。 104 | - 2.在项目目录运行 `docker-compose up -d` 105 | - 3.停止服务 `docker-compose down` 106 | 107 | 108 | 如果没有数据库 GUI 工具,可按照下面步骤进行数据库初始化 109 | - `$ docker ps` // 查看容器ID 110 | - `$ docker exec -it /bin/bash` // 进入容器 111 | - `$ mysql shop_development < /data/database/init.sql -uroot -ppassword` // 初始化数据库 112 | 113 | ### Development 114 | 115 | 先将 database 目录下到 sql 文件迁移到数据库,修改默认的config配置文件 116 | 117 | ```bash 118 | $ npm i 119 | $ npm run dev 120 | $ open http://localhost:7001/ 121 | ``` 122 | ## Redis 123 | - 使用 Redis 作为用户登录凭证存取的地方 124 | - RTS 收集统计数据 (开发中) 125 | 126 | ### Redis划分 127 | 128 | 建议使用 Redis 的时候,对所有 key 做好命名空间划分,便于管理。可把 scope 写到对照表中。 129 | 130 | 借助 jwt 插件做签名校验,管理员的 token 中会包含 id 字段。 131 | 132 | 所有 admin 相关的缓存数据都放在 `admin:xxxx` 下面。 133 | 134 | - `admin:accessToken:${id}` 缓存管理员 Token 信息 135 | - `admin:userinfo:${id}` 缓存管理员基本信息 136 | 137 | ## 数据库 138 | 所有实体表均有 deleted_at 字段(目前基础模块不使用软删除),用于软删除。 139 | 140 | 如果要关闭软删除,将deletedAt字段注释即可 141 | 142 | 进行软删除的时候,关系表的数据不做改动。 143 | 144 | 后期根据需要,用脚本定期清理软删除的数据。 145 | 146 | 147 | 148 | 以下模块未使用软删除: 149 | 150 | - 权限管理 151 | - 角色管理 152 | - 菜单管理 153 | - 管理员管理 154 | 155 | ### 查询注意事项 156 | 157 | 业务软删除单独写一个 BaseModel,其他实体继承该 Model 即可 158 | 159 | - 实体查询,继承 `BaseModel` 的实体会自带软删除判断,例子查看`src/app/model/base.ts` 160 | - 在做关系查询的时候,关系表需要手动加软删除判断 IS NULL,如下: 161 | ```typescript 162 | /** 163 | * 根据菜单id获取数据 164 | * @param id 菜单id 165 | */ 166 | async getAdminMenuById(id: string) { 167 | const row = await this.adminMenuModel 168 | .createQueryBuilder() 169 | .select() 170 | .leftJoinAndSelect( 171 | 'AdminMenuModel.roles', 172 | 'role', 173 | 'role.deletedAt IS NULL' 174 | ) 175 | .where({ id: id }) 176 | .getOne(); 177 | return row; 178 | } 179 | ``` 180 | 181 | ## Jaeger 链路追踪 182 | 183 | Jaeger 是 [OpenTracing](https://opentracing.io/docs/) 的一个实现,链路追踪是一个比较复杂的话题,关于 Jaeger 的具体用法请参考[它的官方文档](https://www.jaegertracing.io/docs/1.22/) 184 | 185 | 本实现基于 ctx 机制,结合 midway 的依赖注入可实现无侵入式 spanContext 传递 186 | - 默认实现接口级别的采样 187 | - 若需小颗粒度采样,可手动管理 span 188 | ```ts 189 | ctx.tracerManager.startSpan('SpanName1') 190 | await doSomethine() 191 | ctx.tracerManager.finishSpan() // 别忘记关闭 192 | ``` 193 | 194 | ## 接口响应统计中间件(设计) 195 | 196 | 做接口响应数据统计的出发点,有两点(即使有类似的第三方包,但还是自己实现以下): 197 | - 帮助排查线上接口响应问题 198 | - 监控系统实时状态 199 | 200 | 虽然框架本身已经有日志功能,但是很多场景下,我们可能需要看下各个接口服务的响应状态 201 | 202 | 是在正常服务,还是已经出现问题。在有监控的帮助下,可以快速帮我们定位日志排查问题。 203 | 204 | 是对应统计实时数据而言,这里我们会使用 RTS 的技术方案,会用到 RabbitMQ 和 Redis 205 | 206 | RabbitMQ 作用在于把统计的计算异步化,从而不影响正常的业务请求处理 207 | 208 | (消费者的逻辑代码,需要写在单独一个工程,独立部署) 209 | 210 | 大致流程如下,手绘的,工具简陋,姑且看一下。 211 | ![IMG_5365 HEIC](https://user-images.githubusercontent.com/10667077/101478900-55a4cb00-398c-11eb-97c3-4a41195c572d.JPG) 212 | 213 | 214 | ## Test 单元测试 215 | 216 | 单元测试框架采用 [Mocha](https://mochajs.org/) , 217 | 支持对 case 进行 `.skip()` `.only()` 的随意**组合**以及**快速**实现, 218 | 适合在包含复杂、耦合的业务项目场景 219 | 220 | 注意事项: 221 | - 单测文件不支持 `nullish` 链式操作符,比如 222 | ```ts 223 | assert(res?.body) // 不支持 224 | assert(res && res.body) // 可行 225 | ``` 226 | 227 | 228 | ## 拓展阅读 229 | - 配套的前端工程请移步 https://github.com/fsd-nodejs/pc 查看这个项目 230 | - 全栈开发文档以及规范 https://github.com/fsd-nodejs/document 查看这个项目 231 | - [midway2.x 深度躺坑记(持续更新) 232 | ](https://github.com/fsd-nodejs/service-mw2/wiki/midway2.x-%E6%B7%B1%E5%BA%A6%E8%BA%BA%E5%9D%91%E8%AE%B0(%E6%8C%81%E7%BB%AD%E6%9B%B4%E6%96%B0)) 233 | - 代码提示 [好玩的代码提示 by waitingsong](https://github.com/fsd-nodejs/service-mw2/pull/32) (考虑到,每个人对于自定义代码提示的要求不一样,就不合并到参考了,这个PR推荐给大家) 234 | 235 | 236 | ## 答疑 237 | 238 | 群里会有热心的朋友,也会有新版本发布推送。钉钉扫码加入答疑群 239 | 240 | 241 | 二群 242 | 243 | ![](https://img.alicdn.com/imgextra/i2/O1CN01SRJO0P1YaqxhtPU2X_!!6000000003076-2-tps-305-391.png) 244 | 245 | 246 | 一群(已满) 247 | 248 | ![](https://img.alicdn.com/imgextra/i2/O1CN01ofEEAL2AEpJHbpse5_!!6000000008172-2-tps-311-401.png) 249 | 250 | ## License 251 | 我们的代码使用 [MIT](http://github.com/fsd-nodejs/service-mw2/blob/master/LICENSE) 协议,请放心使用。 252 | 253 | 254 | 255 | -------------------------------------------------------------------------------- /bootstrap.js: -------------------------------------------------------------------------------- 1 | // 如果使用PM2启动这个项目,可以只需 `NODE_ENV=production node bootstrap.js` 2 | // 获取框架 3 | // eslint-disable-next-line import/order 4 | const WebFramework = require('@midwayjs/web').Framework; 5 | // 初始化 web 框架并传入启动参数 6 | const web = new WebFramework().configure({ 7 | port: 9000, 8 | hostname: '0.0.0.0', 9 | }); 10 | 11 | const { Bootstrap } = require('@midwayjs/bootstrap'); 12 | 13 | // 加载框架并执行 14 | Bootstrap.load(web).run(); 15 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /database/init.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : 127.0.0.1 5 | Source Server Type : MySQL 6 | Source Server Version : 80020 7 | Source Host : 127.0.0.1:3306 8 | Source Schema : laravel 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 80020 12 | File Encoding : 65001 13 | 14 | Date: 20/08/2020 17:17:03 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for admin_menu 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `admin_menu`; 24 | CREATE TABLE `admin_menu` ( 25 | `id` int unsigned NOT NULL AUTO_INCREMENT, 26 | `parent_id` int NOT NULL DEFAULT '0', 27 | `order` int NOT NULL DEFAULT '0', 28 | `title` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, 29 | `uri` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 30 | `permission_id` int DEFAULT NULL, 31 | `created_at` timestamp NULL DEFAULT NULL, 32 | `updated_at` timestamp NULL DEFAULT NULL, 33 | PRIMARY KEY (`id`) 34 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 35 | 36 | -- ---------------------------- 37 | -- Records of admin_menu 38 | -- ---------------------------- 39 | BEGIN; 40 | INSERT INTO `admin_menu` VALUES (1, 0, 1, 'Dashboard', '/', NULL, NULL, NULL); 41 | INSERT INTO `admin_menu` VALUES (2, 0, 2, 'Admin', '', NULL, NULL, NULL); 42 | INSERT INTO `admin_menu` VALUES (3, 2, 3, 'Users', 'auth/users', NULL, NULL, NULL); 43 | INSERT INTO `admin_menu` VALUES (4, 2, 4, 'Roles', 'auth/roles', NULL, NULL, NULL); 44 | INSERT INTO `admin_menu` VALUES (5, 2, 5, 'Permission', 'auth/permissions', NULL, NULL, NULL); 45 | INSERT INTO `admin_menu` VALUES (6, 2, 6, 'Menu', 'auth/menu', NULL, NULL, NULL); 46 | INSERT INTO `admin_menu` VALUES (7, 2, 7, 'Operation log', 'auth/logs', NULL, NULL, NULL); 47 | COMMIT; 48 | 49 | -- ---------------------------- 50 | -- Table structure for admin_operation_log 51 | -- ---------------------------- 52 | DROP TABLE IF EXISTS `admin_operation_log`; 53 | CREATE TABLE `admin_operation_log` ( 54 | `id` int unsigned NOT NULL AUTO_INCREMENT, 55 | `user_id` int NOT NULL, 56 | `path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 57 | `method` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL, 58 | `ip` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 59 | `input` text COLLATE utf8mb4_unicode_ci NOT NULL, 60 | `created_at` timestamp NULL DEFAULT NULL, 61 | `updated_at` timestamp NULL DEFAULT NULL, 62 | PRIMARY KEY (`id`), 63 | KEY `admin_operation_log_user_id_index` (`user_id`) 64 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 65 | 66 | -- ---------------------------- 67 | -- Records of admin_operation_log 68 | -- ---------------------------- 69 | BEGIN; 70 | COMMIT; 71 | 72 | -- ---------------------------- 73 | -- Table structure for admin_permissions 74 | -- ---------------------------- 75 | DROP TABLE IF EXISTS `admin_permissions`; 76 | CREATE TABLE `admin_permissions` ( 77 | `id` int unsigned NOT NULL AUTO_INCREMENT, 78 | `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, 79 | `slug` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, 80 | `http_method` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 81 | `http_path` text COLLATE utf8mb4_unicode_ci, 82 | `created_at` timestamp NULL DEFAULT NULL, 83 | `updated_at` timestamp NULL DEFAULT NULL, 84 | PRIMARY KEY (`id`), 85 | UNIQUE KEY `admin_permissions_name_unique` (`name`), 86 | UNIQUE KEY `admin_permissions_slug_unique` (`slug`) 87 | ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 88 | 89 | -- ---------------------------- 90 | -- Records of admin_permissions 91 | -- ---------------------------- 92 | BEGIN; 93 | INSERT INTO `admin_permissions` VALUES (1, 'All permission', '*', '', '*', NULL, NULL); 94 | INSERT INTO `admin_permissions` VALUES (2, 'Dashboard', 'dashboard', 'GET', '/', NULL, NULL); 95 | INSERT INTO `admin_permissions` VALUES (3, 'Login', 'auth.login', '', '/auth/login\r\n/auth/logout', NULL, NULL); 96 | INSERT INTO `admin_permissions` VALUES (4, 'User setting', 'auth.setting', 'GET,PUT', '/auth/setting', NULL, NULL); 97 | INSERT INTO `admin_permissions` VALUES (5, 'Auth management', 'auth.management', '', '/auth/roles\r\n/auth/permissions\r\n/auth/menu\r\n/auth/logs', NULL, NULL); 98 | COMMIT; 99 | 100 | -- ---------------------------- 101 | -- Table structure for admin_role_menu 102 | -- ---------------------------- 103 | DROP TABLE IF EXISTS `admin_role_menu`; 104 | CREATE TABLE `admin_role_menu` ( 105 | `role_id` int NOT NULL, 106 | `menu_id` int NOT NULL, 107 | `created_at` timestamp NULL DEFAULT NULL, 108 | `updated_at` timestamp NULL DEFAULT NULL, 109 | KEY `admin_role_menu_role_id_menu_id_index` (`role_id`,`menu_id`) 110 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 111 | 112 | -- ---------------------------- 113 | -- Records of admin_role_menu 114 | -- ---------------------------- 115 | BEGIN; 116 | INSERT INTO `admin_role_menu` VALUES (1, 2, NULL, NULL); 117 | COMMIT; 118 | 119 | -- ---------------------------- 120 | -- Table structure for admin_role_permissions 121 | -- ---------------------------- 122 | DROP TABLE IF EXISTS `admin_role_permissions`; 123 | CREATE TABLE `admin_role_permissions` ( 124 | `role_id` int NOT NULL, 125 | `permission_id` int NOT NULL, 126 | `created_at` timestamp NULL DEFAULT NULL, 127 | `updated_at` timestamp NULL DEFAULT NULL, 128 | KEY `admin_role_permissions_role_id_permission_id_index` (`role_id`,`permission_id`) 129 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 130 | 131 | -- ---------------------------- 132 | -- Records of admin_role_permissions 133 | -- ---------------------------- 134 | BEGIN; 135 | INSERT INTO `admin_role_permissions` VALUES (1, 1, NULL, NULL); 136 | COMMIT; 137 | 138 | -- ---------------------------- 139 | -- Table structure for admin_role_users 140 | -- ---------------------------- 141 | DROP TABLE IF EXISTS `admin_role_users`; 142 | CREATE TABLE `admin_role_users` ( 143 | `role_id` int NOT NULL, 144 | `user_id` int NOT NULL, 145 | `created_at` timestamp NULL DEFAULT NULL, 146 | `updated_at` timestamp NULL DEFAULT NULL, 147 | KEY `admin_role_users_role_id_user_id_index` (`role_id`,`user_id`) 148 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 149 | 150 | -- ---------------------------- 151 | -- Records of admin_role_users 152 | -- ---------------------------- 153 | BEGIN; 154 | INSERT INTO `admin_role_users` VALUES (1, 1, NULL, NULL); 155 | COMMIT; 156 | 157 | -- ---------------------------- 158 | -- Table structure for admin_roles 159 | -- ---------------------------- 160 | DROP TABLE IF EXISTS `admin_roles`; 161 | CREATE TABLE `admin_roles` ( 162 | `id` int unsigned NOT NULL AUTO_INCREMENT, 163 | `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, 164 | `slug` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, 165 | `created_at` timestamp NULL DEFAULT NULL, 166 | `updated_at` timestamp NULL DEFAULT NULL, 167 | PRIMARY KEY (`id`), 168 | UNIQUE KEY `admin_roles_name_unique` (`name`), 169 | UNIQUE KEY `admin_roles_slug_unique` (`slug`) 170 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 171 | 172 | -- ---------------------------- 173 | -- Records of admin_roles 174 | -- ---------------------------- 175 | BEGIN; 176 | INSERT INTO `admin_roles` VALUES (1, 'Administrator', 'administrator', '2020-08-20 09:14:57', '2020-08-20 09:14:57'); 177 | COMMIT; 178 | 179 | -- ---------------------------- 180 | -- Table structure for admin_user_permissions 181 | -- ---------------------------- 182 | DROP TABLE IF EXISTS `admin_user_permissions`; 183 | CREATE TABLE `admin_user_permissions` ( 184 | `user_id` int NOT NULL, 185 | `permission_id` int NOT NULL, 186 | `created_at` timestamp NULL DEFAULT NULL, 187 | `updated_at` timestamp NULL DEFAULT NULL, 188 | KEY `admin_user_permissions_user_id_permission_id_index` (`user_id`,`permission_id`) 189 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 190 | 191 | -- ---------------------------- 192 | -- Records of admin_user_permissions 193 | -- ---------------------------- 194 | BEGIN; 195 | COMMIT; 196 | 197 | -- ---------------------------- 198 | -- Table structure for admin_users 199 | -- ---------------------------- 200 | DROP TABLE IF EXISTS `admin_users`; 201 | CREATE TABLE `admin_users` ( 202 | `id` int unsigned NOT NULL AUTO_INCREMENT, 203 | `username` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, 204 | `password` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL, 205 | `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 206 | `avatar` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 207 | `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 208 | `created_at` timestamp NULL DEFAULT NULL, 209 | `updated_at` timestamp NULL DEFAULT NULL, 210 | PRIMARY KEY (`id`), 211 | UNIQUE KEY `admin_users_username_unique` (`username`) 212 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 213 | 214 | -- ---------------------------- 215 | -- Records of admin_users 216 | -- ---------------------------- 217 | BEGIN; 218 | INSERT INTO `admin_users` VALUES (1, 'admin', '$2y$10$7gApqiKCdrZ9FZ7pS/4LDuS42THItSoeTvJuslb.KmhyODjSRzj2a', 'Administrator', NULL, NULL, '2020-08-20 09:14:57', '2020-08-20 09:14:57'); 219 | COMMIT; 220 | 221 | -- ---------------------------- 222 | -- Table structure for failed_jobs 223 | -- ---------------------------- 224 | DROP TABLE IF EXISTS `failed_jobs`; 225 | CREATE TABLE `failed_jobs` ( 226 | `id` bigint unsigned NOT NULL AUTO_INCREMENT, 227 | `connection` text COLLATE utf8mb4_unicode_ci NOT NULL, 228 | `queue` text COLLATE utf8mb4_unicode_ci NOT NULL, 229 | `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, 230 | `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL, 231 | `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 232 | PRIMARY KEY (`id`) 233 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 234 | 235 | -- ---------------------------- 236 | -- Records of failed_jobs 237 | -- ---------------------------- 238 | BEGIN; 239 | COMMIT; 240 | 241 | -- ---------------------------- 242 | -- Table structure for migrations 243 | -- ---------------------------- 244 | DROP TABLE IF EXISTS `migrations`; 245 | CREATE TABLE `migrations` ( 246 | `id` int unsigned NOT NULL AUTO_INCREMENT, 247 | `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 248 | `batch` int NOT NULL, 249 | PRIMARY KEY (`id`) 250 | ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 251 | 252 | -- ---------------------------- 253 | -- Records of migrations 254 | -- ---------------------------- 255 | BEGIN; 256 | INSERT INTO `migrations` VALUES (1, '2014_10_12_000000_create_users_table', 1); 257 | INSERT INTO `migrations` VALUES (2, '2014_10_12_100000_create_password_resets_table', 1); 258 | INSERT INTO `migrations` VALUES (3, '2016_01_04_173148_create_admin_tables', 2); 259 | INSERT INTO `migrations` VALUES (4, '2019_08_19_000000_create_failed_jobs_table', 2); 260 | COMMIT; 261 | 262 | -- ---------------------------- 263 | -- Table structure for password_resets 264 | -- ---------------------------- 265 | DROP TABLE IF EXISTS `password_resets`; 266 | CREATE TABLE `password_resets` ( 267 | `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 268 | `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 269 | `created_at` timestamp NULL DEFAULT NULL, 270 | KEY `password_resets_email_index` (`email`) 271 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 272 | 273 | -- ---------------------------- 274 | -- Records of password_resets 275 | -- ---------------------------- 276 | BEGIN; 277 | COMMIT; 278 | 279 | -- ---------------------------- 280 | -- Table structure for users 281 | -- ---------------------------- 282 | DROP TABLE IF EXISTS `users`; 283 | CREATE TABLE `users` ( 284 | `id` bigint unsigned NOT NULL AUTO_INCREMENT, 285 | `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 286 | `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 287 | `email_verified_at` timestamp NULL DEFAULT NULL, 288 | `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 289 | `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 290 | `created_at` timestamp NULL DEFAULT NULL, 291 | `updated_at` timestamp NULL DEFAULT NULL, 292 | PRIMARY KEY (`id`), 293 | UNIQUE KEY `users_email_unique` (`email`) 294 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 295 | 296 | -- ---------------------------- 297 | -- Records of users 298 | -- ---------------------------- 299 | BEGIN; 300 | COMMIT; 301 | 302 | SET FOREIGN_KEY_CHECKS = 1; 303 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redis: 5 | image: redis:6.2.4 6 | ports: 7 | - 6379:6379 8 | healthcheck: 9 | test: ['CMD', 'redis-cli', 'ping'] 10 | interval: 10s 11 | timeout: 5s 12 | retries: 3 13 | volumes: 14 | - ./.docker/redis/data/:/data 15 | 16 | mysql: 17 | image: mysql:5.7 18 | ports: 19 | - 3306:3306 20 | # 如果需要mysql持久化,则打开注释,会保存数据,重启镜像不会丢失 21 | volumes: 22 | - ./.docker/mysql:/var/lib/mysql 23 | - ./database:/data/database # 挂在本地sql文件到容器 24 | environment: 25 | MYSQL_ROOT_PASSWORD: password 26 | MYSQL_DATABASE: shop_development 27 | healthcheck: 28 | test: ['CMD-SHELL', 'mysqladmin ping -uroot -ppassword'] 29 | interval: 10s 30 | timeout: 5s 31 | retries: 3 32 | 33 | jaeger: 34 | image: jaegertracing/all-in-one:1.23.0 35 | ports: 36 | - 6832:6832/udp 37 | - 16686:16686 38 | 39 | rabbitmq: 40 | image: rabbitmq:3.8.18-beta.1-management 41 | ports: 42 | - 5672:5672 43 | - 15672:15672 44 | # 如果需要rabbitmq持久化,则打开注释,会保存数据,重启镜像不会丢失 45 | volumes: 46 | - ./.docker/rabbitmq/data/:/var/lib/rabbitmq 47 | - ./.docker/rabbitmq/:/var/log/rabbitmq 48 | -------------------------------------------------------------------------------- /f.yml: -------------------------------------------------------------------------------- 1 | service: service-mw2 ## 应用发布到云平台的名字,一般指应用名 2 | 3 | provider: 4 | name: aliyun ## 发布的云平台,aliyun,tencent 等 5 | 6 | deployType: egg ## 部署的应用类型 7 | 8 | custom: 9 | customDomain: 10 | domainName: auto -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testPathIgnorePatterns: ['/test/fixtures'], 7 | coveragePathIgnorePatterns: ['/test/'], 8 | moduleNameMapper: { 9 | '^@/(.*)$': '/src/$1', 10 | }, 11 | collectCoverageFrom: ['**/src/**/*.{js,ts}'], 12 | testTimeout: 40000, 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "service-mw2", 3 | "version": "1.3.0", 4 | "description": "Best practices of Midway.js", 5 | "private": true, 6 | "dependencies": { 7 | "@midwayjs/bootstrap": "^2.12.0", 8 | "@waiting/egg-jwt": "^6.2.0", 9 | "@midwayjs/decorator": "^2.13.2", 10 | "@midwayjs/orm": "^2.13.2", 11 | "@midwayjs/swagger": "^1.1.0", 12 | "@midwayjs/web": "^2.13.2", 13 | "@mw-components/jaeger": "^9.0.1", 14 | "@mw-components/jwt": "^9.2.0", 15 | "@mw-components/koid": "^5.0.0", 16 | "@waiting/shared-core": "^14.4.2", 17 | "@waiting/shared-types": "^14.4.2", 18 | "amqp-connection-manager": "^3.2.2", 19 | "amqplib": "^0.8.0", 20 | "bcryptjs": "^2.4.3", 21 | "egg": "^2.30.0", 22 | "egg-redis": "^2.4.0", 23 | "opentracing": "^0.14.5", 24 | "egg-scripts": "^2.15.1", 25 | "meeko": "^1.8.166", 26 | "midway": "^2.13.2", 27 | "moment": "^2.29.1", 28 | "mysql2": "^2.2.5", 29 | "tsconfig-paths": "^3.10.1", 30 | "typeorm": "^0.2.36" 31 | }, 32 | "devDependencies": { 33 | "@commitlint/cli": "^13.1.0", 34 | "@commitlint/config-conventional": "^13.1.0", 35 | "@midwayjs/cli-plugin-faas": "^1.2.77", 36 | "@midwayjs/fcli-plugin-fc": "^1.2.77", 37 | "@types/jest": "^26.0.24", 38 | "@midwayjs/cli": "^1.2.83", 39 | "@midwayjs/egg-ts-helper": "^1.2.1", 40 | "@midwayjs/mock": "^2.13.2", 41 | "@types/amqplib": "^0.8.1", 42 | "@types/mocha": "^9.0.0", 43 | "@types/node": "^16.4.10", 44 | "@types/rewire": "^2.5.28", 45 | "c8": "^7.10.0", 46 | "commitizen": "^4.2.4", 47 | "cross-env": "^7.0.3", 48 | "cz-conventional-changelog": "^3.3.0", 49 | "eslint-plugin-import": "^2.23.4", 50 | "eslint-plugin-unicorn": "^34.0.1", 51 | "husky": "^7.0.1", 52 | "jest": "^27.0.6", 53 | "espower-typescript": "10", 54 | "intelli-espower-loader": "1", 55 | "mocha": "^9.1.0", 56 | "mocha-lcov-reporter": "1", 57 | "mwts": "^1.2.2", 58 | "rewire": "^5.0.0", 59 | "standard-version": "^9.3.1", 60 | "swagger-ui-dist": "^3.51.1", 61 | "ts-jest": "^27.0.4", 62 | "typescript": "^4.3.5" 63 | }, 64 | "engines": { 65 | "node": ">=14.0.0" 66 | }, 67 | "scripts": { 68 | "start": "egg-scripts start --daemon --title=midway-server-my_midway_project --framework=midway", 69 | "online": "egg-scripts start --title=midway-server-my_midway_project --framework=midway", 70 | "stop": "egg-scripts stop --title=midway-server-my_midway_project", 71 | "start_build": "npm run build && cross-env NODE_ENV=development midway-bin dev", 72 | "dev": "cross-env ets && cross-env NODE_ENV=local midway-bin dev --ts", 73 | "debug": "cross-env NODE_ENV=local midway-bin dev --debug --ts", 74 | "test": "midway-bin test --ts --forceExit", 75 | "test:mocha": "cross-env MIDWAY_SERVER_ENV=unittest mocha --parallel=false", 76 | "cov": "midway-bin cov --ts --forceExit", 77 | "cov:mocha": "cross-env MIDWAY_SERVER_ENV=unittest TS_NODE_TYPE_CHECK=false TS_NODE_TRANSPILE_ONLY=true c8 mocha --parallel=false", 78 | "lint": "mwts check", 79 | "lint:fix": "mwts fix", 80 | "ci": "npm run cov", 81 | "build": "midway-bin build -c", 82 | "commit": "git-cz", 83 | "release": "standard-version", 84 | "deploy": "midway-bin deploy" 85 | }, 86 | "midway-bin-clean": [ 87 | ".vscode/.tsbuildinfo", 88 | "dist" 89 | ], 90 | "midway-integration": { 91 | "lifecycle": { 92 | "before:package:cleanup": "npm run build" 93 | } 94 | }, 95 | "egg": { 96 | "framework": "@midwayjs/web" 97 | }, 98 | "repository": { 99 | "type": "git", 100 | "url": "https://github.com/fsd-nodejs/service-mw2" 101 | }, 102 | "author": "FSD Node.js Group", 103 | "license": "MIT", 104 | "config": { 105 | "commitizen": { 106 | "path": "./node_modules/cz-conventional-changelog" 107 | } 108 | }, 109 | "husky": { 110 | "hooks": { 111 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=fsd-nodejs_service-mw2 2 | sonar.organization=fsd-nodejs 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=service-mw2 6 | #sonar.projectVersion=1.0 7 | 8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 9 | #sonar.sources=. 10 | 11 | # Encoding of the source code. Default is default system encoding 12 | #sonar.sourceEncoding=UTF-8 13 | -------------------------------------------------------------------------------- /src/app/controller/admin/menu.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { 4 | Controller, 5 | Get, 6 | Provide, 7 | Inject, 8 | Query, 9 | ALL, 10 | Validate, 11 | Post, 12 | Patch, 13 | Body, 14 | Del, 15 | } from '@midwayjs/decorator'; 16 | 17 | import { Context } from '@/interface'; 18 | 19 | import { AdminMenuService } from '../../service/admin/menu'; 20 | import { AdminRoleService } from '../../service/admin/role'; 21 | import { AdminPermissionService } from '../../service/admin/permission'; 22 | import { 23 | CreateDTO, 24 | OrderMenuDTO, 25 | QueryDTO, 26 | RemoveDTO, 27 | ShowDTO, 28 | UpdateDTO, 29 | } from '../../dto/admin/menu'; 30 | import MyError from '../../util/my-error'; 31 | 32 | @Provide() 33 | @Controller('/admin/menu', { 34 | tagName: '菜单管理', 35 | description: '包含菜单的增、删、改、查、排序', 36 | }) 37 | export class AdminMenuController { 38 | @Inject('adminMenuService') 39 | service: AdminMenuService; 40 | 41 | @Inject('adminRoleService') 42 | roleService: AdminRoleService; 43 | 44 | @Inject('adminPermissionService') 45 | permissionService: AdminPermissionService; 46 | 47 | @Get('/query', { 48 | summary: '获取菜单列表', 49 | description: '分页接口,获取菜单的列表', 50 | }) 51 | @Validate() 52 | async query(ctx: Context, @Query(ALL) query: QueryDTO) { 53 | const result = await this.service.queryAdminMenu(query); 54 | ctx.helper.success(result); 55 | } 56 | 57 | @Get('/show', { 58 | summary: '获取单个菜单详情', 59 | description: '获取菜单的详细信息,包括其关联的对象', 60 | }) 61 | @Validate() 62 | async show(ctx: Context, @Query(ALL) query: ShowDTO) { 63 | const result = await this.service.getAdminMenuById(query.id); 64 | assert.ok(result, new MyError('菜单不存在,请检查', 400)); 65 | ctx.helper.success(result); 66 | } 67 | 68 | @Post('/create', { 69 | summary: '创建菜单', 70 | description: '会校验要关联角色和权限是否存在', 71 | }) 72 | @Validate() 73 | async create(ctx: Context, @Body(ALL) params: CreateDTO) { 74 | // 检查角色是否存在 75 | await this.roleService.checkRoleExists(params.roles); 76 | 77 | // 检查权限是否存在 78 | await this.permissionService.checkPermissionExists([params.permissionId]); 79 | 80 | const result = await this.service.createAdminMenu(params); 81 | 82 | ctx.helper.success(result, null, 201); 83 | } 84 | 85 | @Patch('/update', { 86 | summary: '更新菜单数据', 87 | description: '可更新菜单一个或多个字段', 88 | }) 89 | @Validate() 90 | async update(ctx: Context, @Body(ALL) params: UpdateDTO) { 91 | // 检查菜单是否存在 92 | await this.service.checkMenuExists([params.id]); 93 | 94 | // 检查角色是否存在 95 | await this.roleService.checkRoleExists(params.roles); 96 | 97 | // 检查权限是否存在 98 | await this.permissionService.checkPermissionExists([params.permissionId]); 99 | 100 | const { affected } = await this.service.updateAdminMenu(params); 101 | assert.ok(affected, new MyError('更新失败,请检查', 400)); 102 | 103 | ctx.helper.success(null, null, 204); 104 | } 105 | 106 | @Del('/remove', { 107 | summary: '删除菜单', 108 | description: '关联关系表不会删除其中的内容,可以同时删除多个菜单', 109 | }) 110 | @Validate() 111 | async remove(ctx: Context, @Body(ALL) params: RemoveDTO) { 112 | // 检查菜单是否存在 113 | await this.service.checkMenuExists(params.ids); 114 | 115 | const total = await this.service.removeAdminMenuByIds(params.ids); 116 | 117 | assert.ok(total, new MyError('删除失败,请检查', 400)); 118 | 119 | ctx.helper.success(null, null, 204); 120 | } 121 | 122 | @Post('/order', { 123 | summary: '对菜单进行排序', 124 | description: 125 | '需要全量的进行排序,数组下标索引即顺序,前端配合 antd-nestable 这个包使用', 126 | }) 127 | @Validate() 128 | async order(ctx: Context, @Body(ALL) params: OrderMenuDTO) { 129 | const { orders } = params; 130 | 131 | // 检查菜单是否存在 132 | await this.service.checkMenuExists(orders.map(item => item.id)); 133 | 134 | const newMenu = orders.map((item, index) => ({ 135 | ...item, 136 | order: index + 1, 137 | })); 138 | 139 | await this.service.orderAdminMenu(newMenu); 140 | 141 | ctx.helper.success(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/app/controller/admin/permission.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { 4 | Controller, 5 | Get, 6 | Provide, 7 | Inject, 8 | Query, 9 | ALL, 10 | Validate, 11 | Post, 12 | Del, 13 | Patch, 14 | Body, 15 | } from '@midwayjs/decorator'; 16 | 17 | import { Context } from '@/interface'; 18 | 19 | import { AdminRoleService } from '../../service/admin/role'; 20 | import { AdminPermissionService } from '../../service/admin/permission'; 21 | import { 22 | QueryDTO, 23 | CreateDTO, 24 | UpdateDTO, 25 | ShowDTO, 26 | RemoveDTO, 27 | } from '../../dto/admin/permission'; 28 | import MyError from '../../util/my-error'; 29 | 30 | @Provide() 31 | @Controller('/admin/permission', { 32 | tagName: '权限管理', 33 | description: '包含权限的增、删、改、查', 34 | }) 35 | export class AdminPermissionController { 36 | @Inject('adminPermissionService') 37 | service: AdminPermissionService; 38 | 39 | @Inject('adminRoleService') 40 | roleService: AdminRoleService; 41 | 42 | @Get('/query', { 43 | summary: '获取权限列表', 44 | description: '分页接口,查询权限列表', 45 | }) 46 | @Validate() 47 | async query(ctx: Context, @Query(ALL) query: QueryDTO) { 48 | const result = await this.service.queryAdminPermission(query); 49 | ctx.helper.success(result); 50 | } 51 | 52 | @Get('/show', { 53 | summary: '获取单个权限详情', 54 | description: '获取权限的详细信息,包括其关联的对象', 55 | }) 56 | @Validate() 57 | async show(ctx: Context, @Query(ALL) query: ShowDTO) { 58 | const result = await this.service.getAdminPermissionById(query.id); 59 | assert.ok(result, new MyError('权限不存在,请检查', 400)); 60 | ctx.helper.success(result); 61 | } 62 | 63 | @Post('/create', { 64 | summary: '创建权限', 65 | description: '', 66 | }) 67 | @Validate() 68 | async create(ctx: Context, @Body(ALL) params: CreateDTO) { 69 | const result = await this.service.createAdminPermission(params); 70 | 71 | ctx.helper.success(result, null, 201); 72 | } 73 | 74 | @Patch('/update', { 75 | summary: '更新权限数据', 76 | description: '可更新权限一个或多个字段', 77 | }) 78 | @Validate() 79 | async update(ctx: Context, @Body(ALL) params: UpdateDTO) { 80 | // 检查权限是否存在 81 | await this.service.checkPermissionExists([params.id]); 82 | 83 | const { affected } = await this.service.updateAdminPermission(params); 84 | assert.ok(affected, new MyError('更新失败,请检查', 400)); 85 | 86 | ctx.helper.success(null, null, 204); 87 | } 88 | 89 | @Del('/remove', { 90 | summary: '删除权限', 91 | description: '关联关系表不会删除其中的内容,可以同时删除多个权限', 92 | }) 93 | @Validate() 94 | async remove(ctx: Context, @Body(ALL) params: RemoveDTO) { 95 | // 检查权限是否存在 96 | await this.service.checkPermissionExists(params.ids); 97 | 98 | const total = await this.service.removeAdminPermissionByIds(params.ids); 99 | assert.ok(total, new MyError('删除失败,请检查', 400)); 100 | 101 | ctx.helper.success(null, null, 204); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/app/controller/admin/role.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { 4 | Controller, 5 | Get, 6 | Provide, 7 | Inject, 8 | Query, 9 | ALL, 10 | Validate, 11 | Post, 12 | Patch, 13 | Del, 14 | Body, 15 | } from '@midwayjs/decorator'; 16 | 17 | import { Context } from '@/interface'; 18 | 19 | import { AdminRoleService } from '../../service/admin/role'; 20 | import { AdminPermissionService } from '../../service/admin/permission'; 21 | import { 22 | QueryDTO, 23 | ShowDTO, 24 | CreateDTO, 25 | UpdateDTO, 26 | RemoveDTO, 27 | } from '../../dto/admin/role'; 28 | import MyError from '../../util/my-error'; 29 | 30 | @Provide() 31 | @Controller('/admin/role', { 32 | tagName: '角色管理', 33 | description: '包含角色的增、删、改、查', 34 | }) 35 | export class AdminRoleController { 36 | @Inject('adminRoleService') 37 | service: AdminRoleService; 38 | 39 | @Inject('adminPermissionService') 40 | permissionService: AdminPermissionService; 41 | 42 | @Get('/query', { 43 | summary: '获取角色列表', 44 | description: '分页接口,查询角色列表', 45 | }) 46 | @Validate() 47 | async query(ctx: Context, @Query(ALL) query: QueryDTO) { 48 | const result = await this.service.queryAdminRole(query); 49 | ctx.helper.success(result); 50 | } 51 | 52 | @Get('/show', { 53 | summary: '获取单个角色详情', 54 | description: '获取角色的详细信息,包括其关联的对象', 55 | }) 56 | @Validate() 57 | async show(ctx: Context, @Query(ALL) query: ShowDTO) { 58 | const result = await this.service.getAdminRoleById(query.id); 59 | assert.ok(result, new MyError('角色不存在,请检查', 400)); 60 | ctx.helper.success(result); 61 | } 62 | 63 | @Post('/create', { 64 | summary: '创建角色', 65 | description: '会校验关联的权限是否存在', 66 | }) 67 | @Validate() 68 | async create(ctx: Context, @Body(ALL) params: CreateDTO) { 69 | // 检查角色是否存在 70 | await this.permissionService.checkPermissionExists(params.permissions); 71 | 72 | const result = await this.service.createAdminRole(params); 73 | 74 | ctx.helper.success(result, null, 201); 75 | } 76 | 77 | @Patch('/update', { 78 | summary: '更新角色数据', 79 | description: '可更新角色一个或多个字段', 80 | }) 81 | @Validate() 82 | async update(ctx: Context, @Body(ALL) params: UpdateDTO) { 83 | // 检查角色是否存在 84 | await this.service.checkRoleExists([params.id]); 85 | 86 | const { affected } = await this.service.updateAdminRole(params); 87 | assert.ok(affected, new MyError('更新失败,请检查', 400)); 88 | 89 | ctx.helper.success(null, null, 204); 90 | } 91 | 92 | @Del('/remove', { 93 | summary: '删除角色', 94 | description: '关联关系表不会删除其中的内容,可以同时删除多个角色', 95 | }) 96 | @Validate() 97 | async remove(ctx: Context, @Body(ALL) params: RemoveDTO) { 98 | // 检查角色是否存在 99 | await this.service.checkRoleExists(params.ids); 100 | 101 | const total = await this.service.removeAdminRoleByIds(params.ids); 102 | assert.ok(total, new MyError('删除失败,请检查', 400)); 103 | 104 | ctx.helper.success(null, null, 204); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/app/controller/admin/user.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { 4 | Controller, 5 | Get, 6 | Provide, 7 | Inject, 8 | Query, 9 | ALL, 10 | Post, 11 | Patch, 12 | Del, 13 | Validate, 14 | Body, 15 | } from '@midwayjs/decorator'; 16 | 17 | import { Context } from '@/interface'; 18 | 19 | import { AdminUserService } from '../../service/admin/user'; 20 | import { 21 | QueryDTO, 22 | ShowDTO, 23 | CreateDTO, 24 | UpdateDTO, 25 | RemoveDTO, 26 | } from '../../dto/admin/user'; 27 | import MyError from '../../util/my-error'; 28 | import { AdminRoleService } from '../../service/admin/role'; 29 | import { AdminPermissionService } from '../../service/admin/permission'; 30 | 31 | @Provide() 32 | @Controller('/admin/user', { 33 | tagName: '管理员管理', 34 | description: '包含管理员的增、删、改、查', 35 | }) 36 | export class AdminUserController { 37 | @Inject('adminUserService') 38 | service: AdminUserService; 39 | 40 | @Inject('adminRoleService') 41 | roleService: AdminRoleService; 42 | 43 | @Inject('adminPermissionService') 44 | permissionService: AdminPermissionService; 45 | 46 | @Get('/query', { 47 | summary: '获取管理员列表', 48 | description: '分页接口,查询管理员列表', 49 | }) 50 | @Validate() 51 | async query(ctx: Context, @Query(ALL) query: QueryDTO) { 52 | const result = await this.service.queryAdminUser(query); 53 | ctx.helper.success(result); 54 | } 55 | 56 | @Get('/show', { 57 | summary: '获取单个管理员详情', 58 | description: '获取管理员的详细信息,包括其关联的对象', 59 | }) 60 | @Validate() 61 | async show(ctx: Context, @Query(ALL) query: ShowDTO) { 62 | const result = await this.service.getAdminUserById(query.id); 63 | assert.ok(result, new MyError('管理员不存在,请检查', 400)); 64 | ctx.helper.success(result); 65 | } 66 | 67 | @Post('/create', { 68 | summary: '创建管理员', 69 | description: '会校验要关联的角色和权限是否存在', 70 | }) 71 | @Validate() 72 | async create(ctx: Context, @Body(ALL) params: CreateDTO) { 73 | const { roles, permissions } = params; 74 | 75 | // 检查角色是否存在 76 | await this.roleService.checkRoleExists(roles); 77 | 78 | // 检查权限是否存在 79 | await this.permissionService.checkPermissionExists(permissions); 80 | 81 | const passwordHash = ctx.helper.bhash(params.password); 82 | 83 | const result = await this.service.createAdminUser({ 84 | ...params, 85 | password: passwordHash, 86 | }); 87 | 88 | ctx.helper.success(result, null, 201); 89 | } 90 | 91 | @Patch('/update', { 92 | summary: '更新管理员数据', 93 | description: '可更新管理员一个或多个字段', 94 | }) 95 | @Validate() 96 | async update(ctx: Context, @Body(ALL) params: UpdateDTO) { 97 | const { roles, permissions } = params; 98 | 99 | // 检查角色是否存在 100 | await this.roleService.checkRoleExists(roles); 101 | 102 | // 检查权限是否存在 103 | await this.permissionService.checkPermissionExists(permissions); 104 | 105 | const { affected } = await this.service.updateAdminUser(params); 106 | assert.ok(affected, new MyError('更新失败,请检查', 400)); 107 | 108 | ctx.helper.success(null, null, 204); 109 | } 110 | 111 | @Del('/remove', { 112 | summary: '删除管理员', 113 | description: '关联关系表不会删除其中的内容,可以同时删除多个管理员', 114 | }) 115 | @Validate() 116 | async remove(ctx: Context, @Body(ALL) params: RemoveDTO) { 117 | // 检查管理员是否存在 118 | await this.service.checkUserExists(params.ids); 119 | 120 | const total = await this.service.removeAdminUserByIds(params.ids); 121 | assert.ok(total, new MyError('删除失败,请检查', 400)); 122 | 123 | ctx.helper.success(null, null, 204); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/app/controller/auth.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { 4 | Controller, 5 | Get, 6 | Post, 7 | Provide, 8 | Inject, 9 | Validate, 10 | Body, 11 | ALL, 12 | } from '@midwayjs/decorator'; 13 | import { CreateApiDoc } from '@midwayjs/swagger'; 14 | 15 | import { Context } from '@/interface'; 16 | 17 | import { AuthService } from '../service/auth'; 18 | import { LoginDTO } from '../dto/auth'; 19 | import MyError from '../util/my-error'; 20 | 21 | @Provide() 22 | @Controller('/auth', { 23 | tagName: '管理员登录授权', 24 | description: '包含管理员授权登录、获取信息等接口 ', 25 | }) 26 | export class AuthController { 27 | @Inject('authService') 28 | service: AuthService; 29 | 30 | /** 31 | * 登录,目前使用帐号+密码模式 32 | */ 33 | @(CreateApiDoc() 34 | .summary('管理员登录') 35 | .description( 36 | '使用帐号密码登录,拿到 token 后,前端需要将 token 放入 header 中,格式 token: Bearer ${token}' 37 | ) 38 | .respond(200, 'success', 'json', { 39 | example: { 40 | token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9xxxx', 41 | currentAuthority: 'admin', 42 | status: 'ok', 43 | type: 'account', 44 | }, 45 | }) 46 | .build()) 47 | @Post('/login') 48 | @Validate() 49 | async login(ctx: Context, @Body(ALL) params: LoginDTO): Promise { 50 | // 后续可能有多种登录方式 51 | const existAdmiUser = await this.service.localHandler(params); 52 | 53 | // 检查管理员存在 54 | assert.ok(existAdmiUser, new MyError('管理员不存在', 400)); 55 | 56 | // 生成Token 57 | const token = await this.service.createAdminUserToken(existAdmiUser); 58 | 59 | // 缓存管理员数据 60 | await this.service.cacheAdminUser(existAdmiUser); 61 | 62 | // TODO: 调用 rotateCsrfSecret 刷新管理员的 CSRF token 63 | // ctx.rotateCsrfSecret() 64 | 65 | ctx.helper.success({ 66 | token, 67 | currentAuthority: 'admin', 68 | status: 'ok', 69 | type: 'account', 70 | }); 71 | } 72 | 73 | /** 74 | * 退出登录 75 | */ 76 | @(CreateApiDoc() 77 | .summary('管理员退出登录') 78 | .description('退出登录,退出成功 data 为{}') 79 | .respond(200, 'success') 80 | .build()) 81 | @Get('/logout') 82 | async logout(ctx: Context): Promise { 83 | const { currentUser } = ctx; 84 | 85 | // 清理管理员数据和token 86 | await this.service.removeAdminUserTokenById(currentUser.id); 87 | await this.service.cleanAdminUserById(currentUser.id); 88 | 89 | ctx.helper.success({}); 90 | } 91 | 92 | /** 93 | * 获取当前管理员的信息 94 | */ 95 | @(CreateApiDoc() 96 | .summary('获取当前管理员的信息') 97 | .description('管理员相关的信息') 98 | .respond(200, 'success', 'json', { 99 | example: { 100 | id: '1', 101 | username: 'admin', 102 | name: 'Administrator', 103 | avatar: 'http://x.y.z', 104 | createdAt: '2020-08-20T01:14:57.000Z', 105 | updatedAt: '2020-08-20T01:14:57.000Z', 106 | }, 107 | }) 108 | .build()) 109 | @Get('/currentUser') 110 | async currentUser(ctx: Context): Promise { 111 | ctx.helper.success(ctx.currentUser); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/app/controller/home.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Inject, Provide } from '@midwayjs/decorator'; 2 | import { CreateApiDoc } from '@midwayjs/swagger'; 3 | import { JwtComponent } from '@mw-components/jwt'; 4 | import { KoidComponent } from '@mw-components/koid'; 5 | 6 | import { Context } from '@/interface'; 7 | 8 | import { RabbitmqService } from '../service/rabbitmq'; 9 | 10 | @Provide() 11 | @Controller('/', { 12 | tagName: '默认的接口', 13 | description: '包含连通性接口、鉴权验证接口', 14 | }) 15 | export class HomeController { 16 | @Inject('jwt:jwtComponent') 17 | jwt: JwtComponent; 18 | 19 | @Inject() 20 | readonly koid: KoidComponent; 21 | 22 | @Inject() 23 | rabbitmqService: RabbitmqService; 24 | 25 | @(CreateApiDoc().summary('获取主页').description('需要鉴权').build()) 26 | @Get('/') 27 | async home(ctx: Context) { 28 | ctx.tracerManager.startSpan('getHome'); 29 | const ret = 'Hello Midwayjs!' + `\nreqId: "${ctx.reqId}"`; 30 | ctx.tracerManager.finishSpan(); 31 | return ret; 32 | } 33 | 34 | @(CreateApiDoc().summary('检查连通性').description('不需要鉴权').build()) 35 | @Get('/ping') 36 | async ping(ctx: Context) { 37 | ctx.body = `OK, runtime is: Node.js ${process.version}`; 38 | } 39 | 40 | @(CreateApiDoc() 41 | .summary('生成雪花ID,输出bigint') 42 | .description('不需要鉴权') 43 | .build()) 44 | @Get('/genid') 45 | genId(): string { 46 | return this.koid.idGenerator.toString(); 47 | } 48 | 49 | @(CreateApiDoc() 50 | .summary('生成雪花ID,输出HEX') 51 | .description('不需要鉴权') 52 | .build()) 53 | @Get('/genidHex') 54 | genIdHex(): string { 55 | return this.koid.nextHex; 56 | } 57 | 58 | @Get('/sendToQueue') 59 | async sendToQueue(ctx: Context) { 60 | const res = await this.rabbitmqService.sendToQueue( 61 | 'my-queue', 62 | 'hello world' 63 | ); 64 | ctx.helper.success(res); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/dto/admin/menu.ts: -------------------------------------------------------------------------------- 1 | import { Rule, RuleType } from '@midwayjs/decorator'; 2 | import { CreateApiPropertyDoc } from '@midwayjs/swagger'; 3 | 4 | /** 5 | * 菜单列表查询参数 6 | */ 7 | export class QueryDTO { 8 | @CreateApiPropertyDoc('当前页') 9 | @Rule(RuleType.number().min(1).max(100000).default(1).optional()) 10 | current?: number; 11 | 12 | @CreateApiPropertyDoc('每页数量') 13 | @Rule(RuleType.number().min(1).max(1000).default(10).optional()) 14 | pageSize?: number; 15 | } 16 | 17 | /** 18 | * 获取单条菜单参数 19 | */ 20 | export class ShowDTO { 21 | @CreateApiPropertyDoc('菜单id') 22 | @Rule(RuleType.string().trim().max(10).required()) 23 | id: string; 24 | } 25 | 26 | /** 27 | * 删除菜单参数 28 | */ 29 | export class RemoveDTO { 30 | @CreateApiPropertyDoc('菜单id数组') 31 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).min(1)) 32 | ids: string[]; 33 | } 34 | 35 | /** 36 | * 创建菜单参数 37 | */ 38 | export class CreateDTO { 39 | @CreateApiPropertyDoc('父级菜单的id') 40 | @Rule(RuleType.string().trim().max(10).optional().default('0')) 41 | parentId?: string; 42 | 43 | @CreateApiPropertyDoc('标题') 44 | @Rule(RuleType.string().trim().max(50).required()) 45 | title: string; 46 | 47 | @CreateApiPropertyDoc('路由地址(前端使用的)') 48 | @Rule( 49 | RuleType.string().trim().max(255).uri({ allowRelative: true }).required() 50 | ) 51 | uri: string; 52 | 53 | @CreateApiPropertyDoc('关联的角色') 54 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).optional()) 55 | roles?: string[]; 56 | 57 | @CreateApiPropertyDoc('关联的权限') 58 | @Rule(RuleType.string().trim().max(10).optional()) 59 | permissionId?: string; 60 | } 61 | 62 | /** 63 | * 更新菜单参数 64 | */ 65 | export class UpdateDTO { 66 | @CreateApiPropertyDoc('菜单的id') 67 | @Rule(RuleType.string().trim().max(10).required()) 68 | id: string; 69 | 70 | @CreateApiPropertyDoc('父级菜单的id') 71 | @Rule(RuleType.string().trim().max(10).optional().default('0')) 72 | parentId?: string; 73 | 74 | @CreateApiPropertyDoc('标题') 75 | @Rule(RuleType.string().trim().max(50).optional()) 76 | title?: string; 77 | 78 | @CreateApiPropertyDoc('路由地址(前端使用的)') 79 | @Rule( 80 | RuleType.string().trim().max(255).uri({ allowRelative: true }).optional() 81 | ) 82 | uri?: string; 83 | 84 | @CreateApiPropertyDoc('关联的角色') 85 | @Rule(RuleType.array().items(RuleType.string().trim().max(10).optional())) 86 | roles?: string[]; 87 | 88 | @CreateApiPropertyDoc('关联的权限') 89 | @Rule(RuleType.string().trim().max(10).optional()) 90 | permissionId?: string; 91 | } 92 | 93 | /** 94 | * 菜单排序参数 95 | */ 96 | export class OrderMenuDTO { 97 | @CreateApiPropertyDoc('对菜单进行排序对数组,每个object 都需要 id parentId') 98 | @Rule( 99 | RuleType.array() 100 | .items( 101 | RuleType.object({ 102 | id: RuleType.string().trim().max(10).required(), 103 | parentId: RuleType.string().trim().max(10).optional().default('0'), 104 | }) 105 | ) 106 | .min(1) 107 | .required() 108 | ) 109 | orders: { 110 | id: string; 111 | parentId: string; 112 | }[]; 113 | } 114 | -------------------------------------------------------------------------------- /src/app/dto/admin/permission.ts: -------------------------------------------------------------------------------- 1 | import { Rule, RuleType } from '@midwayjs/decorator'; 2 | import { CreateApiPropertyDoc } from '@midwayjs/swagger'; 3 | 4 | /** 5 | * 查询权限列表参数 6 | */ 7 | export class QueryDTO { 8 | @CreateApiPropertyDoc('当前页') 9 | @Rule(RuleType.number().min(1).max(100000).default(1).optional()) 10 | current?: number; 11 | 12 | @CreateApiPropertyDoc('每页数量') 13 | @Rule(RuleType.number().min(1).max(1000).default(10).optional()) 14 | pageSize?: number; 15 | 16 | @CreateApiPropertyDoc('筛选字段-id') 17 | @Rule(RuleType.string().trim().max(10).optional().allow('')) 18 | id?: string; 19 | 20 | @CreateApiPropertyDoc('筛选字段-名称') 21 | @Rule(RuleType.string().trim().max(50).optional().allow('')) 22 | name?: string; 23 | 24 | @CreateApiPropertyDoc('筛选字段-标识') 25 | @Rule(RuleType.string().trim().max(50).optional().allow('')) 26 | slug?: string; 27 | 28 | @CreateApiPropertyDoc('筛选字段-路径') 29 | @Rule(RuleType.string().trim().max(50).optional().allow('')) 30 | httpPath?: string; 31 | 32 | @CreateApiPropertyDoc('筛选字段-请求方式') 33 | @Rule(RuleType.string().trim().max(50).optional().allow('')) 34 | httpMethod?: string; 35 | 36 | @CreateApiPropertyDoc( 37 | '排序字段,以字段名加下划线组合,不能有特殊字符和不存在的字段。例如: name_ASC 或者 name_DESC' 38 | ) 39 | @Rule( 40 | RuleType.string() 41 | .trim() 42 | .max(50) 43 | .regex(/^[a-zA-Z]*(_ascend|_descend)$/) 44 | .optional() 45 | ) 46 | sorter?: string; 47 | } 48 | 49 | /** 50 | * 获取单条权限参数 51 | */ 52 | export class ShowDTO { 53 | @CreateApiPropertyDoc('权限的id') 54 | @Rule(RuleType.string().trim().max(10).required()) 55 | id: string; 56 | } 57 | 58 | /** 59 | * 删除权限参数 60 | */ 61 | export class RemoveDTO { 62 | @CreateApiPropertyDoc('权限id的数组') 63 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).min(1)) 64 | ids: string[]; 65 | } 66 | 67 | /** 68 | * 创建权限参数 69 | */ 70 | export class CreateDTO { 71 | @CreateApiPropertyDoc('名称') 72 | @Rule(RuleType.string().trim().max(50).required()) 73 | name: string; 74 | 75 | @CreateApiPropertyDoc('标识') 76 | @Rule(RuleType.string().trim().max(50).required()) 77 | slug: string; 78 | 79 | @CreateApiPropertyDoc('请求方式') 80 | @Rule( 81 | RuleType.array() 82 | .items( 83 | RuleType.string() 84 | .regex(/^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|ANY)$/) 85 | .empty() 86 | .label('httpMethod') 87 | ) 88 | .unique() 89 | .required() 90 | ) 91 | httpMethod: string[]; 92 | 93 | @CreateApiPropertyDoc('接口路径(后端使用的)') 94 | @Rule(RuleType.string().uri({ allowRelative: true }).required()) 95 | httpPath: string; 96 | } 97 | 98 | /** 99 | * 更新权限参数 100 | */ 101 | export class UpdateDTO { 102 | @CreateApiPropertyDoc('权限的id') 103 | @Rule(RuleType.string().trim().max(10).required()) 104 | id: string; 105 | 106 | @CreateApiPropertyDoc('名称') 107 | @Rule(RuleType.string().trim().max(50).optional()) 108 | name?: string; 109 | 110 | @CreateApiPropertyDoc('标识') 111 | @Rule(RuleType.string().trim().max(50).optional()) 112 | slug?: string; 113 | 114 | @CreateApiPropertyDoc('请求方式') 115 | @Rule( 116 | RuleType.array() 117 | .items( 118 | RuleType.string() 119 | .regex(/^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD|ANY)$/) 120 | .empty() 121 | .label('httpMethod') 122 | ) 123 | .unique() 124 | .optional() 125 | ) 126 | httpMethod?: string[]; 127 | 128 | @CreateApiPropertyDoc('接口路径(后端使用的)') 129 | @Rule(RuleType.string().uri({ allowRelative: true }).optional()) 130 | httpPath?: string; 131 | } 132 | -------------------------------------------------------------------------------- /src/app/dto/admin/role.ts: -------------------------------------------------------------------------------- 1 | import { Rule, RuleType } from '@midwayjs/decorator'; 2 | import { CreateApiPropertyDoc } from '@midwayjs/swagger'; 3 | 4 | /** 5 | * 查询角色列表参数 6 | */ 7 | export class QueryDTO { 8 | @CreateApiPropertyDoc('当前页') 9 | @Rule(RuleType.number().min(1).max(100000).default(1).optional()) 10 | current?: number; 11 | 12 | @CreateApiPropertyDoc('每页数量') 13 | @Rule(RuleType.number().min(1).max(1000).default(10).optional()) 14 | pageSize?: number; 15 | 16 | @CreateApiPropertyDoc('筛选字段-id') 17 | @Rule(RuleType.string().trim().max(10).optional()) 18 | id?: string; 19 | 20 | @CreateApiPropertyDoc('筛选字段-名称') 21 | @Rule(RuleType.string().trim().max(50).optional()) 22 | name?: string; 23 | 24 | @CreateApiPropertyDoc('筛选字段-标识') 25 | @Rule(RuleType.string().trim().max(50).optional()) 26 | slug?: string; 27 | 28 | @CreateApiPropertyDoc( 29 | '排序字段,以字段名加下划线组合,不能有特殊字符和不存在的字段。例如: name_ASC 或者 name_DESC' 30 | ) 31 | @Rule( 32 | RuleType.string() 33 | .trim() 34 | .max(50) 35 | .regex(/^[a-zA-Z]*(_ascend|_descend)$/) 36 | .optional() 37 | ) 38 | sorter?: string; 39 | } 40 | 41 | /** 42 | * 获取单条角色参数 43 | */ 44 | export class ShowDTO { 45 | @CreateApiPropertyDoc('角色的id') 46 | @Rule(RuleType.string().trim().max(10).required()) 47 | id: string; 48 | } 49 | 50 | /** 51 | * 删除角色参数 52 | */ 53 | export class RemoveDTO { 54 | @CreateApiPropertyDoc('角色id的数组') 55 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).min(1)) 56 | ids: string[]; 57 | } 58 | 59 | /** 60 | * 创建角色参数 61 | */ 62 | export class CreateDTO { 63 | @CreateApiPropertyDoc('名称') 64 | @Rule(RuleType.string().trim().max(50).required()) 65 | name: string; 66 | 67 | @CreateApiPropertyDoc('标识') 68 | @Rule(RuleType.string().trim().max(50).required()) 69 | slug: string; 70 | 71 | @CreateApiPropertyDoc('关联的权限') 72 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).optional()) 73 | permissions?: string[]; 74 | } 75 | 76 | /** 77 | * 更新角色参数 78 | */ 79 | export class UpdateDTO { 80 | @CreateApiPropertyDoc('角色的id') 81 | @Rule(RuleType.string().trim().max(10).required()) 82 | id: string; 83 | 84 | @CreateApiPropertyDoc('名称') 85 | @Rule(RuleType.string().trim().max(50).optional()) 86 | name?: string; 87 | 88 | @CreateApiPropertyDoc('标识') 89 | @Rule(RuleType.string().trim().max(50).optional()) 90 | slug?: string; 91 | 92 | @CreateApiPropertyDoc('关联的权限') 93 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).optional()) 94 | permissions?: string[]; 95 | } 96 | -------------------------------------------------------------------------------- /src/app/dto/admin/user.ts: -------------------------------------------------------------------------------- 1 | import { Rule, RuleType } from '@midwayjs/decorator'; 2 | import { CreateApiPropertyDoc } from '@midwayjs/swagger'; 3 | 4 | /** 5 | * 查询管理员列表参数 6 | */ 7 | export class QueryDTO { 8 | @CreateApiPropertyDoc('当前页') 9 | @Rule(RuleType.number().min(1).max(100000).default(1).optional()) 10 | current?: number; 11 | 12 | @CreateApiPropertyDoc('每页数量') 13 | @Rule(RuleType.number().min(1).max(1000).default(10).optional()) 14 | pageSize?: number; 15 | 16 | @CreateApiPropertyDoc('筛选字段-id') 17 | @Rule(RuleType.string().trim().max(10).optional()) 18 | id?: string; 19 | 20 | @CreateApiPropertyDoc('筛选字段-名称') 21 | @Rule(RuleType.string().trim().max(50).optional()) 22 | name?: string; 23 | 24 | @CreateApiPropertyDoc('筛选字段-帐号') 25 | @Rule(RuleType.string().trim().max(50).optional()) 26 | username?: string; 27 | 28 | @CreateApiPropertyDoc( 29 | '排序字段,以字段名加下划线组合,不能有特殊字符和不存在的字段。例如: name_ASC 或者 name_DESC' 30 | ) 31 | @Rule( 32 | RuleType.string() 33 | .trim() 34 | .max(50) 35 | .regex(/^[a-zA-Z]*(_ascend|_descend)$/) 36 | .optional() 37 | ) 38 | sorter?: string; 39 | } 40 | 41 | /** 42 | * 获取单个管理员参数 43 | */ 44 | export class ShowDTO { 45 | @CreateApiPropertyDoc('管理员的id') 46 | @Rule(RuleType.string().trim().max(10).required()) 47 | id: string; 48 | } 49 | 50 | /** 51 | * 删除管理员参数 52 | */ 53 | export class RemoveDTO { 54 | @CreateApiPropertyDoc('管理员id的数组') 55 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).min(1)) 56 | ids: string[]; 57 | } 58 | 59 | /** 60 | * 创建管理员参数 61 | */ 62 | export class CreateDTO { 63 | @CreateApiPropertyDoc('帐号,登录用的') 64 | @Rule(RuleType.string().trim().min(5).max(190).required()) 65 | username: string; 66 | 67 | @CreateApiPropertyDoc('名称') 68 | @Rule(RuleType.string().trim().min(5).max(255).required()) 69 | name: string; 70 | 71 | @CreateApiPropertyDoc('头像') 72 | @Rule(RuleType.string().trim().max(255).optional()) 73 | avatar?: string; 74 | 75 | @CreateApiPropertyDoc('密码(数据库入库前会进行加密),前端做二次确认校验') 76 | @Rule(RuleType.string().trim().min(5).max(60).required()) 77 | password: string; 78 | 79 | @CreateApiPropertyDoc('关联的角色') 80 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).optional()) 81 | roles?: string[]; 82 | 83 | @CreateApiPropertyDoc('关联的权限') 84 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).optional()) 85 | permissions?: string[]; 86 | } 87 | 88 | /** 89 | * 更新管理员参数 90 | */ 91 | export class UpdateDTO { 92 | @CreateApiPropertyDoc('管理员id') 93 | @Rule(RuleType.string().trim().max(10).required()) 94 | id: string; 95 | 96 | @CreateApiPropertyDoc('帐号,登录用的') 97 | @Rule(RuleType.string().trim().min(5).max(190).required()) 98 | username: string; 99 | 100 | @CreateApiPropertyDoc('名称') 101 | @Rule(RuleType.string().trim().min(5).max(255).required()) 102 | name: string; 103 | 104 | @CreateApiPropertyDoc('头像') 105 | @Rule(RuleType.string().trim().max(255).optional()) 106 | avatar?: string; 107 | 108 | @CreateApiPropertyDoc('密码(数据库入库前会进行加密),前端做二次确认校验') 109 | @Rule(RuleType.string().trim().min(5).max(60).optional()) 110 | password?: string; 111 | 112 | @CreateApiPropertyDoc('关联的角色') 113 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).optional()) 114 | roles?: string[]; 115 | 116 | @CreateApiPropertyDoc('关联的权限') 117 | @Rule(RuleType.array().items(RuleType.string().trim().max(10)).optional()) 118 | permissions?: string[]; 119 | } 120 | -------------------------------------------------------------------------------- /src/app/dto/auth.ts: -------------------------------------------------------------------------------- 1 | import { Rule, RuleType } from '@midwayjs/decorator'; 2 | import { CreateApiPropertyDoc } from '@midwayjs/swagger'; 3 | 4 | export class LoginDTO { 5 | @CreateApiPropertyDoc('管理员帐号') 6 | @Rule(RuleType.string().required().min(5).max(190)) 7 | username: string; 8 | 9 | @CreateApiPropertyDoc('密码') 10 | @Rule(RuleType.string().required().min(5).max(60)) 11 | password: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/extend/helper.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcryptjs'; 2 | import * as moment from 'moment'; 3 | import * as meeko from 'meeko'; 4 | 5 | moment.locale('zh-cn'); 6 | 7 | // 基本请求 8 | // interface Response { 9 | // success: boolean // if request is success 10 | // data: T // response data 11 | // code?: number // code for errorType 12 | // message?: string // message display to user 13 | // showType?: number // error display type: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 page 14 | // traceId?: string // Convenient for back-end Troubleshooting: unique request ID 15 | // host?: string // onvenient for backend Troubleshooting: host of current access server 16 | // } 17 | 18 | // 分页 19 | // interface PagingData { 20 | // current: number 21 | // pageSize: number 22 | // total: number 23 | // list: T[] 24 | // } 25 | 26 | export = { 27 | /** 28 | * 密文转hash 29 | * @method Helper#bhash 30 | * @param {String} str 需要加密的内容 31 | * @returns {String} 密文 32 | */ 33 | bhash(str: string) { 34 | return bcrypt.hashSync(str, 10); 35 | }, 36 | /** 37 | * hash是否正确 38 | * @param {String} str 需要匹配的内容 39 | * @param {String} hash hash值 40 | * @returns {Boolean} 是否匹配 41 | */ 42 | bcompare(str: string, hash: string) { 43 | return bcrypt.compareSync(str, hash); 44 | }, 45 | 46 | /** 47 | * 对比两个数组差异 48 | * @method Helper#arrayDiff 49 | * @param {(string | number)[]} arrA 数组A 50 | * @param {(string | number)[]} arrB 数组B 51 | * @returns {[increase: (string | number)[], decrease: (string | number)[]]} [increase, decrease] 52 | */ 53 | arrayDiff(arrA: (string | number)[], arrB: (string | number)[]) { 54 | const intersect = meeko.array.intersect(arrA, arrB); 55 | const increase = meeko.array.except(arrA, intersect); 56 | const decrease = meeko.array.except(arrB, intersect); 57 | return [increase, decrease]; 58 | }, 59 | /** 60 | * 处理成功响应 61 | * @method Helper#success 62 | * @param {any} result Return data, Default null 63 | * @param {String} message Error message, Default '请求成功' 64 | * @param {Number} status Status code, Default '200' 65 | * 66 | * @example 67 | * ```js 68 | * ctx.helper.success({}, null, 201); 69 | * ``` 70 | */ 71 | success(this: any, result = null, message = '请求成功', status = 200) { 72 | this.ctx.body = { 73 | code: status, 74 | message, 75 | data: result, 76 | }; 77 | this.ctx.status = status; 78 | }, 79 | 80 | /** 81 | * 处理失败响应(未使用,暂时注释) 82 | * @param ctx 83 | * @param code 84 | * @param message 85 | */ 86 | // error(code: number, message: string) { 87 | // this.ctx.body = { 88 | // code, 89 | // message, 90 | // data: null, 91 | // }; 92 | // this.ctx.status = code; 93 | // }, 94 | }; 95 | -------------------------------------------------------------------------------- /src/app/middleware/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { Provide } from '@midwayjs/decorator'; 2 | import { 3 | IWebMiddleware, 4 | IMidwayWebNext, 5 | MidwayWebMiddleware, 6 | } from '@midwayjs/web'; 7 | 8 | import { Context } from '@/interface'; 9 | 10 | import MyError from '../util/my-error'; 11 | 12 | @Provide() 13 | export class ErrorHandlerMiddleware implements IWebMiddleware { 14 | resolve(): MidwayWebMiddleware { 15 | return errHandleMiddleware; 16 | } 17 | } 18 | 19 | async function errHandleMiddleware( 20 | ctx: Context, 21 | next: IMidwayWebNext 22 | ): Promise { 23 | try { 24 | await next(); 25 | if (ctx.status === 404) { 26 | ctx.body = { code: 404, message: 'Not Found' }; 27 | } 28 | } catch (err) { 29 | // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 30 | ctx.app.emit('error', err, ctx); 31 | 32 | const myerr = err as MyError; 33 | 34 | // 兼容运行ci的时候,assert抛出的错误为AssertionError没有status 35 | const [message, messageStatus] = myerr.message?.split(' &>'); 36 | 37 | let status = myerr.status || parseInt(messageStatus) || 500; 38 | if (myerr.name === 'ValidationError' || message === 'ValidationError') { 39 | status = 422; 40 | } 41 | 42 | ctx._internalError = myerr; 43 | 44 | // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 45 | const error = 46 | status === 500 && ctx.app.config.env === 'prod' 47 | ? 'Internal Server Error' 48 | : message; 49 | 50 | // 从 error 对象上读出各个属性,设置到响应中 51 | ctx.body = { code: status, message: error }; 52 | if (status === 422) { 53 | ctx.body.data = myerr.errors || myerr.details; // 兼容 midway 参数校验 54 | } 55 | ctx.status = status; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/middleware/jwt-auth.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { Provide } from '@midwayjs/decorator'; 4 | import { IWebMiddleware, MidwayWebMiddleware } from '@midwayjs/web'; 5 | 6 | import { IMidwayWebNext, Context } from '../../interface'; 7 | import MyError from '../util/my-error'; 8 | 9 | @Provide() 10 | export class JwtAuth implements IWebMiddleware { 11 | resolve(): MidwayWebMiddleware { 12 | return authMiddleware; 13 | } 14 | } 15 | 16 | async function authMiddleware( 17 | ctx: Context, 18 | next: IMidwayWebNext 19 | ): Promise { 20 | if (!ctx.currentUser) { 21 | ctx.currentUser = {}; 22 | } 23 | 24 | if (ctx.jwtState.user) { 25 | const { user } = ctx.jwtState; 26 | const { jwtAuth } = ctx.app.config; 27 | 28 | // redisToken不存在表示token已过期 29 | const redisToken = await ctx.app.redis.get( 30 | `${jwtAuth.redisScope}:accessToken:${user.id}` 31 | ); 32 | 33 | const [, token] = ctx.header.authorization.split(' '); 34 | // 验证是否为最新的token 35 | assert.ok(token === redisToken, new MyError('Authentication Failed', 401)); 36 | 37 | const userinfo = await ctx.app.redis.get( 38 | `${jwtAuth.redisScope}:userinfo:${user.id}` 39 | ); 40 | 41 | ctx.currentUser = JSON.parse(userinfo); 42 | } 43 | 44 | return next(); 45 | } 46 | -------------------------------------------------------------------------------- /src/app/middleware/request-id.ts: -------------------------------------------------------------------------------- 1 | import { Provide } from '@midwayjs/decorator'; 2 | import { 3 | IWebMiddleware, 4 | IMidwayWebNext, 5 | MidwayWebMiddleware, 6 | } from '@midwayjs/web'; 7 | import { HeadersKey } from '@mw-components/jaeger'; 8 | import { KoidComponent } from '@mw-components/koid'; 9 | 10 | import { Context } from '@/interface'; 11 | 12 | @Provide() 13 | export class RequestIdMiddleware implements IWebMiddleware { 14 | resolve(): MidwayWebMiddleware { 15 | return requestIdMiddleware; 16 | } 17 | } 18 | 19 | async function requestIdMiddleware( 20 | ctx: Context, 21 | next: IMidwayWebNext 22 | ): Promise { 23 | const key = HeadersKey.reqId; 24 | let reqId = ctx.get(key); 25 | 26 | if (reqId) { 27 | ctx.reqId = reqId; 28 | } else { 29 | const koid = await ctx.requestContext.getAsync(KoidComponent); 30 | reqId = koid.idGenerator.toString(); 31 | ctx.reqId = reqId; 32 | } 33 | 34 | ctx.set(key, reqId); 35 | 36 | await next(); 37 | } 38 | -------------------------------------------------------------------------------- /src/app/model/admin-menu.ts: -------------------------------------------------------------------------------- 1 | import { EntityModel } from '@midwayjs/orm'; 2 | import { 3 | Column, 4 | AfterLoad, 5 | PrimaryGeneratedColumn, 6 | ManyToMany, 7 | ManyToOne, 8 | JoinTable, 9 | JoinColumn, 10 | } from 'typeorm'; 11 | 12 | import { BaseModel } from './base'; 13 | import { AdminRoleModel } from './admin-role'; 14 | import { AdminPermissionModel } from './admin-permission'; 15 | 16 | @EntityModel({ 17 | name: 'admin_menu', 18 | }) 19 | export class AdminMenuModel extends BaseModel { 20 | @PrimaryGeneratedColumn({ 21 | type: 'integer', 22 | }) 23 | id: string; 24 | 25 | @Column({ 26 | type: 'integer', 27 | name: 'parent_id', 28 | comment: '父级ID', 29 | }) 30 | parentId: string; 31 | 32 | @Column({ 33 | type: 'integer', 34 | name: 'permission_id', 35 | comment: '权限ID', 36 | }) 37 | permissionId: string; 38 | 39 | @Column({ 40 | type: 'int', 41 | comment: '排序,数值越大越靠后', 42 | }) 43 | order: number; 44 | 45 | @Column({ 46 | type: 'varchar', 47 | length: 50, 48 | }) 49 | title: string; 50 | 51 | @Column({ 52 | type: 'varchar', 53 | length: 255, 54 | comment: '路径', 55 | }) 56 | uri: string; 57 | 58 | @ManyToMany(type => AdminRoleModel, role => role.menu) 59 | @JoinTable({ 60 | name: 'admin_role_menu', 61 | joinColumn: { 62 | name: 'menu_id', 63 | referencedColumnName: 'id', 64 | }, 65 | inverseJoinColumn: { 66 | name: 'role_id', 67 | referencedColumnName: 'id', 68 | }, 69 | }) 70 | roles: AdminRoleModel[]; 71 | 72 | @ManyToOne(type => AdminPermissionModel, permission => permission.menu) 73 | @JoinColumn({ 74 | name: 'permission_id', 75 | referencedColumnName: 'id', 76 | }) 77 | permission: AdminPermissionModel; 78 | 79 | @AfterLoad() 80 | mixin() { 81 | this.parentId = String(this.parentId); 82 | this.permissionId = String(this.permissionId); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/model/admin-permission.ts: -------------------------------------------------------------------------------- 1 | import { EntityModel } from '@midwayjs/orm'; 2 | import { 3 | Column, 4 | AfterLoad, 5 | PrimaryGeneratedColumn, 6 | ManyToMany, 7 | OneToMany, 8 | JoinTable, 9 | BeforeInsert, 10 | BeforeUpdate, 11 | } from 'typeorm'; 12 | 13 | import { BaseModel } from './base'; 14 | import { AdminRoleModel } from './admin-role'; 15 | import { AdminUserModel } from './admin-user'; 16 | import { AdminMenuModel } from './admin-menu'; 17 | 18 | @EntityModel({ 19 | name: 'admin_permissions', 20 | }) 21 | export class AdminPermissionModel extends BaseModel { 22 | @PrimaryGeneratedColumn({ 23 | type: 'integer', 24 | }) 25 | id: string; 26 | 27 | @Column({ 28 | type: 'varchar', 29 | length: 50, 30 | comment: '名称', 31 | }) 32 | name: string; 33 | 34 | @Column({ 35 | type: 'varchar', 36 | comment: '标识', 37 | }) 38 | slug: string; 39 | 40 | @Column({ 41 | type: 'varchar', 42 | name: 'http_method', 43 | comment: 44 | '请求方式 ["ANY", "DELETE", "POST", "GET", "PUT", "PATCH", "OPTIONS", "HEAD"]', 45 | }) 46 | httpMethod: string[]; 47 | 48 | @Column({ 49 | type: 'text', 50 | name: 'http_path', 51 | comment: '请求路径', 52 | }) 53 | httpPath: string; 54 | 55 | @ManyToMany(type => AdminRoleModel, role => role.permissions) 56 | @JoinTable({ 57 | name: 'admin_role_permissions', 58 | joinColumn: { 59 | name: 'permission_id', 60 | referencedColumnName: 'id', 61 | }, 62 | inverseJoinColumn: { 63 | name: 'role_id', 64 | referencedColumnName: 'id', 65 | }, 66 | }) 67 | roles: AdminRoleModel[]; 68 | 69 | @ManyToMany(type => AdminUserModel, user => user.permissions) 70 | @JoinTable({ 71 | name: 'admin_user_permissions', 72 | joinColumn: { 73 | name: 'permission_id', 74 | referencedColumnName: 'id', 75 | }, 76 | inverseJoinColumn: { 77 | name: 'user_id', 78 | referencedColumnName: 'id', 79 | }, 80 | }) 81 | users: AdminUserModel[]; 82 | 83 | @OneToMany(type => AdminMenuModel, menu => menu.permission) 84 | menu: AdminMenuModel[]; 85 | 86 | @AfterLoad() 87 | mixin() { 88 | this.httpMethod = this.httpMethod 89 | ? this.httpMethod.toString().split(',') 90 | : []; 91 | } 92 | 93 | @BeforeInsert() 94 | @BeforeUpdate() 95 | before() { 96 | this.httpMethod = (this.httpMethod.join(',') as unknown) as string[]; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/app/model/admin-role.ts: -------------------------------------------------------------------------------- 1 | import { EntityModel } from '@midwayjs/orm'; 2 | import { Column, PrimaryGeneratedColumn, ManyToMany } from 'typeorm'; 3 | 4 | import { BaseModel } from './base'; 5 | import { AdminPermissionModel } from './admin-permission'; 6 | import { AdminUserModel } from './admin-user'; 7 | import { AdminMenuModel } from './admin-menu'; 8 | 9 | @EntityModel({ 10 | name: 'admin_roles', 11 | }) 12 | export class AdminRoleModel extends BaseModel { 13 | @PrimaryGeneratedColumn({ 14 | type: 'integer', 15 | }) 16 | id: string; 17 | 18 | @Column({ 19 | type: 'varchar', 20 | length: 50, 21 | comment: '名称', 22 | }) 23 | name: string; 24 | 25 | @Column({ 26 | type: 'varchar', 27 | comment: '标识', 28 | }) 29 | slug: string; 30 | 31 | @ManyToMany(type => AdminPermissionModel, permission => permission.roles) 32 | permissions: AdminPermissionModel[]; 33 | 34 | @ManyToMany(type => AdminUserModel, user => user.roles) 35 | users: AdminUserModel[]; 36 | 37 | @ManyToMany(type => AdminMenuModel, menu => menu.roles) 38 | menu: AdminUserModel[]; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/model/admin-user.ts: -------------------------------------------------------------------------------- 1 | import { EntityModel } from '@midwayjs/orm'; 2 | import { Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm'; 3 | 4 | import { BaseModel } from './base'; 5 | import { AdminRoleModel } from './admin-role'; 6 | import { AdminPermissionModel } from './admin-permission'; 7 | 8 | @EntityModel({ 9 | name: 'admin_users', 10 | }) 11 | export class AdminUserModel extends BaseModel { 12 | @PrimaryGeneratedColumn({ 13 | type: 'bigint', 14 | }) 15 | id: string; 16 | 17 | @Column({ 18 | type: 'varchar', 19 | length: 190, 20 | comment: '用户名', 21 | }) 22 | username: string; 23 | 24 | @Column({ 25 | type: 'varchar', 26 | length: 60, 27 | nullable: true, 28 | comment: '密码', 29 | }) 30 | password: string; 31 | 32 | @Column({ 33 | type: 'varchar', 34 | comment: '名称', 35 | }) 36 | name: string; 37 | 38 | @Column({ 39 | type: 'varchar', 40 | length: 255, 41 | comment: '头像', 42 | }) 43 | avatar: string; 44 | 45 | @Column({ 46 | type: 'varchar', 47 | length: 100, 48 | comment: '记住token', 49 | name: 'remember_token', 50 | }) 51 | rememberToken: string; 52 | 53 | @ManyToMany(type => AdminRoleModel, role => role.users) 54 | @JoinTable({ 55 | name: 'admin_role_users', 56 | joinColumn: { 57 | name: 'user_id', 58 | referencedColumnName: 'id', 59 | }, 60 | inverseJoinColumn: { 61 | name: 'role_id', 62 | referencedColumnName: 'id', 63 | }, 64 | }) 65 | roles: AdminRoleModel[]; 66 | 67 | @ManyToMany(type => AdminPermissionModel, permission => permission.users) 68 | permissions: AdminPermissionModel[]; 69 | } 70 | -------------------------------------------------------------------------------- /src/app/model/base.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateDateColumn, 3 | UpdateDateColumn, 4 | // DeleteDateColumn, // 软删除需要引入 5 | AfterLoad, 6 | } from 'typeorm'; 7 | 8 | /** 9 | * 基础的Model,对id字段默认会 转字符串处理 10 | * 11 | * 继承该Model的话,必须是有id字段的表 12 | * 13 | * 默认还会有createdAt、updatedAt 14 | */ 15 | export class BaseModel { 16 | id: string; 17 | 18 | @CreateDateColumn({ 19 | name: 'created_at', 20 | }) 21 | createdAt: Date; 22 | 23 | @UpdateDateColumn({ 24 | name: 'updated_at', 25 | }) 26 | updatedAt: Date; 27 | 28 | // 软删除默认需要配置的字段 29 | // @DeleteDateColumn({ 30 | // name: 'deleted_at', 31 | // select: false, 32 | // }) 33 | // deletedAt: Date; 34 | 35 | // 对字段进行预处理 36 | @AfterLoad() 37 | init() { 38 | this.id = String(this.id); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/service/admin/menu.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { Provide, Inject } from '@midwayjs/decorator'; 4 | import { InjectEntityModel } from '@midwayjs/orm'; 5 | import { Repository, In } from 'typeorm'; 6 | 7 | import { Context } from '@/interface'; 8 | 9 | import { AdminMenuModel } from '../../model/admin-menu'; 10 | import { CreateDTO, QueryDTO, UpdateDTO } from '../../dto/admin/menu'; 11 | import { AdminRoleModel } from '../../model/admin-role'; 12 | import MyError from '../../util/my-error'; 13 | 14 | @Provide() 15 | export class AdminMenuService { 16 | @Inject() 17 | ctx: Context; 18 | 19 | @InjectEntityModel(AdminMenuModel) 20 | adminMenuModel: Repository; 21 | 22 | /** 23 | * 分页查询菜单列表 24 | * @param {QueryDTO} params 查询参数 25 | */ 26 | async queryAdminMenu(params: QueryDTO) { 27 | const { pageSize, current } = params; 28 | const order: any = { order: 'DESC' }; 29 | 30 | // 排序方式 31 | const [list, total] = await this.adminMenuModel.findAndCount({ 32 | order, 33 | take: pageSize, 34 | skip: pageSize * (current - 1), 35 | }); 36 | 37 | return { 38 | current, 39 | pageSize, 40 | total, 41 | list, 42 | }; 43 | } 44 | 45 | /** 46 | * 根据菜单id获取数据 47 | * @param id 菜单id 48 | */ 49 | async getAdminMenuById(id: string) { 50 | const row = await this.adminMenuModel 51 | .createQueryBuilder() 52 | .select() 53 | .leftJoinAndSelect('AdminMenuModel.roles', 'role') 54 | .where({ id: id }) 55 | .getOne(); 56 | return row; 57 | } 58 | 59 | /** 60 | * 创建菜单 61 | * @param {CreateDTO} params 菜单参数 62 | */ 63 | async createAdminMenu(params: CreateDTO) { 64 | let menu = new AdminMenuModel(); 65 | 66 | // 预处理角色参数 67 | const roles = params.roles.map(item => { 68 | const role = new AdminRoleModel(); 69 | role.id = item; 70 | return role; 71 | }); 72 | 73 | menu = this.adminMenuModel.merge(menu, { ...params, roles: roles }); 74 | 75 | const created = await this.adminMenuModel.save(menu); 76 | return created; 77 | } 78 | 79 | /** 80 | * 更新菜单 81 | * @param {UpdateDTO} params 菜单参数 82 | */ 83 | async updateAdminMenu(params: UpdateDTO) { 84 | const { id, roles: newRoles, ...columns } = params; 85 | 86 | const menu = await this.getAdminMenuById(id); 87 | 88 | // 如果有传递roles 89 | if (newRoles) { 90 | const oldRoles = menu.roles.map(item => item.id); 91 | // 对比角色变更差异 92 | const [increase, decrease] = this.ctx.helper.arrayDiff( 93 | newRoles, 94 | oldRoles 95 | ); 96 | 97 | // 更新角色关联数据 98 | await Promise.all([ 99 | this.adminMenuModel 100 | .createQueryBuilder() 101 | .relation(AdminMenuModel, 'roles') 102 | .of(menu) 103 | .add(increase), 104 | this.adminMenuModel 105 | .createQueryBuilder() 106 | .relation(AdminMenuModel, 'roles') 107 | .of(menu) 108 | .remove(decrease), 109 | ]); 110 | } 111 | 112 | return this.adminMenuModel 113 | .createQueryBuilder() 114 | .update(menu) 115 | .set(columns) 116 | .where({ 117 | id: menu.id, 118 | }) 119 | .execute(); 120 | } 121 | 122 | /** 123 | * 删除多条菜单数据(忽略关联表的数据) 124 | * @param {string[]}ids 菜单id 125 | */ 126 | async removeAdminMenuByIds(ids: string[]) { 127 | return this.adminMenuModel 128 | .createQueryBuilder() 129 | .delete() 130 | .where({ 131 | id: In(ids), 132 | }) 133 | .execute(); 134 | } 135 | 136 | /** 137 | * 检查菜单是否存在于数据库,自动抛错 138 | * @param {string[]} ids 菜单id 139 | */ 140 | async checkMenuExists(ids: string[]) { 141 | const count = await this.adminMenuModel.count({ 142 | where: { 143 | id: In(ids), 144 | }, 145 | }); 146 | 147 | assert.deepStrictEqual( 148 | count, 149 | ids.length, 150 | new MyError('菜单不存在,请检查', 400) 151 | ); 152 | } 153 | 154 | /** 155 | * 批量更新菜单的排序和父级ID 156 | * @param params 菜单参数 157 | */ 158 | async orderAdminMenu(params: any[]) { 159 | const queue = params.map(item => { 160 | const { id, ...field } = item; 161 | return this.adminMenuModel 162 | .createQueryBuilder() 163 | .update(field) 164 | .where({ 165 | id, 166 | }) 167 | .execute(); 168 | }); 169 | return Promise.all(queue); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/app/service/admin/permission.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { Provide } from '@midwayjs/decorator'; 4 | import { InjectEntityModel } from '@midwayjs/orm'; 5 | import { Repository, Like, In } from 'typeorm'; 6 | 7 | import { AdminPermissionModel } from '../../model/admin-permission'; 8 | import { QueryDTO, CreateDTO, UpdateDTO } from '../../dto/admin/permission'; 9 | import MyError from '../../util/my-error'; 10 | 11 | @Provide() 12 | export class AdminPermissionService { 13 | @InjectEntityModel(AdminPermissionModel) 14 | adminPermissionModel: Repository; 15 | 16 | /** 17 | * 分页查询权限列表 18 | * @param {QueryDTO} params 19 | */ 20 | async queryAdminPermission(params: QueryDTO) { 21 | const { pageSize, current, sorter, ...filter } = params; 22 | const where: any = {}; 23 | const order: any = { id: 'DESC' }; 24 | 25 | // 排序方式 26 | if (sorter) { 27 | const [column, sort] = sorter.split('_'); 28 | order[column] = sort.split('end')[0].toUpperCase(); 29 | } 30 | 31 | // 模糊匹配id 32 | if (filter.id) { 33 | where.id = Like(filter.id); 34 | } 35 | 36 | // 模糊匹配名称 37 | if (filter.name) { 38 | where.name = Like(`${filter.name}%`); 39 | } 40 | 41 | // 模糊匹配标识 42 | if (filter.slug) { 43 | where.slug = Like(`${filter.slug}%`); 44 | } 45 | 46 | // 模糊匹配路径 47 | if (filter.httpPath) { 48 | where.httpPath = Like(`${filter.httpPath}%`); 49 | } 50 | 51 | // 模糊匹配请求方式 52 | if (filter.httpMethod) { 53 | where.httpMethod = Like(`${filter.httpMethod}%`); 54 | } 55 | 56 | const [list, total] = await this.adminPermissionModel.findAndCount({ 57 | where, 58 | order, 59 | take: pageSize, 60 | skip: pageSize * (current - 1), 61 | }); 62 | 63 | return { 64 | current, 65 | pageSize, 66 | total, 67 | list, 68 | }; 69 | } 70 | 71 | /** 72 | * 通过ID获取单条权限数据 73 | * @param {String} id 74 | */ 75 | async getAdminPermissionById(id: string) { 76 | const row = await this.adminPermissionModel 77 | .createQueryBuilder() 78 | .select() 79 | .leftJoinAndSelect( 80 | 'AdminPermissionModel.roles', 81 | 'role' 82 | // 'role.deletedAt IS NULL' 软删除关联查询需要的例子 83 | ) 84 | .leftJoinAndSelect('AdminPermissionModel.menu', 'menu') 85 | .where({ id: id }) 86 | .getOne(); 87 | return row; 88 | } 89 | 90 | /** 91 | * 创建权限 92 | * @param {CreateDTO} params 权限参数 93 | */ 94 | async createAdminPermission(params: CreateDTO) { 95 | let permission = new AdminPermissionModel(); 96 | permission = this.adminPermissionModel.merge(permission, params); 97 | 98 | const created = await this.adminPermissionModel.save(permission); 99 | return created; 100 | } 101 | 102 | /** 103 | * 更新权限 104 | * @param {UpdateDTO} params 更新权限参数 105 | */ 106 | async updateAdminPermission(params: UpdateDTO) { 107 | const { id, ...columns } = params; 108 | return this.adminPermissionModel 109 | .createQueryBuilder() 110 | .update() 111 | .set(columns) 112 | .where('id = :id', { id }) 113 | .execute(); 114 | } 115 | 116 | /** 117 | * 批量删除多条权限数据(忽略关联表的数据) 118 | * @param {string[]} ids 权限id 119 | */ 120 | async removeAdminPermissionByIds(ids: string[]) { 121 | return ( 122 | this.adminPermissionModel 123 | .createQueryBuilder() 124 | // .softDelete() // 软删除例子 125 | .delete() 126 | .where({ 127 | id: In(ids), 128 | }) 129 | .execute() 130 | ); 131 | } 132 | 133 | /** 134 | * 检查权限是否存在于数据库,自动抛错 135 | * @param {string[]} ids 权限id 136 | */ 137 | async checkPermissionExists(ids: string[] = []) { 138 | const count = await this.adminPermissionModel.count({ 139 | where: { 140 | id: In(ids), 141 | }, 142 | }); 143 | 144 | assert.deepStrictEqual( 145 | count, 146 | ids.length, 147 | new MyError('权限不存在,请检查', 400) 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/app/service/admin/role.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { Provide, Inject } from '@midwayjs/decorator'; 4 | import { InjectEntityModel } from '@midwayjs/orm'; 5 | import { Repository, Like, In } from 'typeorm'; 6 | 7 | import { Context } from '@/interface'; 8 | 9 | import { AdminPermissionModel } from '../../model/admin-permission'; 10 | import { AdminRoleModel } from '../../model/admin-role'; 11 | import { QueryDTO, CreateDTO, UpdateDTO } from '../../dto/admin/role'; 12 | import MyError from '../../util/my-error'; 13 | 14 | @Provide() 15 | export class AdminRoleService { 16 | @Inject() 17 | ctx: Context; 18 | 19 | @InjectEntityModel(AdminRoleModel) 20 | adminRoleModel: Repository; 21 | 22 | /** 23 | * 分页查询角色列表 24 | * @param {QueryDTO} params 25 | */ 26 | async queryAdminRole(params: QueryDTO) { 27 | const { pageSize, current, sorter, ...filter } = params; 28 | const where: any = {}; 29 | const order: any = { id: 'DESC' }; 30 | 31 | // 排序方式 32 | if (sorter) { 33 | const [column, sort] = sorter.split('_'); 34 | order[column] = sort.split('end')[0].toUpperCase(); 35 | } 36 | 37 | // 模糊匹配id 38 | if (filter.id) { 39 | where.id = Like(filter.id); 40 | } 41 | 42 | // 模糊匹配名称 43 | if (filter.name) { 44 | where.name = Like(`${filter.name}%`); 45 | } 46 | 47 | // 模糊匹配标识 48 | if (filter.slug) { 49 | where.slug = Like(`${filter.slug}%`); 50 | } 51 | 52 | const [list, total] = await this.adminRoleModel.findAndCount({ 53 | where, 54 | order, 55 | take: pageSize, 56 | skip: pageSize * (current - 1), 57 | }); 58 | 59 | return { 60 | current, 61 | pageSize, 62 | total, 63 | list, 64 | }; 65 | } 66 | 67 | /** 68 | * 通过ID获取单条角色数据 69 | * @param {String} id 70 | */ 71 | async getAdminRoleById(id: string) { 72 | const row = await this.adminRoleModel 73 | .createQueryBuilder() 74 | .select() 75 | .leftJoinAndSelect('AdminRoleModel.permissions', 'permission') 76 | .leftJoinAndSelect('AdminRoleModel.menu', 'menu') 77 | .where({ id: id }) 78 | .getOne(); 79 | return row; 80 | } 81 | 82 | /** 83 | * 创建角色 84 | * @param {CreateDTO} params 角色参数 85 | */ 86 | async createAdminRole(params: CreateDTO) { 87 | let role = new AdminRoleModel(); 88 | 89 | const permissions = params.permissions 90 | ? params.permissions.map(item => { 91 | const permission = new AdminPermissionModel(); 92 | permission.id = item; 93 | return permission; 94 | }) 95 | : []; 96 | 97 | role = this.adminRoleModel.merge(role, { 98 | ...params, 99 | permissions: permissions, 100 | }); 101 | 102 | const created = await this.adminRoleModel.save(role); 103 | return created; 104 | } 105 | 106 | /** 107 | * 更新角色 108 | * @param {UpdateDTO} params 109 | */ 110 | async updateAdminRole(params: UpdateDTO) { 111 | const { id, permissions: newPermissions, ...column } = params; 112 | const role = await this.getAdminRoleById(id); 113 | 114 | // 如果有传递permissions 115 | if (newPermissions) { 116 | const oldPermissions = role.permissions.map(item => item.id); 117 | 118 | const [increase, decrease] = this.ctx.helper.arrayDiff( 119 | newPermissions, 120 | oldPermissions 121 | ); 122 | 123 | await Promise.all([ 124 | this.adminRoleModel 125 | .createQueryBuilder() 126 | .relation(AdminRoleModel, 'permissions') 127 | .of(role) 128 | .add(increase), 129 | this.adminRoleModel 130 | .createQueryBuilder() 131 | .relation(AdminRoleModel, 'permissions') 132 | .of(role) 133 | .remove(decrease), 134 | ]); 135 | } 136 | 137 | return this.adminRoleModel 138 | .createQueryBuilder() 139 | .update(role) 140 | .set(column) 141 | .where({ id: id }) 142 | .execute(); 143 | } 144 | 145 | /** 146 | * 删除多条角色数据(忽略关联表的数据) 147 | * @param {string[]}ids 角色id 148 | */ 149 | async removeAdminRoleByIds(ids: string[]) { 150 | return this.adminRoleModel 151 | .createQueryBuilder() 152 | .delete() 153 | .where({ 154 | id: In(ids), 155 | }) 156 | .execute(); 157 | } 158 | 159 | /** 160 | * 检查角色是否存在于数据库,自动抛错 161 | * @param {string[]} ids 角色id 162 | */ 163 | async checkRoleExists(ids: string[] = []) { 164 | const count = await this.adminRoleModel.count({ 165 | where: { 166 | id: In(ids), 167 | }, 168 | }); 169 | 170 | assert.deepStrictEqual( 171 | count, 172 | ids.length, 173 | new MyError('角色不存在,请检查', 400) 174 | ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/app/service/admin/user.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { Provide, Inject } from '@midwayjs/decorator'; 4 | import { InjectEntityModel } from '@midwayjs/orm'; 5 | import { Repository, Like, In } from 'typeorm'; 6 | 7 | import { Context } from '@/interface'; 8 | 9 | import { AdminUserModel } from '../../model/admin-user'; 10 | import { AdminRoleModel } from '../../model/admin-role'; 11 | import { AdminPermissionModel } from '../../model/admin-permission'; 12 | import { QueryDTO, CreateDTO, UpdateDTO } from '../../dto/admin/user'; 13 | import MyError from '../../util/my-error'; 14 | 15 | @Provide() 16 | export class AdminUserService { 17 | @Inject() 18 | ctx: Context; 19 | 20 | @InjectEntityModel(AdminUserModel) 21 | adminUserModel: Repository; 22 | 23 | /** 24 | * 分页查询管理员列表 25 | * @param {QueryDTO} params 26 | */ 27 | async queryAdminUser(params: QueryDTO) { 28 | const { pageSize, current, sorter, ...filter } = params; 29 | const where: any = {}; 30 | const order: any = { id: 'DESC' }; 31 | 32 | // 排序方式 33 | if (sorter) { 34 | const [column, sort] = sorter.split('_'); 35 | order[column] = sort.split('end')[0].toUpperCase(); 36 | } 37 | 38 | // 模糊匹配id 39 | if (filter.id) { 40 | where.id = Like(filter.id); 41 | } 42 | 43 | // 模糊匹配名称 44 | if (filter.name) { 45 | where.name = Like(`${filter.name}%`); 46 | } 47 | 48 | // 模糊匹配标识 49 | if (filter.username) { 50 | where.username = Like(`${filter.username}%`); 51 | } 52 | 53 | const [list, total] = await this.adminUserModel.findAndCount({ 54 | where, 55 | order, 56 | take: pageSize, 57 | skip: pageSize * (current - 1), 58 | }); 59 | 60 | return { 61 | current, 62 | pageSize, 63 | total, 64 | list, 65 | }; 66 | } 67 | 68 | /** 69 | * 根据管理员id获取数据 70 | * @param id 管理员id 71 | */ 72 | async getAdminUserById(id: string) { 73 | const row = await this.adminUserModel 74 | .createQueryBuilder() 75 | .select() 76 | .leftJoinAndSelect('AdminUserModel.roles', 'role') 77 | .leftJoinAndSelect('AdminUserModel.permissions', 'permission') 78 | .where({ id: id }) 79 | .getOne(); 80 | return row; 81 | } 82 | 83 | /** 84 | * 创建管理员 85 | * @param {CreateDTO} params 创建参数 86 | */ 87 | async createAdminUser(params: CreateDTO) { 88 | let user = new AdminUserModel(); 89 | 90 | // 预处理角色参数 91 | const roles = params.roles 92 | ? params.roles.map(item => { 93 | const role = new AdminRoleModel(); 94 | role.id = item; 95 | return role; 96 | }) 97 | : []; 98 | 99 | // 预处理权限参数 100 | const permissions = params.permissions 101 | ? params.permissions.map(item => { 102 | const role = new AdminPermissionModel(); 103 | role.id = item; 104 | return role; 105 | }) 106 | : []; 107 | 108 | user = this.adminUserModel.merge(user, { 109 | ...params, 110 | roles: roles, 111 | permissions: permissions, 112 | }); 113 | 114 | const created = await this.adminUserModel.save(user); 115 | return created; 116 | } 117 | 118 | /** 119 | * 更新管理员 120 | * @param {UpdateDTO} params 更新参数 121 | */ 122 | async updateAdminUser(params: UpdateDTO) { 123 | const { 124 | id, 125 | roles: newRoles, 126 | permissions: newPermissions, 127 | password, 128 | ...columns 129 | } = params; 130 | 131 | const user = await this.getAdminUserById(id); 132 | 133 | // 处理密码更新 134 | let newPassword = user.password; 135 | if (password) { 136 | password !== user.password && 137 | (newPassword = this.ctx.helper.bhash(password)); 138 | } 139 | 140 | // 如果有传递roles 141 | if (newRoles) { 142 | const oldRoles = user.roles.map(item => item.id); 143 | // 对比角色变更差异 144 | const [increase, decrease] = this.ctx.helper.arrayDiff( 145 | newRoles, 146 | oldRoles 147 | ); 148 | 149 | // 更新角色关联数据 150 | await Promise.all([ 151 | this.adminUserModel 152 | .createQueryBuilder() 153 | .relation(AdminUserModel, 'roles') 154 | .of(user) 155 | .add(increase), 156 | this.adminUserModel 157 | .createQueryBuilder() 158 | .relation(AdminUserModel, 'roles') 159 | .of(user) 160 | .remove(decrease), 161 | ]); 162 | } 163 | 164 | // 如果有传递permissions 165 | if (newPermissions) { 166 | const oldPermissions = user.permissions.map(item => item.id); 167 | // 对比权限变更差异 168 | const [increase, decrease] = this.ctx.helper.arrayDiff( 169 | newPermissions, 170 | oldPermissions 171 | ); 172 | 173 | // 更新权限关联数据 174 | await Promise.all([ 175 | this.adminUserModel 176 | .createQueryBuilder() 177 | .relation(AdminUserModel, 'permissions') 178 | .of(user) 179 | .add(increase), 180 | this.adminUserModel 181 | .createQueryBuilder() 182 | .relation(AdminUserModel, 'permissions') 183 | .of(user) 184 | .remove(decrease), 185 | ]); 186 | } 187 | 188 | return this.adminUserModel 189 | .createQueryBuilder() 190 | .update(user) 191 | .set({ ...columns, password: newPassword }) 192 | .where({ id: id }) 193 | .execute(); 194 | } 195 | 196 | /** 197 | * 删除多条管理员数据(忽略关联表的数据) 198 | * @param {string[]}ids 管理员id 199 | */ 200 | async removeAdminUserByIds(ids: string[]) { 201 | return this.adminUserModel 202 | .createQueryBuilder() 203 | .delete() 204 | .where({ 205 | id: In(ids), 206 | }) 207 | .execute(); 208 | } 209 | 210 | /** 211 | * 检查管理员是否存在于数据库,自动抛错 212 | * @param {string[]} ids 管理员id 213 | */ 214 | async checkUserExists(ids: string[]) { 215 | const count = await this.adminUserModel.count({ 216 | where: { 217 | id: In(ids), 218 | }, 219 | }); 220 | 221 | assert.deepStrictEqual( 222 | count, 223 | ids.length, 224 | new MyError('管理员不存在,请检查', 400) 225 | ); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/app/service/auth.ts: -------------------------------------------------------------------------------- 1 | import { Provide, Plugin, Inject, Config } from '@midwayjs/decorator'; 2 | import { InjectEntityModel } from '@midwayjs/orm'; 3 | import { JwtComponent } from '@mw-components/jwt'; 4 | import { Redis } from 'ioredis'; 5 | import { Repository } from 'typeorm'; 6 | 7 | import { Context } from '@/interface'; 8 | 9 | import { JwtAuthMiddlewareConfig } from '../../config/config.types'; 10 | import { AdminUserModel } from '../model/admin-user'; 11 | 12 | @Provide() 13 | export class AuthService { 14 | @Inject() 15 | private ctx: Context; 16 | 17 | @Inject('jwt:jwtComponent') 18 | jwt: JwtComponent; 19 | 20 | @Config('jwtAuth') 21 | private jwtAuthConfig: JwtAuthMiddlewareConfig; 22 | 23 | @InjectEntityModel(AdminUserModel) 24 | private adminUserModel: Repository; 25 | 26 | @Plugin() 27 | private redis: Redis; 28 | 29 | /** 30 | * 生成Token(会缓存到Redis中) 31 | * @param {AdminUser} data 保存的数据 32 | * @returns {String} 生成的Token字符串 33 | */ 34 | async createAdminUserToken(data: AdminUserModel): Promise { 35 | const token: string = this.jwt.sign({ id: data.id }, '', { 36 | expiresIn: this.jwtAuthConfig.accessTokenExpiresIn, 37 | }); 38 | await this.redis.set( 39 | `${this.jwtAuthConfig.redisScope}:accessToken:${data.id}`, 40 | token, 41 | 'EX', 42 | this.jwtAuthConfig.accessTokenExpiresIn 43 | ); 44 | return token; 45 | } 46 | 47 | /** 48 | * 获取管理员Redis Token(弃用) 49 | * @param {String} id 管理员id 50 | * @returns {String} Redis中的Token 51 | */ 52 | async getAdminUserTokenById(id: string): Promise { 53 | return this.redis.get(`${this.jwtAuthConfig.redisScope}:accessToken:${id}`); 54 | } 55 | 56 | /** 57 | * 移除管理员Redis Token 58 | * @param {String} id 管理员id 59 | * @returns {number} 变更的数量 60 | */ 61 | async removeAdminUserTokenById(id: string): Promise { 62 | return this.redis.del(`${this.jwtAuthConfig.redisScope}:accessToken:${id}`); 63 | } 64 | 65 | /** 66 | * 根据登录名查找管理员 67 | * @param {String} username 登录名 68 | * @returns {AdminUserModel | null} 承载管理员的 Promise 对象 69 | */ 70 | async getAdminUserByUserName(username: string): Promise { 71 | const user = await this.adminUserModel.findOne({ 72 | where: { 73 | username, 74 | }, 75 | }); 76 | return user; 77 | } 78 | 79 | /** 80 | * 读取Redis缓存中的管理员信息(弃用) 81 | * @param {String} id 82 | * @returns {AdminUserModel} 管理员信息 83 | */ 84 | public async getAdminUserById(id: string): Promise { 85 | const userinfo = (await this.redis.get( 86 | `${this.jwtAuthConfig.redisScope}:userinfo:${id}` 87 | )) as string; 88 | return JSON.parse(userinfo) as AdminUserModel; 89 | } 90 | 91 | /** 92 | * 缓存管理员 93 | * @param {AdminUserModel} data 管理员数据 94 | * @returns {OK | null} 缓存处理结果 95 | */ 96 | async cacheAdminUser(data: AdminUserModel): Promise<'OK' | null> { 97 | const { id, username, name, avatar, createdAt, updatedAt } = data; 98 | 99 | const userinfo = { 100 | id, 101 | username, 102 | name, 103 | avatar, 104 | createdAt, 105 | updatedAt, 106 | }; 107 | 108 | return this.redis.set( 109 | `${this.jwtAuthConfig.redisScope}:userinfo:${userinfo.id}`, 110 | JSON.stringify(userinfo), 111 | 'EX', 112 | this.jwtAuthConfig.accessTokenExpiresIn 113 | ); 114 | } 115 | 116 | /** 117 | * 清理管理员缓存数据 118 | * @param {String} id 管理员id 119 | * @returns {number} 缓存处理结果 120 | */ 121 | async cleanAdminUserById(id: string): Promise { 122 | return this.redis.del(`${this.jwtAuthConfig.redisScope}:userinfo:${id}`); 123 | } 124 | 125 | /** 126 | * 使用帐号密码,本地化登录 127 | * @param {Object} params 包涵username、password等参数 128 | * @returns {AdminUserModel | null} 承载管理员的Promise对象 129 | */ 130 | async localHandler(params: { 131 | username: string; 132 | password: string; 133 | }): Promise { 134 | // 获取管理员函数 135 | const getAdminUser = (username: string) => { 136 | return this.getAdminUserByUserName(username); 137 | }; 138 | 139 | // 查询管理员是否在数据库中 140 | const existAdmiUser = await getAdminUser(params.username); 141 | // 管理员不存在 142 | if (!existAdmiUser) { 143 | return null; 144 | } 145 | // 匹配密码 146 | const passhash = existAdmiUser.password; 147 | const equal = this.ctx.helper.bcompare(params.password, passhash); 148 | if (!equal) { 149 | return null; 150 | } 151 | 152 | // 通过验证 153 | return existAdmiUser; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/app/service/rabbitmq.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Provide, 3 | Scope, 4 | ScopeEnum, 5 | Init, 6 | Config, 7 | Destroy, 8 | Autoload, 9 | } from '@midwayjs/decorator'; 10 | import * as amqp from 'amqp-connection-manager'; 11 | import { Channel, Connection } from 'amqplib'; 12 | 13 | import { RabbitmqConfig } from '../../config/config.types'; 14 | 15 | @Autoload() 16 | @Scope(ScopeEnum.Singleton) // Singleton 单例,全局唯一(进程级别) 17 | @Provide() 18 | export class RabbitmqService { 19 | private channel: Channel; // 通道 20 | 21 | private connection: Connection; // mq 连接 22 | 23 | @Config('rabbitmq') 24 | private rabbitmq: RabbitmqConfig; 25 | 26 | @Init() 27 | async connect() { 28 | // 创建连接 29 | // @FIXME type assert 30 | this.connection = (await amqp.connect( 31 | this.rabbitmq.url 32 | )) as unknown as Connection; 33 | 34 | // 创建 channel 35 | this.channel = await this.connection.createChannel(); 36 | 37 | // 绑定队列 38 | await this.channel.assertQueue('my-queue', { 39 | exclusive: true, 40 | autoDelete: true, 41 | }); 42 | // 绑定交换机 43 | await this.channel.bindQueue('my-queue', 'my-exchange', 'create'); 44 | 45 | return this.connection; 46 | } 47 | 48 | /** 49 | * 发送消息 50 | * @param queueName 队列名称 51 | * @param data 数据 52 | * @returns 53 | */ 54 | public async sendToQueue(queueName: string, data: any) { 55 | return this.channel.sendToQueue(queueName, Buffer.from(data), { 56 | // RabbitMQ关闭时,消息会被保存到磁盘 57 | persistent: true, 58 | }); 59 | } 60 | 61 | @Destroy() 62 | async close() { 63 | await this.channel.close(); 64 | await this.connection.close(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/util/common.ts: -------------------------------------------------------------------------------- 1 | import { NetworkInterfaceInfo, networkInterfaces } from 'os'; 2 | 3 | /** 4 | * 获取网络信息,不包括回环地址信息 5 | */ 6 | export function retrieveExternalNetWorkInfo(): NetworkInterfaceInfo[] { 7 | const ret = Object.entries(networkInterfaces()).reduce( 8 | (acc: NetworkInterfaceInfo[], curr) => { 9 | const [, nets] = curr; 10 | /* c8 ignore next 3 */ 11 | if (!nets) { 12 | return acc; 13 | } 14 | nets.forEach(net => { 15 | // Skip over internal (i.e. 127.0.0.1) addresses 16 | if (!net.internal) { 17 | acc.push(net); 18 | } 19 | }); 20 | return acc; 21 | }, 22 | [] 23 | ); 24 | 25 | return ret; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/util/custom-logger.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | // eslint-disable-next-line node/no-extraneous-import 3 | import { 4 | IMidwayLogger, 5 | MidwayContextLogger, 6 | MidwayTransformableInfo, 7 | // eslint-disable-next-line node/no-extraneous-import 8 | } from '@midwayjs/logger'; 9 | import { genISO8601String } from '@waiting/shared-core'; 10 | 11 | import { Application, Context } from '@/interface'; 12 | 13 | class CustomContextLogger extends MidwayContextLogger { 14 | formatContextLabel() { 15 | const { ctx } = this; 16 | // format: '[$userId/$ip/$traceId/$use_ms $method $url]' 17 | const userId = (ctx.userId as string) || '-'; 18 | const traceId = ctx.reqId || '-'; 19 | const use = Date.now() - ctx.startTime; 20 | const ret = 21 | userId + 22 | '/' + 23 | ctx.ip + 24 | '/' + 25 | traceId + 26 | '/' + 27 | use.toString() + 28 | 'ms ' + 29 | ctx.method + 30 | ' ' + 31 | ctx.url; 32 | 33 | return ret; 34 | } 35 | } 36 | 37 | export function customLogger(logger: IMidwayLogger, app: Application): void { 38 | // 格式化日志时间戳 39 | logger.updateTransformableInfo(updateTransformableInfo); 40 | // 上下文日志打印请求id 41 | app.setContextLoggerClass(CustomContextLogger); 42 | } 43 | 44 | export function updateTransformableInfo( 45 | info: MidwayTransformableInfo 46 | ): MidwayTransformableInfo { 47 | const ret = { 48 | ...info, 49 | timestamp: genISO8601String(), 50 | }; 51 | return ret; 52 | } 53 | 54 | declare module 'egg' { 55 | interface Application { 56 | // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error 57 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 58 | // @ts-ignore 59 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 60 | setContextLoggerClass: (BaseContextLoggerClass: any) => void; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/util/my-error.ts: -------------------------------------------------------------------------------- 1 | export default class MyError extends Error { 2 | status: number; 3 | errors: any[] | undefined; 4 | details?: unknown; 5 | 6 | /** 7 | * 全局自定义错误结构 8 | * 可以接受多条错误信息,用于业务抛错 9 | * @param message 错误信息 10 | * @param status 状态码。不填写默认500,错误一般为 4xx, 5xx 11 | * @param errors 错误数组(jio 表单错误多条) 12 | */ 13 | constructor(message: string, status?: number, errors?: any[]) { 14 | super(message + ` &>${status || ''}`); // 兼容ci测试时,assert无法自定义增加status 15 | this.status = typeof status === 'number' ? status : 0; 16 | this.errors = errors; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { EggAppInfo } from 'egg'; 2 | import { ConnectionOptions } from 'typeorm'; 3 | 4 | import { DefaultConfig } from './config.types'; 5 | 6 | /** 7 | * 关于环境变量的配置,请查阅文档:https://www.yuque.com/midwayjs/midway_v2/eggjs#0JHun 8 | */ 9 | export default (appInfo: EggAppInfo): DefaultConfig => { 10 | const config = {} as DefaultConfig; 11 | 12 | // use for cookie sign key, should change to your own and keep security 13 | config.keys = appInfo.name + '_1602294995416_4568'; 14 | 15 | // add your config here 16 | config.middleware = ['jwtAuth']; 17 | 18 | config.midwayFeature = { 19 | // true 使用 midway-logger 20 | // false 或空代表使用 egg-logger 21 | replaceEggLogger: true, 22 | }; 23 | 24 | // 默认管理员 25 | config.admin = { 26 | username: 'admin', 27 | password: 'admin', 28 | }; 29 | 30 | // 数据库配置 31 | config.orm = { 32 | type: 'mysql', 33 | host: process.env.MYSQL_HOST || '127.0.0.1', 34 | port: process.env.MYSQL_PORT || 3306, 35 | username: process.env.MYSQL_USER || '', 36 | password: process.env.MYSQL_PASSWORD || '', 37 | database: process.env.MYSQL_DATABASE || undefined, 38 | synchronize: false, 39 | logging: true, 40 | timezone: '+08:00', 41 | } as ConnectionOptions; 42 | 43 | // redis配置 44 | config.redis = { 45 | client: { 46 | port: +process.env.REDIS_PORT || 6379, // Redis port 47 | host: process.env.REDIS_HOST || '127.0.0.1', // Redis host 48 | password: process.env.REDIS_PASSWORD || '', 49 | db: +process.env.REDIS_DB || 0, 50 | }, 51 | }; 52 | 53 | // swagger文档配置,默认地址 http://127.0.0.1:7001/swagger-ui/index.html 54 | config.swagger = { 55 | title: 'service-mw2', 56 | description: 'service-mw2 模版工程的接口定义', 57 | version: '1.1.0', 58 | termsOfService: 'https://github.com/fsd-nodejs/service-mw2', 59 | contact: { 60 | name: 'tkvern', 61 | url: 'https://github.com/tkvern', 62 | email: 'verncake@gmail.com', 63 | }, 64 | license: { 65 | name: 'MIT', 66 | url: 'https://github.com/midwayjs/midway/blob/serverless/LICENSE', 67 | }, 68 | }; 69 | 70 | // snowflake id generator config 71 | // '2020-01-01T00:00:00Z' 72 | const epoch = 1577836800000; 73 | config.koid = { 74 | dataCenter: 0, 75 | worker: 0, 76 | epoch, 77 | }; 78 | 79 | // rabbitmq 基本配置 默认管理界面 http://127.0.0.1:15672/ (这个项目只包含生产者的代码) 80 | config.rabbitmq = { 81 | url: process.env.RABBITMQ_URL || 'amqp://localhost', 82 | }; 83 | 84 | return config; 85 | }; 86 | -------------------------------------------------------------------------------- /src/config/config.local.ts: -------------------------------------------------------------------------------- 1 | import { EggRedisOptions } from 'egg-redis'; 2 | import { TracerConfig, defaultTracerConfig } from '@mw-components/jaeger'; 3 | import { ConnectionOptions } from 'typeorm'; 4 | import { 5 | JwtConfig, 6 | JwtMiddlewareConfig, 7 | initialJwtMiddlewareConfig, 8 | } from '@mw-components/jwt'; 9 | 10 | import { JwtAuthMiddlewareConfig } from './config.types'; 11 | 12 | // jwt配置 13 | export const jwtConfig: JwtConfig = { 14 | secret: '123456', // 默认密钥,生产环境一定要更改! 15 | }; 16 | export const jwtMiddlewareConfig: JwtMiddlewareConfig = { 17 | ...initialJwtMiddlewareConfig, 18 | enableMiddleware: true, 19 | }; 20 | jwtMiddlewareConfig.ignore = jwtMiddlewareConfig.ignore?.concat([ 21 | '/auth/login', 22 | '/ping', 23 | '/genid', 24 | '/genidHex', 25 | /\/swagger-u.*/u, 26 | ]); 27 | // jwt token 校验中间件(需配合jwt使用, ignore的配置与jwt一致) 28 | export const jwtAuth: JwtAuthMiddlewareConfig = { 29 | ignore: jwtMiddlewareConfig.ignore, 30 | redisScope: 'admin', // redis的作用域前缀 31 | accessTokenExpiresIn: 60 * 60 * 24 * 3, // 签名过期时间也可写 32 | }; 33 | 34 | // 数据库配置 35 | export const orm: ConnectionOptions = { 36 | type: 'mysql', 37 | host: '127.0.0.1', 38 | port: 3306, 39 | username: 'root', 40 | password: 'password', 41 | database: 'shop_development', 42 | synchronize: false, 43 | logging: true, 44 | }; 45 | 46 | // redis 配置 47 | export const redis: EggRedisOptions = { 48 | client: { 49 | port: 6379, // Redis port 50 | host: '127.0.0.1', // Redis host 51 | password: '', 52 | db: 0, 53 | }, 54 | }; 55 | 56 | // jaeger 配置 默认访问地址http://localhost:16686/ 57 | export const tracer: TracerConfig = { 58 | ...defaultTracerConfig, 59 | whiteList: ['/favicon.ico', '/favicon.png', '/ping', '/metrics'], 60 | tracingConfig: { 61 | sampler: { 62 | type: 'probabilistic', 63 | param: 1, 64 | }, 65 | reporter: { 66 | agentHost: '127.0.0.1', 67 | }, 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /src/config/config.prod.ts: -------------------------------------------------------------------------------- 1 | import { TracerConfig, defaultTracerConfig } from '@mw-components/jaeger'; 2 | import { 3 | JwtConfig, 4 | JwtMiddlewareConfig, 5 | initialJwtMiddlewareConfig, 6 | } from '@mw-components/jwt'; 7 | 8 | import { JwtAuthMiddlewareConfig } from './config.types'; 9 | 10 | // jwt配置 11 | export const jwtConfig: JwtConfig = { 12 | secret: '', // 默认密钥,生产环境一定要更改! 13 | }; 14 | export const jwtMiddlewareConfig: JwtMiddlewareConfig = { 15 | ...initialJwtMiddlewareConfig, 16 | ignore: ['/auth/login', '/ping', '/genid', '/genidHex', /\/swagger-u.*/u], 17 | }; 18 | // jwt token 校验中间件(需配合jwt使用, ignore的配置与jwt一致) 19 | export const jwtAuth: JwtAuthMiddlewareConfig = { 20 | ignore: jwtMiddlewareConfig.ignore, 21 | redisScope: 'admin', // redis的作用域前缀 22 | accessTokenExpiresIn: 60 * 60 * 24 * 3, // 签名过期时间也可写 23 | }; 24 | 25 | export const tracer: TracerConfig = { 26 | ...defaultTracerConfig, 27 | reqThrottleMsForPriority: 1000, 28 | whiteList: ['/favicon.ico', '/favicon.png', '/ping', '/metrics'], 29 | tracingConfig: { 30 | sampler: { 31 | type: 'probabilistic', 32 | param: 0.0001, 33 | }, 34 | reporter: { 35 | agentHost: '127.0.0.1', 36 | }, 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/config/config.types.ts: -------------------------------------------------------------------------------- 1 | import { JwtMiddlewareConfig } from '@mw-components/jwt'; 2 | import { NpmPkg } from '@waiting/shared-types'; 3 | import { EggAppConfig, PowerPartial } from 'egg'; 4 | 5 | export type DefaultConfig = PowerPartial; 6 | 7 | /** JwtAuthMiddleware */ 8 | export interface JwtAuthMiddlewareConfig { 9 | /** 签名过期时间也可写 */ 10 | accessTokenExpiresIn: number; 11 | ignore: JwtMiddlewareConfig['ignore']; 12 | /** redis的作用域前缀 */ 13 | redisScope: string; 14 | } 15 | 16 | export interface RabbitmqConfig { 17 | /** mq 地址 */ 18 | url: string; 19 | } 20 | 21 | declare module 'egg' { 22 | /** 23 | * config 配置文件的 TS 声明 24 | */ 25 | interface EggAppConfig { 26 | admin: Record; 27 | coreMiddleware: string[]; 28 | jwtAuth: JwtAuthMiddlewareConfig; 29 | pkgJson: NpmPkg; 30 | rabbitmq: RabbitmqConfig; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/config/config.unittest.ts: -------------------------------------------------------------------------------- 1 | import { EggRedisOptions } from 'egg-redis'; 2 | import { TracerConfig, defaultTracerConfig } from '@mw-components/jaeger'; 3 | import { Config as KoidConfig } from '@mw-components/koid'; 4 | import { ConnectionOptions } from 'typeorm'; 5 | 6 | import { RabbitmqConfig } from './config.types'; 7 | 8 | export { jwtConfig, jwtMiddlewareConfig, jwtAuth } from './config.local'; 9 | 10 | export const security = { 11 | csrf: false, 12 | }; 13 | 14 | // 数据库配置 15 | export const orm: ConnectionOptions = { 16 | type: 'mysql', 17 | host: process.env.MYSQL_HOST || '127.0.0.1', 18 | port: +process.env.MYSQL_PORT || 3306, 19 | username: process.env.MYSQL_USER || 'root', 20 | password: process.env.MYSQL_PASSWORD || 'password', 21 | database: process.env.MYSQL_DATABASE || 'shop_development', 22 | synchronize: false, 23 | logging: false, 24 | }; 25 | 26 | // redis配置 27 | export const redis: EggRedisOptions = { 28 | client: { 29 | port: +process.env.REDIS_PORT || 6379, // Redis port 30 | host: process.env.REDIS_HOST || '127.0.0.1', // Redis host 31 | password: process.env.REDIS_PASSWORD || '', 32 | db: +process.env.REDIS_DB || 0, 33 | }, 34 | }; 35 | 36 | // 建议跑测试的时候设置 true 屏蔽日志(true),这样手动故意触发的错误,都不会显示处理。设置 false 则输出日志 37 | export const logger = { 38 | disableConsoleAfterReady: false, 39 | }; 40 | 41 | export const tracer: TracerConfig = { 42 | ...defaultTracerConfig, 43 | whiteList: [ 44 | '/favicon.ico', 45 | '/favicon.png', 46 | '/ping', 47 | '/metrics', 48 | '/untracedPath', 49 | '', 50 | /\/unitTest[\d.]+/u, 51 | ], 52 | tracingConfig: { 53 | sampler: { 54 | type: 'const', 55 | param: 1, 56 | }, 57 | reporter: { 58 | agentHost: '127.0.0.1', 59 | }, 60 | }, 61 | }; 62 | 63 | const epoch = 1577836800000; 64 | export const koid: KoidConfig = { 65 | dataCenter: 12, 66 | worker: 23, 67 | epoch, 68 | }; 69 | 70 | // rabbitmq 基本配置 71 | export const rabbitmq: RabbitmqConfig = { 72 | url: 'amqp://rabbitmq', 73 | }; 74 | -------------------------------------------------------------------------------- /src/config/plugin.ts: -------------------------------------------------------------------------------- 1 | import { EggPlugin } from 'egg'; 2 | 3 | // 启用redis 4 | export const redis = { 5 | enable: true, 6 | package: 'egg-redis', 7 | }; 8 | 9 | export default { 10 | static: true, // default is true 11 | redis, 12 | } as EggPlugin; 13 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-extraneous-import */ 2 | // eslint-disable-next-line node/no-unpublished-import 3 | import 'tsconfig-paths/register'; 4 | 5 | import { App, Configuration, Logger } from '@midwayjs/decorator'; 6 | import * as swagger from '@midwayjs/swagger'; 7 | import { ILifeCycle } from '@midwayjs/core'; 8 | import { IMidwayLogger } from '@midwayjs/logger'; 9 | import * as jaeger from '@mw-components/jaeger'; 10 | import * as jwt from '@mw-components/jwt'; 11 | import * as koid from '@mw-components/koid'; 12 | 13 | import { Application, NpmPkg } from '@/interface'; 14 | 15 | import { customLogger } from './app/util/custom-logger'; 16 | 17 | @Configuration({ 18 | imports: [ 19 | jaeger, 20 | koid, 21 | jwt, 22 | '@midwayjs/orm', // 加载 orm 组件 23 | // 加载swagger组件 24 | { 25 | component: swagger, 26 | enabledEnvironment: ['local'], 27 | }, 28 | ], 29 | }) 30 | export class ContainerConfiguration implements ILifeCycle { 31 | @App() 32 | app: Application; 33 | 34 | @Logger() 35 | readonly logger: IMidwayLogger; 36 | 37 | // 启动前处理 38 | async onReady(): Promise { 39 | this.app.config.pkgJson = this.app.config.pkg as NpmPkg; 40 | 41 | // 定制化日志 42 | customLogger(this.logger, this.app); 43 | 44 | // const coreMiddlewareArr = this.app.getConfig('coreMiddleware') as string[] 45 | const coreMiddlewareArr = this.app.config.coreMiddleware as string[]; 46 | 47 | // 增加全局错误处理中间件(确保在最前) 48 | coreMiddlewareArr.splice(0, 0, 'errorHandlerMiddleware'); 49 | 50 | // 增加全局x-request-id处理中间件 51 | coreMiddlewareArr.splice(1, 0, 'requestIdMiddleware'); 52 | 53 | // 需要显式在 app 启动时用 getAsync() 的方式进行触发,否则该类只有在首次被业务逻辑调用的时候才能初始化 54 | // await this.app.getApplicationContext().getAsync('rabbitmqService'); 55 | 56 | const { pkgJson } = this.app.config; 57 | const info = { 58 | pkgName: pkgJson.name, 59 | pkgVersion: pkgJson.version, 60 | }; 61 | // eslint-disable-next-line no-console 62 | console.log('✅ Your APP launched', info); 63 | } 64 | 65 | // 可以在这里做些停止后处理 66 | // async onStop(): Promise { 67 | // } 68 | } 69 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { 3 | IMidwayWebApplication as Application, 4 | IMidwayWebContext as Context, 5 | IMidwayWebNext, 6 | } from '@midwayjs/web'; 7 | import { JwtState } from '@mw-components/jwt'; 8 | 9 | export { Application, Context, IMidwayWebNext }; 10 | 11 | export { IMidwayContainer } from '@midwayjs/core'; 12 | 13 | /** 14 | * @description User-Service parameters 15 | */ 16 | export interface IUserOptions { 17 | uid: number; 18 | } 19 | 20 | export { TracerLog } from '@mw-components/jaeger'; 21 | export { NpmPkg } from '@waiting/shared-types'; 22 | 23 | declare module '@midwayjs/core' { 24 | interface Context { 25 | reqId: string; 26 | _internalError?: Error; 27 | jwtState: JwtState; 28 | } 29 | } 30 | 31 | export interface JwtUser { 32 | id: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/typings/meeko.ts: -------------------------------------------------------------------------------- 1 | declare module 'meeko' { 2 | interface ArrayFunctions { 3 | /** 4 | * 取得交集 5 | * @param {(string | number)[]} arg0 数组0 6 | * @param {(string | number)[]} arg1 数组1 7 | * @returns {(string | number)[]} 交集 8 | */ 9 | intersect: ( 10 | arg0: (string | number)[], 11 | arg1: (string | number)[] 12 | ) => (string | number)[]; 13 | 14 | /** 15 | * 取得差集 16 | * @param {(string | number)[]} arg0 数组0 17 | * @param {(string | number)[]} arg1 数组1 18 | * @returns {(string | number)[]} 差集 19 | */ 20 | except: ( 21 | arg0: (string | number)[], 22 | arg1: (string | number)[] 23 | ) => (string | number)[]; 24 | } 25 | // 在此只是简单地进行类型描述 26 | export const array: ArrayFunctions; 27 | } 28 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | ROSTemplateFormatVersion: '2015-09-01' 2 | Transform: 'Aliyun::Serverless-2018-04-03' 3 | Resources: 4 | service-mw2: 5 | Type: 'Aliyun::Serverless::Service' 6 | Properties: 7 | Description: 测试serverless的服务 8 | Role: 'acs:ram::1591526048690788:role/aliyunfcdefaultrole' 9 | LogConfig: 10 | Project: service-mw2 11 | Logstore: service-mw2 12 | EnableRequestMetrics: true 13 | VpcConfig: 14 | VpcId: vpc-wz9je35dho09vagpuawl9 15 | VSwitchIds: 16 | - vsw-wz9w8z1jkwnsr3eedgsnv 17 | SecurityGroupId: sg-wz9egg8kg0g6dkf89sp9 18 | TracingConfig: Enable 19 | InternetAccess: true 20 | app_index: 21 | Type: 'Aliyun::Serverless::Function' 22 | Properties: 23 | Initializer: index.initializer 24 | InitializationTimeout: 30 25 | Handler: index.handler 26 | Runtime: custom-container 27 | Timeout: 30 28 | MemorySize: 256 29 | EnvironmentVariables: 30 | LD_LIBRARY_PATH: >- 31 | /code/.fun/root/usr/local/lib:/code/.fun/root/usr/lib:/code/.fun/root/usr/lib/x86_64-linux-gnu:/code/.fun/root/usr/lib64:/code/.fun/root/lib:/code/.fun/root/lib/x86_64-linux-gnu:/code/.fun/root/python/lib/python2.7/site-packages:/code/.fun/root/python/lib/python3.6/site-packages:/code:/code/lib:/usr/local/lib 32 | MYSQL_DATABASE: service-mw2 33 | MYSQL_HOST: 10.0.0.228 34 | MYSQL_PASSWORD: XPPYP6NGHXmyTBM6 35 | MYSQL_PORT: '3306' 36 | MYSQL_USER: service-mw2 37 | NODE_PATH: '/code/node_modules:/usr/local/lib/node_modules' 38 | PATH: >- 39 | /code/.fun/root/usr/local/bin:/code/.fun/root/usr/local/sbin:/code/.fun/root/usr/bin:/code/.fun/root/usr/sbin:/code/.fun/root/sbin:/code/.fun/root/bin:/code:/code/node_modules/.bin:/code/.fun/python/bin:/code/.fun/node_modules/.bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/bin 40 | PYTHONUSERBASE: /code/.fun/python 41 | REDIS_DB: '1' 42 | REDIS_HOST: 10.0.0.228 43 | REDIS_PORT: '6379' 44 | NODE_ENV: 'production' 45 | InstanceType: e1 46 | InstanceLifecycleConfig: 47 | PreFreeze: 48 | Handler: '' 49 | Timeout: 3 50 | PreStop: 51 | Handler: '' 52 | Timeout: 3 53 | CAPort: 9000 54 | CustomContainerConfig: 55 | Args: '' 56 | Command: '' 57 | Image: 'registry.cn-shenzhen.aliyuncs.com/service-mw2/midway:1.0.14' 58 | Events: 59 | http-app_index: 60 | Type: HTTP 61 | Properties: 62 | AuthType: anonymous 63 | Methods: 64 | - GET 65 | - PUT 66 | - POST 67 | - DELETE 68 | - HEAD 69 | - PATCH 70 | -------------------------------------------------------------------------------- /test/controller/admin/menu.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../../root.config'; 5 | 6 | 7 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 8 | 9 | describe(filename, () => { 10 | let currentMenu: any; 11 | 12 | it('should get /admin/menu/query ', async () => { 13 | const { httpRequest, currentUser } = testConfig 14 | 15 | assert(currentUser.token) 16 | const response = await httpRequest 17 | .get('/admin/menu/query') 18 | .set('Authorization', `Bearer ${currentUser.token}`) 19 | .expect(200); 20 | assert.ok(response.body.data.total); 21 | }); 22 | 23 | it('should get /admin/menu/show ', async () => { 24 | const { httpRequest, currentUser } = testConfig 25 | 26 | assert(currentUser.token) 27 | const response = await httpRequest 28 | .get('/admin/menu/query') 29 | .set('Authorization', `Bearer ${currentUser.token}`) 30 | .expect(200); 31 | assert.ok(response.body.data.total); 32 | 33 | const { list } = response.body.data; 34 | const response2 = await httpRequest 35 | .get('/admin/menu/show') 36 | .query({ 37 | id: list[0].id, 38 | }) 39 | .set('Authorization', `Bearer ${currentUser.token}`); 40 | assert.deepEqual(response2.body.data.id, list[0].id); 41 | }); 42 | 43 | it('should post /admin/menu/create ', async () => { 44 | const { httpRequest, currentUser } = testConfig 45 | 46 | assert(currentUser.token) 47 | const params = { 48 | title: 'fakeTitle', 49 | uri: 'fakeUri', 50 | roles: ['1'], 51 | permissionId: '1', 52 | }; 53 | const response = await httpRequest 54 | .post('/admin/menu/create') 55 | .set('Authorization', `Bearer ${currentUser.token}`) 56 | .type('form') 57 | .send(params) 58 | .expect(201); 59 | assert.ok(response.body.data); 60 | currentMenu = response.body.data; 61 | }); 62 | 63 | it('should patch /admin/menu/update ', async () => { 64 | const { httpRequest, currentUser } = testConfig 65 | 66 | assert(currentUser.token) 67 | const params = { 68 | id: currentMenu.id, 69 | title: 'fakeTitle2', 70 | uri: 'fakeUri2', 71 | roles: ['1'], 72 | permissionId: '2', 73 | }; 74 | const response = await httpRequest 75 | .patch('/admin/menu/update') 76 | .set('Authorization', `Bearer ${currentUser.token}`) 77 | .type('form') 78 | .send(params) 79 | .expect(204); 80 | assert.deepEqual(response.status, 204); 81 | }); 82 | 83 | it('should delete /admin/menu/remove ', async () => { 84 | const { httpRequest, currentUser } = testConfig 85 | 86 | assert(currentUser.token) 87 | const params = { 88 | ids: [currentMenu.id], 89 | }; 90 | const response = await httpRequest 91 | .del('/admin/menu/remove') 92 | .set('Authorization', `Bearer ${currentUser.token}`) 93 | .type('form') 94 | .send(params) 95 | .expect(204); 96 | assert.deepEqual(response.status, 204); 97 | }); 98 | 99 | it('should order /admin/menu/order ', async () => { 100 | const { httpRequest, currentUser } = testConfig 101 | 102 | assert(currentUser.token) 103 | const response1 = await httpRequest 104 | .get('/admin/menu/query') 105 | .set('Authorization', `Bearer ${currentUser.token}`) 106 | .expect(200); 107 | assert.deepEqual(response1.status, 200); 108 | 109 | const newList = response1.body.data.list.map((item: any) => { 110 | return { 111 | id: item.id, 112 | parentId: item.parentId, 113 | }; 114 | }); 115 | 116 | const response2 = await httpRequest 117 | .post('/admin/menu/order') 118 | .set('Authorization', `Bearer ${currentUser.token}`) 119 | .type('form') 120 | .send({ 121 | orders: newList, 122 | }) 123 | .expect(200); 124 | assert.deepEqual(response2.status, 200); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/controller/admin/permission.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../../root.config'; 5 | 6 | 7 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 8 | 9 | describe(filename, () => { 10 | let currentPermission: any; 11 | 12 | it('should get /admin/permission/query ', async () => { 13 | const { httpRequest, currentUser } = testConfig 14 | 15 | assert(currentUser.token) 16 | const response = await httpRequest 17 | .get('/admin/permission/query') 18 | .query({ 19 | sorter: 'id_descend', 20 | id: '2', 21 | name: 'Dash', 22 | slug: 'dash', 23 | httpPath: '/', 24 | httpMethod: 'GET' 25 | }) 26 | .set('Authorization', `Bearer ${currentUser.token}`) 27 | .expect(200); 28 | assert.ok(response.body.data.total); 29 | }); 30 | 31 | it('should get /admin/permission/show ', async () => { 32 | const { httpRequest, currentUser } = testConfig 33 | 34 | assert(currentUser.token) 35 | const response = await httpRequest 36 | .get('/admin/permission/query') 37 | .set('Authorization', `Bearer ${currentUser.token}`) 38 | .expect(200); 39 | assert.ok(response.body.data.total); 40 | const { list } = response.body.data; 41 | const response2 = await httpRequest 42 | .get('/admin/permission/show') 43 | .query({ 44 | id: list[0].id, 45 | }) 46 | .set('Authorization', `Bearer ${currentUser.token}`); 47 | assert.deepEqual(response2.body.data.id, list[0].id); 48 | }); 49 | 50 | it('should post /admin/permission/create ', async () => { 51 | const { httpRequest, currentUser } = testConfig 52 | 53 | assert(currentUser.token) 54 | const params = { 55 | name: 'fakeName', 56 | slug: 'fakeSlug', 57 | httpMethod: ['GET', 'POST'], 58 | httpPath: '/fake/path', 59 | }; 60 | const response = await httpRequest 61 | .post('/admin/permission/create') 62 | .set('Authorization', `Bearer ${currentUser.token}`) 63 | .type('form') 64 | .send(params) 65 | .expect(201); 66 | assert.ok(response.body.data); 67 | currentPermission = response.body.data; 68 | }); 69 | 70 | it('should patch /admin/permission/update ', async () => { 71 | const { httpRequest, currentUser } = testConfig 72 | 73 | assert(currentUser.token) 74 | const params = { 75 | id: currentPermission.id, 76 | httpPath: '/fake/path2', 77 | }; 78 | const response = await httpRequest 79 | .patch('/admin/permission/update') 80 | .set('Authorization', `Bearer ${currentUser.token}`) 81 | .type('form') 82 | .send(params) 83 | .expect(204); 84 | assert.deepEqual(response.status, 204); 85 | }); 86 | 87 | it('should delete /admin/permission/remove ', async () => { 88 | const { httpRequest, currentUser } = testConfig 89 | 90 | assert(currentUser.token) 91 | const params = { 92 | ids: [currentPermission.id], 93 | }; 94 | const response = await httpRequest 95 | .del('/admin/permission/remove') 96 | .set('Authorization', `Bearer ${currentUser.token}`) 97 | .type('form') 98 | .send(params) 99 | .expect(204); 100 | assert.deepEqual(response.status, 204); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/controller/admin/role.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../../root.config'; 5 | 6 | 7 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 8 | 9 | describe(filename, () => { 10 | let currentRole: any; 11 | 12 | it('should get /admin/role/query ', async () => { 13 | const { httpRequest, currentUser } = testConfig 14 | 15 | assert(currentUser.token) 16 | const response = await httpRequest 17 | .get('/admin/role/query') 18 | .query({ 19 | sorter: 'id_descend', 20 | id: '1', 21 | name: 'Admin', 22 | slug: 'admin' 23 | }) 24 | .set('Authorization', `Bearer ${currentUser.token}`) 25 | .expect(200); 26 | assert.ok(response.body.data.total); 27 | }); 28 | 29 | it('should get /admin/role/show ', async () => { 30 | const { httpRequest, currentUser } = testConfig 31 | 32 | assert(currentUser.token) 33 | const response = await httpRequest 34 | .get('/admin/role/query') 35 | .set('Authorization', `Bearer ${currentUser.token}`) 36 | .expect(200); 37 | assert.ok(response.body.data.total); 38 | const { list } = response.body.data; 39 | 40 | const response2 = await httpRequest 41 | .get('/admin/role/show') 42 | .query({ 43 | id: list[0].id, 44 | }) 45 | .set('Authorization', `Bearer ${currentUser.token}`); 46 | assert.deepEqual(response2.body.data.id, list[0].id); 47 | }); 48 | 49 | it('should post /admin/role/create ', async () => { 50 | const { httpRequest, currentUser } = testConfig 51 | 52 | assert(currentUser.token) 53 | const params = { 54 | name: 'fakeName', 55 | slug: 'fakeSlug', 56 | permissions: ['2', '3'] 57 | }; 58 | const response = await httpRequest 59 | .post('/admin/role/create') 60 | .set('Authorization', `Bearer ${currentUser.token}`) 61 | .type('form') 62 | .send(params) 63 | .expect(201); 64 | assert.ok(response.body.data); 65 | currentRole = response.body.data; 66 | }); 67 | 68 | it('should patch /admin/role/update ', async () => { 69 | const { httpRequest, currentUser } = testConfig 70 | 71 | assert(currentUser.token) 72 | const params = { 73 | id: currentRole.id, 74 | slug: 'fakeSlug2', 75 | permissions: ['2'] 76 | }; 77 | const response = await httpRequest 78 | .patch('/admin/role/update') 79 | .set('Authorization', `Bearer ${currentUser.token}`) 80 | .type('form') 81 | .send(params) 82 | .expect(204); 83 | assert.deepEqual(response.status, 204); 84 | }); 85 | 86 | it('should delete /admin/role/remove ', async () => { 87 | const { httpRequest, currentUser } = testConfig 88 | 89 | assert(currentUser.token) 90 | const params = { 91 | ids: [currentRole.id], 92 | }; 93 | const response = await httpRequest 94 | .del('/admin/role/remove') 95 | .set('Authorization', `Bearer ${currentUser.token}`) 96 | .type('form') 97 | .send(params) 98 | .expect(204); 99 | assert.deepEqual(response.status, 204); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/controller/admin/user.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../../root.config'; 5 | 6 | 7 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 8 | 9 | describe(filename, () => { 10 | let currentAdminUser: any; 11 | 12 | it('should get /admin/user/query ', async () => { 13 | const { httpRequest, currentUser } = testConfig 14 | 15 | assert(currentUser.token) 16 | const response = await httpRequest 17 | .get('/admin/user/query') 18 | .query({ 19 | sorter: 'id_descend', 20 | id: '1', 21 | name: 'Admin', 22 | username: 'admin', 23 | }) 24 | .set('Authorization', `Bearer ${currentUser.token}`) 25 | .expect(200); 26 | assert.ok(response.body.data.total); 27 | }); 28 | 29 | it('should get /admin/user/show ', async () => { 30 | const { httpRequest, currentUser } = testConfig 31 | 32 | assert(currentUser.token) 33 | const response = await httpRequest 34 | .get('/admin/user/query') 35 | .set('Authorization', `Bearer ${currentUser.token}`) 36 | .expect(200); 37 | assert.ok(response.body.data.total); 38 | const { list } = response.body.data; 39 | 40 | const response2 = await httpRequest 41 | .get('/admin/user/show') 42 | .query({ 43 | id: list[0].id, 44 | }) 45 | .set('Authorization', `Bearer ${currentUser.token}`); 46 | assert.deepEqual(response2.body.data.id, list[0].id); 47 | }); 48 | 49 | it('should post /admin/user/create ', async () => { 50 | const { httpRequest, currentUser } = testConfig 51 | 52 | assert(currentUser.token) 53 | const params = { 54 | name: 'fakeName', 55 | username: 'fakeUserName', 56 | password: '123456', 57 | roles: ['1'], 58 | permissions: ['1'], 59 | }; 60 | const response = await httpRequest 61 | .post('/admin/user/create') 62 | .set('Authorization', `Bearer ${currentUser.token}`) 63 | .type('form') 64 | .send(params) 65 | .expect(201); 66 | assert.ok(response.body.data); 67 | currentAdminUser = response.body.data; 68 | }); 69 | 70 | it('should patch /admin/user/update ', async () => { 71 | const { httpRequest, currentUser } = testConfig 72 | 73 | assert(currentUser.token) 74 | const params = { 75 | id: currentAdminUser.id, 76 | name: 'fakeName2', 77 | username: 'fakeUserName2', 78 | password: '1234567', 79 | roles: ['1'], 80 | permissions: ['1'], 81 | }; 82 | const response = await httpRequest 83 | .patch('/admin/user/update') 84 | .set('Authorization', `Bearer ${currentUser.token}`) 85 | .type('form') 86 | .send(params) 87 | .expect(204); 88 | assert.deepEqual(response.status, 204); 89 | }); 90 | 91 | it('should delete /admin/user/remove ', async () => { 92 | const { httpRequest, currentUser } = testConfig 93 | 94 | assert(currentUser.token) 95 | const params = { 96 | ids: [currentAdminUser.id], 97 | }; 98 | const response = await httpRequest 99 | .del('/admin/user/remove') 100 | .set('Authorization', `Bearer ${currentUser.token}`) 101 | .type('form') 102 | .send(params) 103 | .expect(204); 104 | assert.deepEqual(response.status, 204); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/controller/auth-invalid.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../root.config'; 5 | 6 | 7 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 8 | 9 | describe(filename, () => { 10 | 11 | it('should POST /auth/login by wrong username and password', async () => { 12 | const { app, httpRequest } = testConfig 13 | 14 | const response = await httpRequest 15 | .post('/auth/login') 16 | .type('form') 17 | .send({ 18 | username: app.config.admin.username, 19 | password: '123456', 20 | }) 21 | .expect(400); 22 | 23 | assert(response.body.code === 400); 24 | }); 25 | 26 | it('should POST /auth/login by wrong username', async () => { 27 | const { httpRequest } = testConfig 28 | 29 | const response = await httpRequest 30 | .post('/auth/login') 31 | .type('form') 32 | .send({ 33 | username: 'fakename', 34 | password: '123456', 35 | }) 36 | .expect(400); 37 | 38 | assert(response.body.code === 400); 39 | }); 40 | 41 | it('should POST /auth/login by wrong input', async () => { 42 | const { httpRequest } = testConfig 43 | 44 | const response = await httpRequest 45 | .post('/auth/login') 46 | .type('form') 47 | .expect(422); 48 | 49 | assert(response.body.code === 422); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /test/controller/auth-valid.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../root.config'; 5 | 6 | 7 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 8 | 9 | describe(filename, () => { 10 | 11 | it('should POST /auth/login by correct username and password', async () => { 12 | const { app, httpRequest } = testConfig 13 | 14 | const response = await httpRequest 15 | .post('/auth/login') 16 | .type('form') 17 | .send(app.config.admin) 18 | .expect(200); 19 | 20 | assert(response.body.data.token); 21 | }); 22 | 23 | it('should GET /auth/currentUser', async () => { 24 | const { httpRequest, currentUser } = testConfig 25 | 26 | assert(currentUser.token) 27 | const response = await httpRequest 28 | .get('/auth/currentUser') 29 | .set('Authorization', `Bearer ${currentUser.token}`) 30 | .expect(200); 31 | 32 | assert(response.body.code === 200); 33 | }); 34 | 35 | it('should GET 404 with valid token', async () => { 36 | const { httpRequest, currentUser } = testConfig 37 | 38 | assert(currentUser.token) 39 | const response = await httpRequest 40 | .get('/auth/currentUserNotFound') 41 | .set('Authorization', `Bearer ${currentUser.token}`) 42 | .expect(404); 43 | 44 | assert(response.body.code === 404); 45 | }); 46 | 47 | it('should GET /auth/logout', async () => { 48 | const { httpRequest, currentUser } = testConfig 49 | 50 | assert(currentUser.token) 51 | const response = await httpRequest 52 | .get('/auth/logout') 53 | .set('Authorization', `Bearer ${currentUser.token}`) 54 | .expect(200); 55 | 56 | assert(response.body.code === 200); 57 | }); 58 | 59 | it('should GET /auth/currentUser was logouted', async () => { 60 | const { httpRequest, currentUser } = testConfig 61 | 62 | assert(currentUser.token) 63 | 64 | await httpRequest 65 | .get('/auth/logout') 66 | .set('Authorization', `Bearer ${currentUser.token}`) 67 | .expect(200); 68 | 69 | const response = await httpRequest 70 | .get('/auth/currentUser') 71 | .set('Authorization', `Bearer ${currentUser.token}`) 72 | .expect(401); 73 | 74 | assert(response.body.code === 401); 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /test/controller/home.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | import { KoidComponent } from '@mw-components/koid'; 4 | 5 | import { testConfig } from '../root.config' 6 | 7 | 8 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 9 | 10 | describe(filename, () => { 11 | 12 | it('should GET /', async () => { 13 | const { httpRequest, currentUser } = testConfig 14 | 15 | assert(currentUser.token) 16 | const response = await httpRequest 17 | .get('/') 18 | .set('Authorization', `Bearer ${currentUser.token}`) 19 | .expect(200); 20 | 21 | const msg: string = response.text; 22 | assert.ok(msg && msg.includes('Hello Midwayjs!')); 23 | assert.ok(/reqId: "[1-9]\d{9,18}"/u.test(msg), msg); // 6755455236955799552 24 | }); 25 | 26 | it('should GET /ping', async () => { 27 | const { httpRequest, currentUser } = testConfig 28 | 29 | assert(currentUser.token) 30 | const ret = await httpRequest.get('/ping').expect(200); 31 | 32 | const msg: string = ret.text; 33 | assert.ok(msg && msg.includes('OK')); 34 | }); 35 | 36 | it('should GET /genid', async () => { 37 | const { app, httpRequest, currentUser } = testConfig 38 | 39 | assert(currentUser.token) 40 | const response = await httpRequest.get('/genid').expect(200); 41 | 42 | const msg: string = response.text; 43 | assert.ok(/[1-9]\d{9,18}/u.test(msg)); // 6755455236955799552 44 | 45 | const ctx = app.createAnonymousContext(); 46 | const koid = await ctx.requestContext.getAsync(KoidComponent); 47 | 48 | const info = koid.retrieveFromId(msg); 49 | assert.ok(info.dataCenter === koid.config.dataCenter); 50 | assert.ok(info.worker === koid.config.worker); 51 | }); 52 | 53 | it('should GET /genidHex', async () => { 54 | const { app, httpRequest, currentUser } = testConfig 55 | 56 | assert(currentUser.token) 57 | const response = await httpRequest.get('/genidHex').expect(200); 58 | 59 | const msg: string = response.text; 60 | assert.ok(/[\dxa-f]{16}/u.test(msg), msg); // 5dc032befecd8000, 02a5f26eb5197000 61 | 62 | const ctx = app.createAnonymousContext(); 63 | const koid = await ctx.requestContext.getAsync(KoidComponent); 64 | 65 | const info = koid.retrieveFromId(msg); 66 | assert.ok(info.dataCenter === koid.config.dataCenter); 67 | assert.ok(info.worker === koid.config.worker); 68 | }); 69 | 70 | it('should GET /sendToQueue', async () => { 71 | const { httpRequest, currentUser } = testConfig 72 | 73 | assert(currentUser.token) 74 | const response = await httpRequest 75 | .get('/sendToQueue') 76 | .set('Authorization', `Bearer ${currentUser.token}`) 77 | .expect(200); 78 | assert.ok(response.body.data); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/middleware/error-handler.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../root.config'; 5 | 6 | import { ErrorHandlerMiddleware } from '../../src/app/middleware/error-handler'; 7 | import MyError from '../../src/app/util/my-error'; 8 | import { Context } from '../../src/interface' 9 | 10 | 11 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 12 | 13 | describe(filename, () => { 14 | it('should 404 works', async () => { 15 | const { app, next } = testConfig 16 | const ctx = app.createAnonymousContext() as Context 17 | 18 | ctx.status = 404 19 | const inst = await ctx.requestContext.getAsync(ErrorHandlerMiddleware) 20 | const mw = inst.resolve() 21 | // @ts-expect-error 22 | await mw(ctx, next) 23 | 24 | const { body, status } = ctx 25 | assert.ok(status === 404) 26 | assert.ok(body.message === 'Not Found') 27 | }) 28 | 29 | it('should 422 works', async () => { 30 | const { app } = testConfig 31 | const ctx = app.createAnonymousContext() as Context 32 | 33 | ctx.status = 200 34 | const inst = await ctx.requestContext.getAsync(ErrorHandlerMiddleware) 35 | const mw = inst.resolve() 36 | // @ts-expect-error 37 | await mw(ctx, nextThrowError) 38 | 39 | const { status, body } = ctx 40 | assert.ok(status === 422, status.toString()) 41 | assert.ok(body.message === 'ValidationError') 42 | }) 43 | 44 | 45 | it('should 500 works', async () => { 46 | const { app } = testConfig 47 | const ctx = app.createAnonymousContext() as Context 48 | 49 | ctx.status = 200 50 | ctx.app.config.env = 'prod' 51 | const inst = await ctx.requestContext.getAsync(ErrorHandlerMiddleware) 52 | const mw = inst.resolve() 53 | // @ts-expect-error 54 | await mw(ctx, nextThrowError500) 55 | const { body, status } = ctx 56 | assert.ok(status === 500) 57 | assert.ok(body.message === 'Internal Server Error') 58 | }) 59 | }) 60 | 61 | 62 | 63 | async function nextThrowError(): Promise { 64 | throw new MyError('ValidationError') 65 | } 66 | 67 | async function nextThrowError500(): Promise { 68 | throw new MyError('Server Error', 500) 69 | } 70 | -------------------------------------------------------------------------------- /test/middleware/request-id.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 3 | /* eslint-disable @typescript-eslint/no-var-requires */ 4 | 5 | import { relative } from 'path'; 6 | import assert from 'assert'; 7 | 8 | import { testConfig } from '../root.config'; 9 | import { KoidComponent } from '@mw-components/koid' 10 | import { HeadersKey } from '@mw-components/jaeger'; 11 | 12 | import { RequestIdMiddleware } from '../../src/app/middleware/request-id' 13 | import { Context } from '../../src/interface' 14 | 15 | 16 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 17 | 18 | describe(filename, () => { 19 | it('should works', async () => { 20 | const { app, next } = testConfig 21 | const ctx = app.createAnonymousContext() as Context 22 | 23 | const key = HeadersKey.reqId 24 | ctx.status = 200 25 | const inst = await ctx.requestContext.getAsync(RequestIdMiddleware) 26 | const mw = inst.resolve() 27 | // @ts-expect-error 28 | await mw(ctx, next) 29 | 30 | const { status, reqId } = ctx 31 | assert.ok(status === 200) 32 | assert.ok(reqId && reqId.length) 33 | 34 | const koid = await ctx.requestContext.getAsync(KoidComponent) 35 | const info = koid.retrieveFromId(reqId) 36 | assert.ok(typeof info.dataCenter === 'number') 37 | assert.ok(typeof info.worker === 'number') 38 | assert.ok(typeof info.timestamp === 'number') 39 | 40 | const xReqId = ctx.response.get(key) 41 | assert.ok(xReqId === reqId) 42 | }) 43 | 44 | it('should works with existing x-request-id header', async () => { 45 | const { app, next } = testConfig 46 | const ctx = app.createAnonymousContext() as Context 47 | 48 | const key = HeadersKey.reqId 49 | ctx.status = 200 50 | 51 | const input = Math.random().toString() 52 | assert.ok(input.length) 53 | // ctx.set(key, input) not works 54 | ctx.request.headers[key] = input 55 | 56 | const inst = await ctx.requestContext.getAsync(RequestIdMiddleware) 57 | const mw = inst.resolve() 58 | // @ts-expect-error 59 | await mw(ctx, next) 60 | 61 | const { status } = ctx 62 | assert.ok(status === 200) 63 | 64 | const xReqId = ctx.response.get(key) 65 | assert.ok(xReqId === input) 66 | }) 67 | 68 | }) 69 | 70 | -------------------------------------------------------------------------------- /test/root.config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { IMidwayKoaNext } from '@midwayjs/koa' 4 | import { JwtComponent } from '@mw-components/jwt' 5 | import supertest, { SuperTest } from 'supertest' 6 | 7 | import { 8 | Application, 9 | IMidwayContainer, 10 | } from '../src/interface' 11 | 12 | 13 | export interface TestConfig { 14 | /** host of test process */ 15 | host: string 16 | app: Application 17 | container: IMidwayContainer 18 | httpRequest: SuperTest 19 | jwt: JwtComponent 20 | next: IMidwayKoaNext 21 | logsDir: string 22 | currentUser?: { 23 | token: string 24 | } 25 | } 26 | const next: IMidwayKoaNext = async () => { return } 27 | export const testConfig = { 28 | next, 29 | logsDir: join(__dirname, 'logs'), 30 | } as TestConfig 31 | 32 | -------------------------------------------------------------------------------- /test/root.hooks.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 4 | import 'tsconfig-paths/register' 5 | 6 | import assert from 'assert'; 7 | 8 | import { createApp, close, createHttpRequest } from '@midwayjs/mock' 9 | import { Framework } from '@midwayjs/web' 10 | import { JwtComponent } from '@mw-components/jwt' 11 | 12 | import { testConfig } from './root.config' 13 | import { removeFileOrDir } from './util/util' 14 | 15 | 16 | /** 17 | * @see https://mochajs.org/#root-hook-plugins 18 | * beforeAll: 19 | * - In serial mode(Mocha’s default ), before all tests begin, once only 20 | * - In parallel mode, run before all tests begin, for each file 21 | * beforeEach: 22 | * - In both modes, run before each test 23 | */ 24 | export const mochaHooks = async () => { 25 | // avoid run multi times 26 | if (! process.env.mochaRootHookFlag) { 27 | process.env.mochaRootHookFlag = 'true' 28 | } 29 | 30 | return { 31 | beforeAll: async () => { 32 | const app = await createApp() 33 | const container = app.getApplicationContext() 34 | testConfig.app = app 35 | testConfig.container = container 36 | testConfig.httpRequest = createHttpRequest(app) 37 | testConfig.jwt = await testConfig.container.getAsync(JwtComponent) 38 | 39 | const { url } = testConfig.httpRequest.get('/') 40 | testConfig.host = url 41 | 42 | assert(! testConfig.currentUser) 43 | }, 44 | 45 | beforeEach: async () => { 46 | const { app } = testConfig 47 | 48 | const response = await testConfig.httpRequest 49 | .post('/auth/login') 50 | .type('form') 51 | .send(app.config.admin) 52 | .expect(200) 53 | testConfig.currentUser = response.body.data 54 | assert(testConfig.currentUser) 55 | assert(typeof testConfig.currentUser.token === 'string') 56 | assert(testConfig.currentUser.token.length) 57 | }, 58 | 59 | afterEach: async () => { 60 | }, 61 | 62 | afterAll: async () => { 63 | if (testConfig.app) { 64 | await close(testConfig.app) 65 | } 66 | removeFileOrDir(testConfig.logsDir).catch(() => { void 0 }) 67 | }, 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /test/service/auth.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../root.config'; 5 | 6 | import { AuthService } from '../../src/app/service/auth'; 7 | 8 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 9 | 10 | describe(filename, () => { 11 | it('#getAdminUserByUserName >should get exists user', async () => { 12 | const { container } = testConfig 13 | 14 | const authService = await container.getAsync(AuthService); 15 | const user = await authService.getAdminUserByUserName('admin'); 16 | assert.ok(user); 17 | assert.deepStrictEqual(user.username, 'admin'); 18 | }); 19 | 20 | it.skip('#getAdminUserTokenById >should get null when user not exists', async () => { 21 | const { container } = testConfig 22 | 23 | const authService = await container.getAsync(AuthService); 24 | const user = await authService.getAdminUserByUserName('fakeAdmin'); 25 | assert.deepStrictEqual(user, null); 26 | }); 27 | 28 | it('#localHandler >should get exists user and password is passed', async () => { 29 | const { app } = testConfig 30 | const ctx = app.createAnonymousContext(); 31 | 32 | const authService = await ctx.requestContext.getAsync(AuthService); 33 | const params = { username: 'admin', password: 'admin' }; 34 | const user = await authService.localHandler(params); 35 | assert.ok(user); 36 | assert.deepStrictEqual(user.username, params.username); 37 | }); 38 | 39 | it('#localHandler >should get null when user not exists', async () => { 40 | const { app } = testConfig 41 | const ctx = app.createAnonymousContext(); 42 | 43 | const authService = await ctx.requestContext.getAsync(AuthService); 44 | const params = { username: 'fakeAdmin', password: 'admin' }; 45 | const user = await authService.localHandler(params); 46 | assert.deepStrictEqual(user, null); 47 | }); 48 | 49 | it('#localHandler >should get null when user password not equal', async () => { 50 | const { app } = testConfig 51 | const ctx = app.createAnonymousContext(); 52 | 53 | const authService = await ctx.requestContext.getAsync(AuthService); 54 | const params = { username: 'admin', password: '123456' }; 55 | const user = await authService.localHandler(params); 56 | assert.deepStrictEqual(user, null); 57 | }); 58 | 59 | it('#createAdminUserToken >should created token to redis', async () => { 60 | const { app } = testConfig 61 | 62 | const authService = await app.applicationContext.getAsync(AuthService); 63 | const user = await authService.getAdminUserByUserName('admin'); 64 | assert.ok(user); 65 | const token = user && (await authService.createAdminUserToken(user)); 66 | assert.ok(token); 67 | }); 68 | 69 | it('#getAdminUserTokenById >should get token from redis', async () => { 70 | const { app } = testConfig 71 | 72 | const authService = await app.applicationContext.getAsync(AuthService); 73 | const user = await authService.getAdminUserByUserName('admin'); 74 | assert.ok(user); 75 | const token = user && (await authService.getAdminUserTokenById(user.id)); 76 | assert.ok(token); 77 | }); 78 | 79 | it('#removeAdminUserTokenById >should remove token from redis', async () => { 80 | const { app } = testConfig 81 | 82 | const authService = await app.applicationContext.getAsync(AuthService); 83 | const user = await authService.getAdminUserByUserName('admin'); 84 | assert.ok(user); 85 | const removed = 86 | user && (await authService.removeAdminUserTokenById(user.id)); 87 | assert.ok(removed); 88 | }); 89 | 90 | it('#cacheAdminUser >should get OK when cached user to redis', async () => { 91 | const { app } = testConfig 92 | 93 | const authService = await app.applicationContext.getAsync(AuthService); 94 | const user = await authService.getAdminUserByUserName('admin'); 95 | assert.ok(user); 96 | const cached = user && (await authService.cacheAdminUser(user)); 97 | assert.deepStrictEqual(cached, 'OK'); 98 | }); 99 | 100 | it('#getAdminUserById >should get userinfo from redis', async () => { 101 | const { app } = testConfig 102 | 103 | const authService = await app.applicationContext.getAsync(AuthService); 104 | const user = await authService.getAdminUserByUserName('admin'); 105 | assert.ok(user); 106 | const userinfo = user && (await authService.getAdminUserById(user.id)); 107 | assert.ok(userinfo); 108 | assert.deepStrictEqual(userinfo.username, user.username); 109 | }); 110 | 111 | it('#cleanAdminUserById >should remove userinfo from redis', async () => { 112 | const { app } = testConfig 113 | 114 | const authService = await app.applicationContext.getAsync(AuthService); 115 | const user = await authService.getAdminUserByUserName('admin'); 116 | assert.ok(user); 117 | const removed = user && (await authService.cleanAdminUserById(user.id)); 118 | assert.ok(removed); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/service/menu.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../root.config'; 5 | import { AdminMenuService } from '../../src/app/service/admin/menu'; 6 | 7 | 8 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 9 | 10 | describe(filename, () => { 11 | let currentMenu: any; 12 | 13 | it('#queryAdminMenu >should get menu list total > 0', async () => { 14 | const { app } = testConfig 15 | const ctx = app.createAnonymousContext() 16 | 17 | const menuService = await ctx.requestContext.getAsync(AdminMenuService); 18 | const queryParams = { 19 | pageSize: 10, 20 | current: 1, 21 | }; 22 | const { total } = await menuService.queryAdminMenu(queryParams); 23 | assert.ok(total); 24 | }); 25 | 26 | it('#createAdminMenu >should created menu', async () => { 27 | const { app } = testConfig 28 | const ctx = app.createAnonymousContext() 29 | 30 | const menuService = await ctx.requestContext.getAsync(AdminMenuService); 31 | const params = { 32 | parentId: '0', 33 | title: 'fakeTitle', 34 | uri: 'fakeUri', 35 | roles: ['1'], 36 | permissionId: '1', 37 | }; 38 | const menu = await menuService.createAdminMenu(params); 39 | 40 | assert.ok(menu); 41 | currentMenu = menu; 42 | }); 43 | 44 | it('#getAdminMenuById >should get menu by id', async () => { 45 | const { app } = testConfig 46 | const ctx = app.createAnonymousContext() 47 | 48 | const menuService = await ctx.requestContext.getAsync(AdminMenuService); 49 | const menu = await menuService.getAdminMenuById(currentMenu.id); 50 | 51 | assert.ok(menu); 52 | }); 53 | 54 | it('#updateAdminMenu >should update menu', async () => { 55 | const { app } = testConfig 56 | const ctx = app.createAnonymousContext() 57 | 58 | const menuService = await ctx.requestContext.getAsync(AdminMenuService); 59 | const { id } = currentMenu; 60 | const { affected } = await menuService.updateAdminMenu({ 61 | id, 62 | parentId: '0', 63 | title: 'fakeTitle2', 64 | uri: 'fakeUri2', 65 | roles: [], 66 | permissionId: '2', 67 | }); 68 | assert.ok(affected); 69 | }); 70 | 71 | it('#removeAdminMenuByIds >should remove menu', async () => { 72 | const { app } = testConfig 73 | const ctx = app.createAnonymousContext() 74 | 75 | const menuService = await ctx.requestContext.getAsync(AdminMenuService); 76 | const { id } = currentMenu; 77 | const total = await menuService.removeAdminMenuByIds([id]); 78 | assert.ok(total); 79 | }); 80 | 81 | it('#orderAdminMemu >should order menu', async () => { 82 | const { app } = testConfig 83 | const ctx = app.createAnonymousContext() 84 | 85 | const menuService = await ctx.requestContext.getAsync(AdminMenuService); 86 | const queryParams = { 87 | pageSize: 1000, 88 | current: 1, 89 | }; 90 | const { list } = await menuService.queryAdminMenu(queryParams); 91 | 92 | const newList = list.map((item, index) => { 93 | return { 94 | id: item.id, 95 | parentId: item.parentId, 96 | order: list.length - index, 97 | }; 98 | }); 99 | 100 | await menuService.orderAdminMenu(newList); 101 | 102 | const newMenu = await menuService.getAdminMenuById(list[0].id); 103 | 104 | assert.ok(newMenu) 105 | assert.deepEqual(newMenu.order, newList[0].order); 106 | 107 | const sortList = list.map((item, index) => { 108 | return { 109 | id: item.id, 110 | parentId: item.parentId, 111 | order: index + 1, 112 | }; 113 | }); 114 | await menuService.orderAdminMenu(sortList); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/service/permission.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { relative } from 'path'; 3 | import assert from 'assert'; 4 | 5 | import { testConfig } from '../root.config'; 6 | import { AdminPermissionService } from '../../src/app/service/admin/permission'; 7 | 8 | 9 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 10 | 11 | describe(filename, () => { 12 | let currentPermission: any; 13 | 14 | it('#queryAdminPermission >should get permission list total > 0', async () => { 15 | const { container } = testConfig 16 | 17 | const permissionService = await container.getAsync(AdminPermissionService); 18 | const queryParams = { 19 | pageSize: 10, 20 | current: 1, 21 | }; 22 | const { total } = await permissionService.queryAdminPermission(queryParams); 23 | assert.ok(total); 24 | }); 25 | 26 | it('#queryAdminPermission >should get permission list and sorter by id asc', async () => { 27 | const { container } = testConfig 28 | 29 | const permissionService = await container.getAsync(AdminPermissionService); 30 | const queryParams = { 31 | pageSize: 10, 32 | current: 1, 33 | }; 34 | const { list: descList } = await permissionService.queryAdminPermission( 35 | queryParams 36 | ); 37 | const { list: ascList } = await permissionService.queryAdminPermission({ 38 | ...queryParams, 39 | sorter: 'id_asc', 40 | }); 41 | 42 | assert.notDeepEqual(descList[0].id, ascList[0].id); 43 | }); 44 | 45 | it('#queryAdminPermission >should get permission list and query by id', async () => { 46 | const { container } = testConfig 47 | 48 | const permissionService = await container.getAsync(AdminPermissionService); 49 | const queryParams = { 50 | pageSize: 10, 51 | current: 1, 52 | }; 53 | const { list } = await permissionService.queryAdminPermission(queryParams); 54 | const { total } = await permissionService.queryAdminPermission({ 55 | ...queryParams, 56 | id: list[0].id, 57 | }); 58 | 59 | assert.ok(total); 60 | }); 61 | 62 | it('#queryAdminPermission >should get permission list and query by name', async () => { 63 | const { container } = testConfig 64 | 65 | const permissionService = await container.getAsync(AdminPermissionService); 66 | const queryParams = { 67 | pageSize: 10, 68 | current: 1, 69 | }; 70 | const { list } = await permissionService.queryAdminPermission(queryParams); 71 | const { total } = await permissionService.queryAdminPermission({ 72 | ...queryParams, 73 | name: list[0].name, 74 | }); 75 | 76 | assert.ok(total); 77 | }); 78 | 79 | it('#queryAdminPermission >should get permission list and query by slug', async () => { 80 | const { container } = testConfig 81 | 82 | const permissionService = await container.getAsync(AdminPermissionService); 83 | const queryParams = { 84 | pageSize: 10, 85 | current: 1, 86 | }; 87 | const { list } = await permissionService.queryAdminPermission(queryParams); 88 | const { total } = await permissionService.queryAdminPermission({ 89 | ...queryParams, 90 | slug: list[0].slug, 91 | }); 92 | 93 | assert.ok(total); 94 | }); 95 | 96 | it('#queryAdminPermission >should get permission list and query by httpPath', async () => { 97 | const { container } = testConfig 98 | 99 | const permissionService = await container.getAsync(AdminPermissionService); 100 | const queryParams = { 101 | pageSize: 10, 102 | current: 1, 103 | }; 104 | const { list } = await permissionService.queryAdminPermission(queryParams); 105 | const { total } = await permissionService.queryAdminPermission({ 106 | ...queryParams, 107 | httpPath: list[0].httpPath, 108 | }); 109 | 110 | assert.ok(total); 111 | }); 112 | 113 | it('#queryAdminPermission >should get permission list and query by httpMethod', async () => { 114 | const { container } = testConfig 115 | 116 | const permissionService = await container.getAsync(AdminPermissionService); 117 | const queryParams = { 118 | pageSize: 10, 119 | current: 1, 120 | }; 121 | const { list } = await permissionService.queryAdminPermission(queryParams); 122 | const { total } = await permissionService.queryAdminPermission({ 123 | ...queryParams, 124 | httpMethod: list[0].httpMethod[0], 125 | }); 126 | 127 | assert.ok(total); 128 | }); 129 | 130 | it('#createAdminPermission >should created permission', async () => { 131 | const { container } = testConfig 132 | 133 | const permissionService = await container.getAsync(AdminPermissionService); 134 | const params = { 135 | name: 'fakeName', 136 | slug: 'fakeSlug', 137 | httpPath: '/fake/path', 138 | httpMethod: ['GET'] 139 | }; 140 | const permission = await permissionService.createAdminPermission(params); 141 | 142 | assert.ok(permission); 143 | currentPermission = permission; 144 | }); 145 | 146 | it('#getAdminPermissionById >should get permission by id', async () => { 147 | const { container } = testConfig 148 | 149 | const permissionService = await container.getAsync(AdminPermissionService); 150 | const permission = await permissionService.getAdminPermissionById( 151 | currentPermission.id 152 | ); 153 | 154 | assert.ok(permission); 155 | }); 156 | 157 | it('#updateAdminPermission >should update permission', async () => { 158 | const { container } = testConfig 159 | 160 | const permissionService = await container.getAsync(AdminPermissionService); 161 | const { id } = currentPermission; 162 | const { affected } = await permissionService.updateAdminPermission({ 163 | id, 164 | name: 'fake2', 165 | httpPath: '/fake/path2', 166 | slug: 'fakeSlug', 167 | httpMethod: ['GET'] 168 | }); 169 | assert.ok(affected); 170 | }); 171 | 172 | it('#removeAdminPermissionByIds >should remove permission', async () => { 173 | const { container } = testConfig 174 | 175 | const permissionService = await container.getAsync(AdminPermissionService); 176 | const { id } = currentPermission; 177 | const total = await permissionService.removeAdminPermissionByIds([id]); 178 | assert.ok(total); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /test/service/rabbitmq.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../root.config'; 5 | import { RabbitmqService } from '../../src/app/service/rabbitmq'; 6 | 7 | 8 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 9 | 10 | describe(filename, () => { 11 | it('#sendToQueue >send message "hello world"', async () => { 12 | const { container } = testConfig 13 | 14 | const rabbitmqService = await container.getAsync(RabbitmqService); 15 | const res = await rabbitmqService.sendToQueue('my-queue', 'hello world'); 16 | assert.ok(res); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/service/role.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { relative } from 'path'; 3 | import assert from 'assert'; 4 | 5 | import { testConfig } from '../root.config'; 6 | import { AdminRoleService } from '../../src/app/service/admin/role'; 7 | 8 | 9 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 10 | 11 | describe(filename, () => { 12 | let currentRole: any; 13 | 14 | it('#queryAdminRole >should get role list total > 0', async () => { 15 | const { container } = testConfig 16 | 17 | const roleService = await container.getAsync(AdminRoleService); 18 | const queryParams = { 19 | pageSize: 10, 20 | current: 1, 21 | }; 22 | const { total } = await roleService.queryAdminRole(queryParams); 23 | assert.ok(total); 24 | }); 25 | 26 | it('#queryAdminRole >should get role list and query by id', async () => { 27 | const { container } = testConfig 28 | 29 | const roleService = await container.getAsync(AdminRoleService); 30 | const queryParams = { 31 | pageSize: 10, 32 | current: 1, 33 | }; 34 | const { list } = await roleService.queryAdminRole(queryParams); 35 | const { total } = await roleService.queryAdminRole({ 36 | ...queryParams, 37 | id: list[0].id, 38 | }); 39 | 40 | assert.ok(total); 41 | }); 42 | 43 | it('#queryAdminRole >should get role list and query by name', async () => { 44 | const { container } = testConfig 45 | 46 | const roleService = await container.getAsync(AdminRoleService); 47 | const queryParams = { 48 | pageSize: 10, 49 | current: 1, 50 | }; 51 | const { list } = await roleService.queryAdminRole(queryParams); 52 | const { total } = await roleService.queryAdminRole({ 53 | ...queryParams, 54 | name: list[0].name, 55 | }); 56 | 57 | assert.ok(total); 58 | }); 59 | 60 | it('#queryAdminRole >should get role list and query by slug', async () => { 61 | const { container } = testConfig 62 | 63 | const roleService = await container.getAsync(AdminRoleService); 64 | const queryParams = { 65 | pageSize: 10, 66 | current: 1, 67 | }; 68 | const { list } = await roleService.queryAdminRole(queryParams); 69 | const { total } = await roleService.queryAdminRole({ 70 | ...queryParams, 71 | slug: list[0].slug, 72 | }); 73 | 74 | assert.ok(total); 75 | }); 76 | 77 | it('#createAdminRole >should created role', async () => { 78 | const { app } = testConfig 79 | const ctx = app.createAnonymousContext(); 80 | 81 | const roleService = await ctx.requestContext.getAsync(AdminRoleService); 82 | const params = { 83 | name: 'fakeName', 84 | slug: 'fakeSlug', 85 | permissions: ['1'], 86 | }; 87 | const role = await roleService.createAdminRole(params); 88 | 89 | assert.ok(role); 90 | currentRole = role; 91 | }); 92 | 93 | it('#queryAdminRole >should get role list and sorter by id asc', async () => { 94 | const { container } = testConfig 95 | 96 | const roleService = await container.getAsync(AdminRoleService); 97 | const queryParams = { 98 | pageSize: 10, 99 | current: 1, 100 | }; 101 | const { list: descList } = await roleService.queryAdminRole(queryParams); 102 | const { list: ascList } = await roleService.queryAdminRole({ 103 | ...queryParams, 104 | sorter: 'id_asc', 105 | }); 106 | 107 | assert.notDeepEqual(descList[0].id, ascList[0].id); 108 | }); 109 | 110 | it('#getAdminRoleById >should get role by id', async () => { 111 | const { container } = testConfig 112 | 113 | const roleService = await container.getAsync(AdminRoleService); 114 | const role = await roleService.getAdminRoleById(currentRole.id); 115 | 116 | assert.ok(role); 117 | }); 118 | 119 | it('#updateAdminRole >should update role', async () => { 120 | const { app } = testConfig 121 | const ctx = app.createAnonymousContext(); 122 | 123 | const roleService = await ctx.requestContext.getAsync(AdminRoleService); 124 | const { id } = currentRole; 125 | const { affected } = await roleService.updateAdminRole({ 126 | id, 127 | name: 'fakeName2', 128 | permissions: ['2'], 129 | slug: 'fakeSlug2' 130 | }); 131 | assert.ok(affected); 132 | }); 133 | 134 | it('#removeAdminRoleByIds >should remove role', async () => { 135 | const { container } = testConfig 136 | 137 | const roleService = await container.getAsync(AdminRoleService); 138 | const { id } = currentRole; 139 | const total = await roleService.removeAdminRoleByIds([id]); 140 | assert.ok(total); 141 | }); 142 | 143 | it('#createAdminRole >should created role, no permission', async () => { 144 | const { app } = testConfig 145 | const ctx = app.createAnonymousContext(); 146 | 147 | const roleService = await ctx.requestContext.getAsync(AdminRoleService); 148 | const params = { 149 | name: 'fakeName3', 150 | slug: 'fakeSlug3', 151 | }; 152 | const role = await roleService.createAdminRole(params); 153 | 154 | assert.ok(role); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /test/service/user.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { testConfig } from '../root.config'; 5 | import { AdminUserService } from '../../src/app/service/admin/user'; 6 | 7 | 8 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 9 | 10 | describe(filename, () => { 11 | let currentUser: any; 12 | 13 | it('#queryAdminUser >should get user list total > 0', async () => { 14 | const { container } = testConfig 15 | 16 | const userService = await container.getAsync(AdminUserService); 17 | const queryParams = { 18 | pageSize: 10, 19 | current: 1, 20 | }; 21 | const { total } = await userService.queryAdminUser(queryParams); 22 | assert.ok(total); 23 | }); 24 | 25 | it('#queryAdminUser >should get user list and query by id', async () => { 26 | const { container } = testConfig 27 | 28 | const userService = await container.getAsync(AdminUserService); 29 | const queryParams = { 30 | pageSize: 10, 31 | current: 1, 32 | }; 33 | const { list } = await userService.queryAdminUser(queryParams); 34 | const { total } = await userService.queryAdminUser({ 35 | ...queryParams, 36 | id: list[0].id, 37 | }); 38 | 39 | assert.ok(total); 40 | }); 41 | 42 | it('#queryAdminUser >should get user list and query by name', async () => { 43 | const { container } = testConfig 44 | 45 | const userService = await container.getAsync(AdminUserService); 46 | const queryParams = { 47 | pageSize: 10, 48 | current: 1, 49 | }; 50 | const { list } = await userService.queryAdminUser(queryParams); 51 | const { total } = await userService.queryAdminUser({ 52 | ...queryParams, 53 | name: list[0].name, 54 | }); 55 | 56 | assert.ok(total); 57 | }); 58 | 59 | it('#queryAdminUser >should get user list and query by username', async () => { 60 | const { container } = testConfig 61 | 62 | const userService = await container.getAsync(AdminUserService); 63 | const queryParams = { 64 | pageSize: 10, 65 | current: 1, 66 | }; 67 | const { list } = await userService.queryAdminUser(queryParams); 68 | const { total } = await userService.queryAdminUser({ 69 | ...queryParams, 70 | username: list[0].username, 71 | }); 72 | 73 | assert.ok(total); 74 | }); 75 | 76 | it('#createAdminUser >should created user', async () => { 77 | const { app } = testConfig 78 | const ctx = app.createAnonymousContext(); 79 | 80 | const userService = await ctx.requestContext.getAsync(AdminUserService); 81 | const params = { 82 | name: 'fakeName3', 83 | username: 'fakeUserName3', 84 | password: ctx.helper.bhash('123456'), 85 | roles: ['1'], 86 | permissions: ['1'], 87 | }; 88 | const user = await userService.createAdminUser(params); 89 | 90 | assert.ok(user); 91 | currentUser = user; 92 | }); 93 | 94 | it('#queryAdminUser >should get user list and sorter by id asc', async () => { 95 | const { container } = testConfig 96 | 97 | const userService = await container.getAsync(AdminUserService); 98 | const queryParams = { 99 | pageSize: 10, 100 | current: 1, 101 | }; 102 | const { list: descList } = await userService.queryAdminUser(queryParams); 103 | const { list: ascList } = await userService.queryAdminUser({ 104 | ...queryParams, 105 | sorter: 'id_asc', 106 | }); 107 | 108 | assert.notDeepEqual(descList[0].id, ascList[0].id); 109 | }); 110 | 111 | it('#getAdminUserById >should get user by id', async () => { 112 | const { container } = testConfig 113 | 114 | const userService = await container.getAsync(AdminUserService); 115 | const user = await userService.getAdminUserById(currentUser.id); 116 | 117 | assert.ok(user); 118 | }); 119 | 120 | it('#updateAdminUser >should update user', async () => { 121 | const { app } = testConfig 122 | const ctx = app.createAnonymousContext(); 123 | 124 | const userService = await ctx.requestContext.getAsync(AdminUserService); 125 | const { id } = currentUser; 126 | const { affected } = await userService.updateAdminUser({ 127 | id, 128 | name: 'fakeName4', 129 | username: 'fakeUserName4', 130 | password: '123456', 131 | roles: [], 132 | permissions: [], 133 | }); 134 | assert.ok(affected); 135 | }); 136 | 137 | it('#removeAdminUserByIds >should remove user', async () => { 138 | const { container } = testConfig 139 | 140 | const userService = await container.getAsync(AdminUserService); 141 | const { id } = currentUser; 142 | const total = await userService.removeAdminUserByIds([id]); 143 | assert.ok(total); 144 | }); 145 | 146 | it('#createAdminUser >should created user, no role, no permission', async () => { 147 | const { app } = testConfig 148 | const ctx = app.createAnonymousContext(); 149 | 150 | const userService = await ctx.requestContext.getAsync(AdminUserService); 151 | const params = { 152 | name: 'fakeName5', 153 | username: 'fakeUserName5', 154 | password: ctx.helper.bhash('123456'), 155 | }; 156 | const user = await userService.createAdminUser(params); 157 | 158 | assert.ok(user); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /test/util/common.test.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | import assert from 'assert'; 3 | 4 | import { retrieveExternalNetWorkInfo } from '../../src/app/util/common' 5 | 6 | 7 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 8 | 9 | describe(filename, () => { 10 | it('Should retrieveExternalNetWorkInfo works', async () => { 11 | const infoList = retrieveExternalNetWorkInfo() 12 | assert.ok(infoList.length > 0) 13 | }) 14 | }) 15 | 16 | -------------------------------------------------------------------------------- /test/util/custom-logger.test.ts: -------------------------------------------------------------------------------- 1 | import { relative, join } from 'path'; 2 | import { 3 | clearAllLoggers, 4 | createFileLogger, 5 | } from '@midwayjs/logger' 6 | import assert from 'assert'; 7 | 8 | import { testConfig } from '../root.config'; 9 | 10 | import { updateTransformableInfo } from '../../src/app/util/custom-logger' 11 | 12 | import { removeFileOrDir, sleep, matchISO8601ContentTimes, getCurrentDateString } from './util' 13 | 14 | 15 | const filename = relative(process.cwd(), __filename).replace(/\\/ug, '/') 16 | 17 | describe(filename, () => { 18 | 19 | it('should dynamic change info data', async () => { 20 | const { logsDir } = testConfig 21 | clearAllLoggers() 22 | await removeFileOrDir(logsDir) 23 | 24 | const fileLogName = 'test-logger.log' 25 | 26 | const logger = createFileLogger('file', { 27 | dir: logsDir, 28 | fileLogName, 29 | }) 30 | 31 | // @ts-expect-error 32 | logger.updateTransformableInfo(updateTransformableInfo) 33 | logger.info('file logger') 34 | logger.warn('file logger1') 35 | logger.error('file logger2') 36 | await sleep() 37 | 38 | const dd = new Date() 39 | const d1 = getCurrentDateString() // 2021-03-18 40 | const d2 = dd.getHours().toString().padStart(2, '0') 41 | const tzo = -dd.getTimezoneOffset() 42 | const dif = tzo >= 0 ? '\\+' : '-' 43 | const off1 = pad(tzo / 60, 2) 44 | const off2 = pad(tzo % 60, 2) 45 | 46 | // '2021-03-17T19:47:28.123+08:00 47 | const needle = new RegExp(`${d1}T${d2}:\\d{2}:\\d{2}\\.\\d{3}${dif}${off1}:${off2}\\s`, 'ug') 48 | const ret = matchISO8601ContentTimes(join(logsDir, fileLogName), needle) 49 | assert.strictEqual(ret, 3) 50 | }) 51 | 52 | }) 53 | 54 | function pad(num: number, length: number): string { 55 | const norm = Math.floor(Math.abs(num)) 56 | return norm.toString().padStart(length, '0') 57 | } 58 | 59 | -------------------------------------------------------------------------------- /test/util/util.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'fs' 2 | 3 | import { remove } from 'fs-extra' 4 | 5 | 6 | export async function removeFileOrDir(path: string): Promise { 7 | await remove(path) 8 | await sleep(500) 9 | } 10 | 11 | export async function sleep(timeout = 1000): Promise { 12 | return new Promise((resolve) => { 13 | setTimeout(resolve, timeout) 14 | }) 15 | } 16 | 17 | export function matchISO8601ContentTimes(path: string, matchString: string | RegExp): number { 18 | let content: string | void 19 | const path2 = path + '.' + getCurrentDateString() 20 | 21 | if (existsSync(path)) { 22 | content = readFileSync(path, { encoding: 'utf8' }) 23 | } 24 | else if (existsSync(path2)) { 25 | content = readFileSync(path2, { encoding: 'utf8' }) 26 | } 27 | 28 | if (! content) { 29 | return 0 30 | } 31 | 32 | const regx = typeof matchString === 'string' 33 | ? new RegExp(matchString, 'ug') 34 | : matchString 35 | 36 | // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec 37 | const ret = content.match(regx) || [] 38 | return ret.length 39 | } 40 | 41 | 42 | export function getCurrentDateString(): string { 43 | const dd = new Date() 44 | return `${dd.getFullYear()}-${(dd.getMonth() + 1).toString().padStart(2, '0')}-${dd.getDate().toString().padStart(2, '0')}` 45 | } 46 | 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "declaration": true, 6 | "emitDecoratorMetadata": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "inlineSourceMap": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noImplicitThis": true, 13 | "noUnusedLocals": true, 14 | "outDir": "dist", 15 | "paths": { 16 | "@/*": ["./src/*"] 17 | }, 18 | "pretty": true, 19 | "skipLibCheck": true, 20 | "stripInternal": true, 21 | "target": "ES2020", 22 | "typeRoots": ["./typings", "./node_modules/@types"], 23 | // "types" : ["jest", "node"] 24 | }, 25 | "exclude": ["dist", "node_modules", "test"] 26 | } 27 | -------------------------------------------------------------------------------- /typings/app/extend/helper.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper 2 | // Do not modify this file!!!!!!!!! 3 | import 'egg'; 4 | import ExtendHelper from '../../../src/app/extend/helper'; 5 | type ExtendHelperType = typeof ExtendHelper; 6 | declare module 'egg' { 7 | interface IHelper extends ExtendHelperType { } 8 | } -------------------------------------------------------------------------------- /typings/app/index.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper 2 | // Do not modify this file!!!!!!!!! 3 | import 'egg'; 4 | import '@midwayjs/web'; 5 | export * from 'egg'; 6 | export as namespace Egg; -------------------------------------------------------------------------------- /typings/config/index.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper 2 | // Do not modify this file!!!!!!!!! 3 | import 'egg'; 4 | import '@midwayjs/web'; 5 | import 'egg-onerror'; 6 | import 'egg-session'; 7 | import 'egg-i18n'; 8 | import 'egg-watcher'; 9 | import 'egg-multipart'; 10 | import 'egg-security'; 11 | import 'egg-development'; 12 | import 'egg-logrotator'; 13 | import 'egg-schedule'; 14 | import 'egg-static'; 15 | import 'egg-jsonp'; 16 | import 'egg-view'; 17 | import 'midway-schedule'; 18 | import 'egg-redis'; 19 | import '@waiting/egg-jwt'; 20 | import { EggPluginItem } from 'egg'; 21 | declare module 'egg' { 22 | interface EggPlugin { 23 | onerror?: EggPluginItem; 24 | session?: EggPluginItem; 25 | i18n?: EggPluginItem; 26 | watcher?: EggPluginItem; 27 | multipart?: EggPluginItem; 28 | security?: EggPluginItem; 29 | development?: EggPluginItem; 30 | logrotator?: EggPluginItem; 31 | schedule?: EggPluginItem; 32 | static?: EggPluginItem; 33 | jsonp?: EggPluginItem; 34 | view?: EggPluginItem; 35 | schedulePlus?: EggPluginItem; 36 | redis?: EggPluginItem; 37 | jwt?: EggPluginItem; 38 | } 39 | } -------------------------------------------------------------------------------- /typings/config/plugin.d.ts: -------------------------------------------------------------------------------- 1 | // This file is created by egg-ts-helper 2 | // Do not modify this file!!!!!!!!! 3 | import 'egg'; 4 | import '@midwayjs/web'; 5 | import 'egg-onerror'; 6 | import 'egg-session'; 7 | import 'egg-i18n'; 8 | import 'egg-watcher'; 9 | import 'egg-multipart'; 10 | import 'egg-security'; 11 | import 'egg-development'; 12 | import 'egg-logrotator'; 13 | import 'egg-schedule'; 14 | import 'egg-static'; 15 | import 'egg-jsonp'; 16 | import 'egg-view'; 17 | import 'midway-schedule'; 18 | import 'egg-redis'; 19 | import '@waiting/egg-jwt'; 20 | import { EggPluginItem } from 'egg'; 21 | declare module 'egg' { 22 | interface EggPlugin { 23 | onerror?: EggPluginItem; 24 | session?: EggPluginItem; 25 | i18n?: EggPluginItem; 26 | watcher?: EggPluginItem; 27 | multipart?: EggPluginItem; 28 | security?: EggPluginItem; 29 | development?: EggPluginItem; 30 | logrotator?: EggPluginItem; 31 | schedule?: EggPluginItem; 32 | static?: EggPluginItem; 33 | jsonp?: EggPluginItem; 34 | view?: EggPluginItem; 35 | schedulePlus?: EggPluginItem; 36 | redis?: EggPluginItem; 37 | jwt?: EggPluginItem; 38 | } 39 | } --------------------------------------------------------------------------------