[
  {
    "path": ".dockerignore",
    "content": "# compiled output\n/dist\n/node_modules\npackage-lock.json\nyarn.lock\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# OS\n.DS_Store\n\n# Tests\n/coverage\n/.nyc_output\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# Code\nsrc/config/config.development.*\ndocs/*\nsql/*\ntest/*\nREADME.md"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    project: 'tsconfig.json',\n    sourceType: 'module',\n  },\n  plugins: ['@typescript-eslint/eslint-plugin'],\n  extends: [\n    'plugin:@typescript-eslint/recommended',\n    'plugin:prettier/recommended',\n  ],\n  root: true,\n  env: {\n    node: true,\n    jest: true,\n  },\n  ignorePatterns: ['.eslintrc.js'],\n  rules: {\n    '@typescript-eslint/interface-name-prefix': 'off',\n    '@typescript-eslint/explicit-function-return-type': 'off',\n    '@typescript-eslint/explicit-module-boundary-types': 'off',\n    '@typescript-eslint/no-explicit-any': 'off',\n  },\n};\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report（报告问题）\nabout: Create a report to help us improve\n---\n<!--\n    注意：为更好的解决你的问题，请参考模板提供完整信息，准确描述问题，信息不全的 issue 将被关闭。\n\n    Note: In order to better solve your problem, please refer to the template to provide complete information, accurately describe the problem, and the incomplete information issue will be closed.\n-->\n\n\n## Bug report（问题描述）\n\n#### Steps to reproduce（问题复现步骤）\n<!--\n1. [xxx]\n2. [xxx]\n3. [xxxx]\n-->\n\n#### Screenshot or Gif（截图或动态图）\n\n\n#### Link to minimal reproduction（最小可在线还原demo）\n\n<!--\nPlease only use Codepen, JSFiddle, CodeSandbox or a github repo\n-->\n\n#### Other relevant information（格外信息）\n- Your OS:\n- Node.js version:\n- Mysql version:\n- Redis version:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature Request（新功能建议）\nabout: Suggest an idea for this project\n---\n\n## Feature request（新功能建议）\n\n"
  },
  {
    "path": ".github/workflows/build-rc.yml",
    "content": "name: Build RC Image\n\non:\n  create:\n    tags:\n      - '*'\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Set up QEMU\n        uses: docker/setup-qemu-action@v1\n      -\n        name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n      -\n        name: Login to DockerHub\n        uses: docker/login-action@v1\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      -\n        name: Build and push\n        id: docker_build\n        uses: docker/build-push-action@v2\n        with:\n          push: true\n          tags: ${{ secrets.DOCKERHUB_USERNAME }}/sfnestadmin:rc\n      -\n        name: Image digest\n        run: echo ${{ steps.docker_build.outputs.digest }}\n"
  },
  {
    "path": ".github/workflows/build-stable.yml",
    "content": "name: Build Stable Image\n\non:\n  push:\n    branches:\n      - 'main'\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Set up QEMU\n        uses: docker/setup-qemu-action@v1\n      -\n        name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n      -\n        name: Login to DockerHub\n        uses: docker/login-action@v1\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      -\n        name: Build and push\n        id: docker_build\n        uses: docker/build-push-action@v2\n        with:\n          push: true\n          tags: ${{ secrets.DOCKERHUB_USERNAME }}/sfnestadmin:stable\n      -\n        name: Image digest\n        run: echo ${{ steps.docker_build.outputs.digest }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# compiled output\n/dist\n/node_modules\npackage-lock.json\nyarn.lock\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# OS\n.DS_Store\n\n# Tests\n/coverage\n/.nyc_output\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# Code\nsrc/config/config.development.*\ndocs/sample/mysql/"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"tabWidth\": 2,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:lts-alpine as builder\nWORKDIR /sf-nest-admin\n\n# set timezone\nRUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime\nRUN echo 'Asia/Shanghai' > /etc/timezone\n\n# RUN npm set registry https://registry.npm.taobao.org\n# cache step\nCOPY package.json /sf-nest-admin/package.json\nRUN yarn install\n# build\nCOPY ./ /sf-nest-admin\nRUN yarn build\n# clean dev dep\nRUN rm -rf node_modules\nRUN yarn install --production\n\n# httpserver set port\nEXPOSE 7001\n# websokcet set port\nEXPOSE 7002\n\nCMD [\"yarn\", \"start:prod\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021-present Changyuan Yang\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# sf-nest-admin\n\n![](https://img.shields.io/github/commit-activity/m/hackycy/sf-nest-admin) ![](https://img.shields.io/github/license/hackycy/sf-nest-admin) ![](https://img.shields.io/github/repo-size/hackycy/sf-nest-admin) ![](https://img.shields.io/github/languages/top/hackycy/sf-nest-admin)\n\n**基于NestJs + TypeScript + TypeORM + Redis + MySql + Vue + Element-UI编写的一款简单高效的前后端分离的权限管理系统。希望这个项目在全栈的路上能够帮助到你。**\n\n- 使用文档：[https://blog.si-yee.com/sf-admin-cli/](https://blog.si-yee.com/sf-admin-cli/)\n- 演示站点：[http://opensource.admin.si-yee.com](http://opensource.admin.si-yee.com/)\n- **Vue3版请移步：**[https://github.com/arklnk/ark-admin-nest](https://github.com/arklnk/ark-admin-nest)\n- Swagger Api文档：[http://opensource.admin.si-yee.com/api/doc/admin/swagger-api/static/index.html](http://opensource.admin.si-yee.com/api/doc/admin/swagger-api/static/index.html)\n\n演示环境账号密码：\n\n|     账号     |  密码  |           权限           |\n| :----------: | :----: | :----------------------: |\n|  openadmin   | 123456 | 仅只有各个功能的查询权限 |\n| monitoradmin | 123456 |  系统监控页面及按钮权限  |\n\n> 所有新建的用户初始密码都为123456\n\n# 欢迎Star && PR\n\n**如果项目有帮助到你可以点个Star支持下。有更好的实现欢迎PR。**\n\n# LICENSE\n\n[MIT](LICENSE)\n"
  },
  {
    "path": "docs/sample/config.development.ts",
    "content": "import * as qiniu from 'qiniu';\n\nexport default {\n  rootRoleId: 1,\n  // jwt sign secret\n  jwt: {\n    secret: process.env.JWT_SECRET || '123456',\n  },\n  // typeorm config\n  database: {\n    type: 'mysql',\n    host: '127.0.0.1',\n    port: 3306,\n    username: 'root',\n    password: '123456',\n    database: 'sf-admin',\n    synchronize: false,\n    logging: false,\n  },\n  redis: {\n    host: '127.0.0.1', // default value\n    port: 6379, // default value\n    password: '123456',\n    db: 0,\n  },\n  // qiniu config\n  qiniu: {\n    accessKey: 'xxx',\n    secretKey: 'xxx',\n    domain: 'xxx',\n    bucket: 'xxx',\n    zone: qiniu.zone.Zone_z0,\n    access: 'public',\n  },\n};\n"
  },
  {
    "path": "docs/sample/docker-compose.yml",
    "content": "version: \"2.0\"\n\nservices:\n\n  db:\n    image: mysql:5.7.34\n    command: --default-authentication-plugin=mysql_native_password\n    restart: always\n    volumes:\n      - ./mysql:/var/lib/mysql/ # ./mysql路径可以替换成自己的路径\n      - ../../sql/:/docker-entrypoint-initdb.d/ # 初始化的脚本\n    ports:\n      - 3306:3306\n    environment:\n      TZ: Asia/Shanghai\n      MYSQL_ROOT_PASSWORD: 123456\n      MYSQL_DATABASE: sf-admin\n      MYSQL_USER: sf-admin\n      MYSQL_PASSWORD: 123456\n\n  redis:\n    image: redis:alpine\n    command: --requirepass \"123456\"\n    restart: always\n    ports:\n      - 6379:6379\n    environment:\n      TZ: Asia/Shanghai\n\n  sfserver:\n    image: qa894178522/sfnestadmin:stable\n    restart: always\n    depends_on:\n      - db\n      - redis\n    environment:\n      MYSQL_HOST: db\n      MYSQL_PORT: 3306\n      MYSQL_USERNAME: sf-admin\n      MYSQL_PASSWORD: 123456\n      MYSQL_DATABASE: sf-admin\n      REDIS_HOST: redis\n      REDIS_PORT: 6379\n      REDIS_PASSWORD: 123456\n      # 可选\n      MAILER_HOST: xxx\n      MAILER_PORT: xxx\n      MAILER_USER: xxx\n      MAILER_PASS: xxx\n      AMAP_KEY: xxx\n      QINIU_ACCESSKEY: xxx\n      QINIU_SECRETKEY: xxx\n      QINIU_DOMAIN: xxx\n      QINIU_BUCKET: xxx\n      QINIU_ZONE: xxx # Zone_as0 | Zone_na0 | Zone_z0 | Zone_z1 | Zone_z2\n      QINIU_ACCESS_TYPE: public # or private\n\n  sfvue:\n    image: qa894178522/sfvueadmin:nest\n    restart: always\n    environment:\n      TZ: Asia/Shanghai\n    depends_on:\n      - sfserver\n    ports:\n      - 7002:80\n"
  },
  {
    "path": "docs/权限管理数据库设计文档.md",
    "content": "# 文档修订记录\n\n| 日期       | 版本   | 说明             | 作者    |\n| :--------- | :----- | :--------------- | :------ |\n| 2020-08-21 | v1.0.0 | 创建             | hackycy |\n| 2021-09-27 | v2.0.0 | 增加sys_config表 | hackycy |\n\n# 参考文献\n\nhttps://blog.csdn.net/gglinux/article/details/68948901\n\nhttps://www.zybuluo.com/stonezhou/note/1292262\n\nhttp://www.csdeshang.com/home/dsmall/database.html\n\n# 数据模型实体属性\n\n## sys_menu\n\n**表注释： 系统菜单**\n\n| 字段      | 类型         | 空   | 默认 | 注释                            |\n| --------- | ------------ | ---- | ---- | ------------------------------- |\n| id        | int(20)      | 否   |      | ID                              |\n| parent_id | int(20)      |      |      | 父菜单ID                        |\n| name      | varchar(255) | 否   |      | 菜单名称                        |\n| router    | varchar(255) |      |      | 菜单地址                        |\n| perms     | varchar(255) |      |      | 权限标识                        |\n| type      | tinyint(4)   | 否   | 0    | 类型，0：目录、1：菜单、2：按钮 |\n| icon      | varchar(255) |      |      | 对应图标                        |\n| order_num | int(11)      |      | 0    | 排序                            |\n| view_path | varchar(255) |      |      | 视图地址，对应vue文件           |\n| keepalive | boolean      |      | true | 路由缓存                        |\n| Is_show   | boolean      |      | true | 是否显示在菜单栏                |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段   | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ------ | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id     | 0    | A        | 否   |      |\n\n## sys_department\n\n**表注释：系统部门**\n\n| 字段      | 类型         | 空   | 默认 | 注释       |\n| --------- | ------------ | ---- | ---- | ---------- |\n| id        | int(20)      | 否   |      | ID         |\n| parent_id | int(20)      |      |      | 上级部门ID |\n| name      | varchar(255) | 否   |      | 部门名称   |\n| order_num | int(11)      |      | 0    | 排序       |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id   | 0    | A        | 否   |      |\n\n## sys_user\n\n**表注释：系统用户**\n\n| 字段          | 类型         | 空   | 默认 | 注释                                       |\n| ------------- | ------------ | ---- | ---- | ------------------------------------------ |\n| id            | int(20)      | 否   |      | ID                                         |\n| department_id | int(20)      | 否   |      | 部门编号                                   |\n| name          | varchar(255) | 否   |      | 姓名                                       |\n| username      | varchar(255) | 否   |      | 登录账号                                   |\n| password      | varchar(255) | 否   |      | 密码                                       |\n| psalt         | varchar(32)  | 否   |      | 密码盐值（随机生成，每个用户对应一个盐值） |\n| nick_name     | varchar(255) |      |      | 昵称                                       |\n| head_img      | varchar(255) |      |      | 头像                                       |\n| email         | varchar(255) |      |      | 邮箱                                       |\n| phone         | varchar(20)  |      |      | 手机号                                     |\n| remark        | varchar(255) |      |      | 备注                                       |\n| status        | tinyint(4)   |      | 1    | 状态：0：禁用，1：启用                     |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段     | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | -------- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id       | 0    | A        | 否   |      |\n| UNIQUE  | BTREE | 是   | 否   | username |      |          | 否   |      |\n\n## sys_role\n\n**表注释：系统角色**\n\n| 字段    | 类型         | 空   | 默认 | 注释   |\n| ------- | ------------ | ---- | ---- | ------ |\n| id      | int(20)      | 否   |      | ID     |\n| user_id | varchar(255) | 否   |      | 创建人 |\n| name    | varchar(255) | 否   |      | 名称   |\n| label   | varchar(50)  | 否   |      | 标签   |\n| remark  | varchar(255) |      |      | 备注   |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段  | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ----- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id    | 0    | A        | 否   |      |\n| UNIQUE  | BTREE | 是   | 否   | label |      | A        | 否   |      |\n| UNIQUE  | BTREE | 是   | 否   | name  |      | A        | 否   |      |\n\n## sys_role_department\n\n**表注释：系统角色部门关系**\n\n| 字段          | 类型    | 空   | 默认 | 注释   |\n| ------------- | ------- | ---- | ---- | ------ |\n| id            | int(20) | 否   |      | ID     |\n| role_id       | int(20) | 否   |      | 角色ID |\n| department_id | int(20) | 否   |      | 部门ID |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id   | 0    | A        | 否   |      |\n\n## sys_role_menu\n\n**表注释：系统角色菜单关系**\n\n| 字段    | 类型    | 空   | 默认 | 注释   |\n| ------- | ------- | ---- | ---- | ------ |\n| id      | int(20) | 否   |      | ID     |\n| role_id | int(20) | 否   |      | 角色ID |\n| menu_id | int(20) | 否   |      | 菜单ID |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id   | 0    | A        | 否   |      |\n\n## sys_user_role\n\n**表注释：系统用户角色关系**\n\n| 字段    | 类型    | 空   | 默认 | 注释   |\n| ------- | ------- | ---- | ---- | ------ |\n| id      | int(20) | 否   |      | ID     |\n| user_id | int(20) | 否   |      | 用户ID |\n| role_id | int(20) | 否   |      | 角色ID |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id   | 0    | A        | 否   |      |\n\n## sys_login_log\n\n**表注释：登录日志表**\n\n| 字段    | 类型         | 空   | 默认 | 注释                         |\n| ------- | ------------ | ---- | ---- | ---------------------------- |\n| id      | int(20)      | 否   |      | ID                           |\n| user_id | int(20)      |      |      | 登录的用户id                 |\n| ip      | varchar(255) |      |      | 登录ip                       |\n| time    | datetime     |      |      | 登陆时间（未使用，保留字段） |\n| ua      | varchar(500) |      |      | user-agent                   |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id   | 0    | A        | 否   |      |\n\n## sys_task\n\n**表注释：系统任务表**\n\n| 字段       | 类型         | 空   | 默认 | 注释                                  |\n| ---------- | ------------ | ---- | ---- | ------------------------------------- |\n| id         | int(20)      | 否   |      | ID                                    |\n| name       | varchar(50)  | 否   |      | 任务名称                              |\n| service    | varchar(255) | 否   |      | 需要执行的service                     |\n| type       | tinyint(4)   | 否   | 0    | 任务模式：0为cron，1为时间间隔        |\n| status     | tinyint(1)   |      | 0    | 任务状态：0为停止，1为运行            |\n| start_time | datetime     |      |      | 任务开始时间，type为0时生效           |\n| end_time   | datetime     |      |      | 任务结束时间，type为0时生效           |\n| limit      | int          |      | 0    | 最大执行次数，小于或者等于0则不限次数 |\n| cron       | varchar(255) |      |      | cron表达式，type为0时生效             |\n| every      | int          |      |      | 执行间隔，type为1时生效               |\n| data       | text         |      |      | 传入数据                              |\n| job_opts   | text         |      |      | bull job options                      |\n| remark     | varchar(255) |      |      | 备注                                  |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id   | 0    | A        | 否   |      |\n| UNIQUE  | BTREE | 是   | 否   | name |      | A        | 否   |      |\n\n## sys_task_log\n\n**表注释：任务日志表**\n\n| 字段    | 类型       | 空   | 默认 | 注释                                  |\n| ------- | ---------- | ---- | ---- | ------------------------------------- |\n| id      | int(20)    | 否   |      | id                                    |\n| task_id | int(20)    | 否   |      | 对应任务id                            |\n| status  | Tinyint(4) | 否   | 0    | 任务状态：0为失败，1为成功 |\n| detail  | Text       |      |      | 任务详情                              |\n| consume_time | int(20) | | 0 | 任务完成耗时 (ms) |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id   | 0    | A        | 否   |      |\n\n## sys_config\n\n**表注释：系统参数配置表**\n\n| 字段   | 类型         | 空   | 默认 | 注释         |\n| ------ | ------------ | ---- | ---- | ------------ |\n| id     | int(20)      | 否   |      | id           |\n| key    | varchar(50)  | 否   |      | 参数配置键   |\n| value  | varchar(255) | 是   |      | 参数配置值   |\n| name   | varchar(50)  | 否   |      | 参数配置名称 |\n| remark | varchar(255) | 是   |      | 参数配置备注 |\n\n**索引**\n\n| 键名    | 类型  | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空   | 注释 |\n| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |\n| PRIMARY | BTREE | 是   | 否   | id   | 0    | A        | 否   |      |\n| UNIQUE  | BTREE | 是   | 否   | key  |      | A        | 否   |      |\n"
  },
  {
    "path": "docs/通用协作规范.md",
    "content": "# 参考\n\nhttps://github.com/GDJiaMi/frontend-standards\n\nhttps://www.git-tower.com/learn/git/ebook/cn/command-line/advanced-topics/git-flow\n\n# 1、工作流规范（基于Git）\n\n## 1.1、开发\n\n### 1.1.1、[版本规范](https://semver.org/lang/zh-CN/)\n\n版本格式：主版本号.次版本号.修订号，版本号递增规则如下：\n\n1. 主版本号：当你做了不兼容的 API 修改，\n2. 次版本号：当你做了向下兼容的功能性新增，\n3. 修订号：当你做了向下兼容的问题修正。\n\n先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面，作为延伸。\n\n### 1.1.2、Git分支模型\n\n#### master分支\n\nmaster分支表示一个稳定的发布版本. 对应百宝袋的大版本.\n\n- 场景: 所有应用会跟随版本迭代, 在dev分支测试稳定后, 会合并到master分支, 并使用tag标记应用版本\n- tag规范: `v{version}`, 例如v0.1.0\n- 人员: 由项目负责人进行审核合并, 普通开发者没有权限\n\n#### dev分支\n\n开发者主要工作的分支, 最新的特性或bug修复都会提交到这个分支. 开发者如果在该分支进行了提交，在push到远程之前应该先pull一下， 并尽量使用rebase模式，保证分支的简洁\n\n- 命名规范: dev\n- tag规范: 在dev分支中也可能会经历发布过程, 例如bug修复版本. 这里同样使用tag来标记这些发布. 例如v0.1.1\n- 提交规范：如果实在开发分支上进行开发，在推送到远程之前，应该使用`git rebase`形式更新本地分支。\n\n#### feature分支\n\n涉及多人协作或者大功能的开发, 应该从dev分支checkout出独立的feature分支, 避免干扰dev分支\n\n- 场景:\n  - 涉及多人协作: 团队多个成员在同一个项目下负责开发不同的功能, 这时候每个成员在自己的feature分支独立开发\n  - 大功能开发: 大功能开发跨越周期比较长, 需要多次迭代才会稳定. 这时候应该在独立的分支上开发. 方便跟踪历史记录, 也免于干扰dev分支的迭代和发布\n- 命名规范\n  - feature/name: name是功能名称\n  - feature/version: 这也是团队常见的模式, 当无法使用一个功能名称来描述时, 可以使用版本号作为’功能’\n- 合并时机\n  1. 当feature分支迭代稳定, 并通过测试后, 合并到dev分支. 合并到dev后, **feature分支的生命周期就结束了**. 后续bug修复和功能优化直接在dev开发\n  2. 当多个feature分支需要合并对外发布临时版本时. 合并到preview分支 . ⚠️这种情况不应该合并到dev分支, 因为feature分支可能还不稳定或未完成. 比如为了联调某些功能.\n- 合并方式\n  - 不要使用fast-forward. 这样可以在分支图上查看到分支历史\n\n#### preview分支\n\n临时的预览分支, preview分支用于临时合并feature分支, 这其中可能会修复某些bug或者冲突. 可以选择性地将这些提交cherrypick回feature分支. 当预览结束后就可以销毁preview分支\n\n#### release分支\n\n遵循gitflow规范\n\n- 场景: 需要为某个正式版本修复bug(hotFix)时, 从master的对应tag中checkout release分支\n- 命名规范: release/{version} \n- 如何修复\n    + 如果对应bug可以在dev分支直接被修复, 可以先提交到dev分支(或者已经修复了), 然后再cherrypick到release分支\n    + 如果bug在新版本无法复现. 比如新版本升级了依赖. 那么在release分支直接修复即可\n\n### 1.1.3、提交信息规范\n\n#### 格式\n\n我们采用angular的提交规范, 在这个规范的基础上支持(可选)`emoji`进行修饰\n\n```\n<type>(<scope>): <subject>\n\n<body>\n\n<footer>\n```\n\n##### header\n\n> 如果提交时feature或者fix(已发布的版本), 这些提交信息应该出现在CHANGELOG\n\n- type: 说明commit的类别. 可以配合emoji使用, 让阅读者更快地区分提交的类型,允许以下类型:\n  - feature或feat: 引入新功能\n  - fix: 修复了bug\n  - docs: 文档\n  - style: 优化项目结构或者代码格式\n  - refactor: 代码重构. 代码重构不涉及新功能和bug修复. 不应该影响原有功能, 包括对外暴露的接口\n  - test: 增加测试\n  - chore: 构建过程, 辅助工具升级. 如升级依赖, 升级构建工具\n  - perf: 性能优化\n  - revert: revert之前的commit\n    - git revert 命令用于撤销之前的一个提交, 并在为这个撤销操作生成一个提交\n  - build或release: 构建或发布版本\n  - ci: 持续集成\n  - types: 类型定义文件更改\n  - workflow: 工作流改进\n  - wip: 开发中\n  - safe: 修复安全问题\n- scope: 可选. 说明提交影响的范围. 例如样式, 后端接口, 逻辑层等等\n- Subject: 提交目的的简短描述, 动词开头, 不超过80个字符. 不要为了提交而提交\n\n##### body\n\n可选. 对本次提交的详细描述. 如果变动很简单, 可以省略\n\n##### footer\n\n可选. 只用于说明不兼容变动(break change)和关闭 Issue(如果使用使用gitlab或github管理bug的话)\n\n#### 模板参考\n\nhttps://github.com/angular/angular/commits/master\n\n```\n# 新增一条 Commit 记录\ngit commit -m 'chore(package.json): 新增 AngularJS 规范，Commit 时会自动调用钩子（GitHook）来判断 Message 是否有效'\n\n# 搜索跟 package.json 文件相关的历史记录\ngit log HEAD --grep chore(package.json)\n```\n\n### 1.1.4、BUG处理规则\n\n对于测试，目前会经历两个阶段\n\n- 冒烟测试：在对测试正式发版之前会要求对代码进行自测，及冒烟测试。\n- 正式测试阶段：正式测试阶段测试人员会在RDMS进行bug提交和管理，对BUG的处理规则如下：\n      - [解决待关闭]: 修改了程序代码, 问题解决;\n      - [不做处理]: 没有修改程序代码, 是由于其他原因(需求变更等), 而解决的问题;\n      - [退回]: 无规律或只出现一次的BUG, 研发没找到原因, 加上必要排查日志后, 可退回给测试; 复现后重新打开\n      - [正在处理]: 已大致定位原因, 需要较多时间处理的BUG, 可置为\"正在处理\"\n\n> BUG的数量可能会和个人的KPI挂钩。所以要谨慎自测\n\n### 1.1.5、处理定制化需求\n\n- 痛点\n  - 对于定制化需求, 并不会引入到正规的代码流中, 一般情况下会checkout出一个分支, 来专门做这里定制化需求, 然后单独发版. 使用分支模式的缺点有:\n    - 更新问题\n      - 每次正规代码更新都要合并到该分支. 当分支较多时分支图就会比较混乱\n      - 正规代码合并是必然会带来风险的, 比如项目结构变动, 依赖库变动. 都可能导致定制化的代码失效\n- 解决办法\n  - 减少代码耦合\n    - 尽量将定制化需求模块化, 最小化和正规代码之间的接触面. 这是解决该问题最根本的方式.\n      - 检验方式是结构变化时, 没有或很少适配代码\n  - 考虑通过代码层面区分\n    - 例如通过权限系统来配置. 通过后端接口动态配置\n  - 优先使用fork模式\n    - 有些场景确实无法通过代码层面解决, 比如ios应用定制启动图, icon, 应用名称, 外观等等. 这种方式优先使用fork模式, fork模式和分支模式没本质区别, 但是至少可以避免干扰正规开发流程\n\n## 1.2、发布工作流\n\n- 流程\n  1. 进行代码变更\n  2. 提交这些变更, 进行CI让这些变更通过测试\n     - 如果没通过就打tag, 一旦出现测试失败, tag就得重新打\n  3. 提升package.json的版本号, 更新CHANGELOG.md\n  4. 打上tag, 提交\n  5. 可选. 合并到release分支\n- 工具\n  - 使用[jm-deploy release](https://github.com/carney520/jm-deploy)自动化发布并生成CHANGELOG.md\n\n## 1.3、持续集成\n\n所有项目基于coding的持续集成来完成。\n\n## 1.4、扩展\n\n- [如何写好 Git commit log?](https://www.zhihu.com/question/21209619)\n- [提交信息emoji规范](https://gitmoji.carloscuesta.me/)\n- [Commit message 和 Change log 编写指南](http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html)\n- [Git远程操作详解](http://www.ruanyifeng.com/blog/2014/06/git_remote.html)\n- [git钩子定制团队代码提交流程规范](https://www.jianshu.com/p/527e34f53b51)\n- [保持fork之后的项目和上游同步](https://github.com/staticblog/wiki/wiki/保持fork之后的项目和上游同步)\n\n# 2、编码规范\n\n## 2.1、代码格式化\n\n- [Prettier](https://prettier.io/) -  关于代码格式化的所有东西都交给它吧！\n\n基本上，所有代码格式相关的工作都可以交给Prettier来做，在这个基础上再使用Eslint覆盖语义相关的检查\n\n## 2.2、Code Review\n\n#### Architecture/Design\n\n- 单一职责原则.\n  - 这是经常被违背的原则。一个类只能干一个事情, 一个方法最好也只干一件事情。 比较常见的违背是一个类既干UI的事情，又干逻辑的事情, 这个在低质量的代码里很常见。\n\n- 行为是否统一\n\n  - 比如缓存是否统一，错误处理是否统一， 错误提示是否统一， 弹出框是否统一 等等。\n  - 同一逻辑/同一行为 有没有走同一Code Path？低质量程序的另一个特征是，同一行为/同一逻辑，因为出现在不同的地方或者被不同的方式触发，没有走同一Code Path 或者各处有一份copy的实现， 导致非常难以维护。\n\n- 代码污染\n\n  - 代码有没有对其他模块强耦合 ？\n\n- 重复代码\n\n  - 主要看有没有把公用组件，可复用的代码，函数抽取出来。\n\n- Open/Closed 原则\n\n  - 就是好不好扩展。 Open for extension, closed for modification.\n\n- 面向接口编程 和 不是 面向实现编程\n\n  - 主要就是看有没有进行合适的抽象， 把一些行为抽象为接口。\n\n- 健壮性\n\n  - 对Corner case有没有考虑完整，逻辑是否健壮？有没有潜在的bug？\n  - 有没有内存泄漏？有没有循环依赖?（针对特定语言，比如Objective-C) ？有没有野指针？\n  - 有没有考虑线程安全性， 数据访问的一致性\n\n- 错误处理\n\n  - 有没有很好的Error Handling？比如网络出错，IO出错。\n\n- 改动是不是对代码的提升\n\n  - 新的改动是打补丁，让代码质量继续恶化，还是对代码质量做了修复？\n\n- 效率/性能\n\n  - 客户端程序 对频繁消息 和较大数据等耗时操作是否处理得当。\n  - 关键算法的时间复杂度多少？有没有可能有潜在的性能瓶颈。\n\n其中有一部分问题，比如一些设计原则， 可预见的效率问题， 开发模式一致性的问题 应该尽早在Design Review阶段解决。如果Design阶段没有解决，那至少在Code Review阶段也要把它找出来。\n\n#### Style\n\n- 可读性\n\n  - 衡量可读性的可以有很好实践的标准，就是Reviewer能否非常容易的理解这个代码。 如果不是，那意味着代码的可读性要进行改进。\n\n- 命名\n\n  - 命名对可读性非常重要，我倾向于函数名/方法名长一点都没关系，必须是能自我阐述的。\n  - 英语用词尽量准确一点（哪怕有时候需要借助Google Translate，是值得的）\n\n- 函数长度/类长度\n\n  - 函数太长的不好阅读。 类太长了，比如超过了1000行，那你要看一下是否违反的“单一职责” 原则。\n\n- 注释\n\n  - 恰到好处的注释。 但更多我看到比较差质量的工程的一个特点是缺少注释。\n\n- 参数个数\n\n  - 不要太多， 一般不要超过3个。\n\n#### Review Your Own Code First\n\n- 跟著名的橡皮鸭调试法（Rubber Duck Debugging）一样，每次提交前整体把自己的代码过一遍非常有帮助，尤其是看看有没有犯低级错误。\n\n#### 如何进行Code Review\n\n- 多问问题。多问 “这块儿是怎么工作的？” “如果有XXX case，你这个怎么处理？”\n- 每次提交的代码不要太多，最好不要超过1000行，否则review起来效率会非常低。\n- 当面讨论代替Comments。 大部分情况下小组内的同事是坐在一起的，face to face的 code review是非常有效的。\n- 区分重点，不要舍本逐末。 优先抓住 设计，可读性，健壮性等重点问题。\n\n#### Code Review的意识\n\n- 作为一个Developer , 不仅要Deliver working code, 还要Deliver maintainable code.\n- 必要时进行重构，随着项目的迭代，在计划新增功能的同时，开发要主动计划重构的工作项。\n- 开放的心态，虚心接受大家的Review Comments。\n\n# 3、文档规范\n\n## 3.1、文档中心\n\n采用Coding提供的WIKI作为文档中心，采用Markdown格式。\n\n可视化编辑器\n\n- **Visual Code**: 大部分代码编辑都支持Markdown编辑和预览\n- [**Mou**](https://link.jianshu.com/?t=http://mouapp.com/): Mac下的老牌编辑器\n- [**typora**](https://typora.io/): 跨平台的Markdown编辑器，推荐\n\n## 3.2、代码即文档\n\n通过‘代码即文档’的方式至少可以**保持文档和代码同步更新**；另外**很多工具会分析代码的数据类型**，自动帮我们生成参数和返回值定义，这也可以减少很多文档编写工作以及出错率。\n\n相关的工具有:\n\n- API文档 \n  - Typescript \n    - [tsdoc](https://github.com/microsoft/tsdoc) Typescript官方的注释文档标准\n    - [typedoc](https://github.com/TypeStrong/typedoc) 基于tsdoc标准的文档生成器\n  - Javascript \n    - [jsdoc](https://github.com/jsdoc/jsdoc) Javascript文档注释标准和生成器\n- 后端接口文档 \n  - [Swagger](https://swagger.io) Restful接口文档规范\n  - GraphQL: 这个有很多工具，例如[graphiql](https://github.com/graphql/graphiql), 集成了Playground和文档，很先进\n  - [Easy Mock](https://easy-mock.com/login) 一个可视化，并且能快速生成模拟数据的服务\n- 组件文档 \n  - [StoryBook](https://storybook.js.org) 通用的组件开发、测试、文档工具\n  - React \n    - [Docz](http://docz.site)\n    - [Styleguidist](https://github.com/styleguidist/react-styleguidist)\n  - Vue \n    - [vue-styleguidist](https://github.com/vue-styleguidist/vue-styleguidist)\n\n## 3.3、注释即文档\n\n**必要和适量的注释对阅读源代码的人来说就是一个路牌, 可以少走很多弯路**.\n\n关于注释的一些准则，[<阿里巴巴Java开发手册>](https://github.com/alibaba/p3c/blob/master/p3c-gitbook/编程规约/注释规约.md)总结得非常好, 推荐基于这个来建立注释规范。另外通过ESlint是可以对注释进行一定程度的规范。\n\n# 4、UI规范\n\n待定\n\n# 5、测试规范\n\n![图片](/api/project/281160/files/2347052/imagePreview)\n\n## 单元测试\n\n单元测试有很多**好处**, 比如:\n\n- **提高信心，适应变化和迭代**. 如果现有代码有较为完善的单元测试，在代码重构时，可以检验模块是否依然可以工作, 一旦变更导致错误，单元测试也可以帮助我们快速定位并修复错误\n- **单元测试是集成测试的基础**\n- **测试即文档**。如果文档不能解决你的问题，在你打算看源码之前，可以查看单元测试。通过这些测试用例，开发人员可以直观地理解程序单元的基础API\n- **提升代码质量。易于测试的代码，一般都是好代码**\n\n**测什么?**\n\n业务代码或业务组件是比较难以实施单元测试的，一方面它们比较多变、另一方面很多团队很少有精力维护这部分单元测试。所以**通常只要求对一些基础/底层的组件、框架或者服务进行测试, 视情况考虑是否要测试业务代码**\n\n**测试的准则**:\n\n- 推荐Petroware的[Unit Testing Guidelines](https://petroware.no/unittesting.html), 总结了27条单元测试准则，非常受用.\n- 另外<阿里巴巴的Java开发手册>中总结的[单元测试准则](https://github.com/alibaba/p3c/blob/master/p3c-gitbook/单元测试.md), 也不错，虽然书名是Java，准则是通用的.\n\n**单元测试指标**:\n\n一般使用[`测试覆盖率`](https://zh.wikipedia.org/wiki/代碼覆蓋率)来量化，尽管对于覆盖率能不能衡量单元测试的有效性存在较多争议。\n\n大部分情况下还是推荐尽可能提高覆盖率, 比如要求`语句覆盖率达到70%；核心模块的语句覆盖率和分支覆盖率都要达到100%`. 视团队情况而定\n\n扩展:\n\n- [测试覆盖（率）到底有什么用？](https://www.infoq.cn/article/test-coverage-rate-role)\n- [阿里巴巴Java开发文档-单元测试](https://www.kancloud.cn/kanglin/java_developers_guide/539190)\n\n**相关工具**\n\n- Headless Browsers: 无头浏览器是网页自动化的重要运行环境。 常用于功能测试、单元测试、网络爬虫 \n\n  - [puppeteer](https://github.com/GoogleChrome/puppeteer)\n  - [Headless Chromium](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md)\n\n- 测试框架 \n\n  - Jest\n\n    Facebook的单元测试框架. 零配置, 支持组件快照测试、模块Mock、Spy. 一般场景, 单元测试学它一个就行了 \n\n    - 组件测试 \n      - [testing-library](https://github.com/testing-library) \n      - [Enzyme](https://github.com/airbnb/enzyme)\n\n  - [Intern](https://theintern.github.io/)\n\n- 单元测试 \n\n  - [AVA](https://github.com/avajs/ava)\n  - [Jasmine](http://jasmine.github.io/)\n  - [Mocha](http://mochajs.org/)\n  - [Tape](https://github.com/substack/tape)\n\n- 断言库 \n\n  - [Chai](http://chaijs.com/)\n  - [expect.js](https://github.com/Automattic/expect.js)\n  - [should.js](http://shouldjs.github.io/)\n\n- Mock/Stubs/Spies \n\n  - [sinon.js](http://sinonjs.org/)\n\n- 代码覆盖率 \n\n  - [istanbul](https://github.com/gotwarlost/istanbul)\n\n- 基准测试 \n\n  - [benchmark.js](http://benchmarkjs.com/)\n  - [jsperf.com](https://jsperf.com/)\n\n# 6、异常处理、监控\n\n## 6.1、异常处理\n\n参考《阿里巴巴开发手册》中的[异常处理]([https://github.com/alibaba/p3c/blob/master/p3c-gitbook/%E5%BC%82%E5%B8%B8%E6%97%A5%E5%BF%97/%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86.md](https://github.com/alibaba/p3c/blob/master/p3c-gitbook/异常日志/异常处理.md))\n\n## 6.2、日志\n\n- 避免重复打印日志\n- 谨慎地记录日志, 划分日志级别。比如生产环境禁止输出debug日志；有选择地输出info日志；\n- 使用前缀对日志进行分类, 例如: `[User] xxxx`\n\n## 6.2、异常监控\n\n异常监控通常会通过三种方式来收集异常数据:\n\n1. 全局捕获。\n2. 主动上报。在try/catch中主动上报.\n3. 用户反馈。比如弹窗让用户填写反馈信息.\n\n第三方工具推荐\n\n- [Bugly](https://bugly.qq.com/v2/) 免费\n- [Sentry](https://sentry.io/welcome/) 免费基本够用\n\n# 7、前后端协作规范\n\n## 7.1、协作流程\n\n前后端协作流程如下:\n\n![图片](/api/project/281160/files/2347269/imagePreview)\n\n1、需求分析。参与者一般有前后端、测试、以及产品. 由产品主持，对需求进行宣贯，接受开发和测试的反馈，确保大家对需求有一致的认知\n\n2、前后端开发讨论。讨论应用的一些开发设计，沟通技术点、难点、以及分工问题.\n\n3、设计接口文档。可以由前后端一起设计；或者由后端设计、前端确认是否符合要求\n\n4、并行开发。前后端并行开发，在这个阶段，前端可以先实现静态页面; 或者根据接口文档对接口进行Mock, 来模拟对接后端接口\n\n5、在联调之前，要求后端做好接口测试\n\n6、真实环境联调。前端将接口请求代理到后端服务，进行真实环境联调。\n\n## 7.2、接口规范\n\n采用RESTFUL设计规范。\n\n**需要注意的点**:\n\n- 明确区分是正常还是异常, 严格遵循接口的异常原语. 上述接口形式都有明确的异常原语，比如JSONRPC，当出现异常时应该返回`错误对象`响应，而不是在正常的响应体中返回错误代码. 另外要规范化的错误码, HTTP响应码就是一个不错的学习对象\n- 明确数据类型。很多后端写的接口都是string和number不分的，如果妥协的话、前端就需要针对这个属性做特殊处理，这也可能是潜在的bug\n- 明确空值的意义。比如在做更新操作是，空值是表示重置，还是忽略更新？\n- 响应避免冗余的嵌套。\n- 接口版本化，保持向下兼容。就像我们上文的‘语义化版本规范’说的，对于后端来说，API就是公共的接口. 公共暴露的接口应该有一个版本号，来说明当前描述的接口做了什么变动，是否向下兼容。 现在前端代码可能会在客户端被缓存，例如小程序。如果后端做了break change，就会影响这部分用户。\n\n## 7.3、接口文档规范\n\n后端通过接口文档向前端暴露接口相关的信息。通常需要包含这些信息：\n\n- 版本号\n- 文档描述\n- 服务的入口. 例如基本路径\n- 测试服务器. 可选\n- 简单使用示例\n- 安全和认证\n- 请求限制\n- 错误说明\n- 版本\n- 字段类型\n- 具体接口定义 \n  - 方法名称或者URL\n  - 方法描述\n  - 请求参数及其描述，必须说明类型(数据类型、是否可选等)\n  - 响应参数及其描述, 必须说明类型(数据类型、是否可选等)\n  - 可能的异常情况、错误代码、以及描述\n  - 请求示例，可选\n\n> 也可采用Coding提供的API文档模板来改写\n\n**人工维护导致的问题**:\n\n上文‘代码即文档’就提到了人工维护接口文档可能导致代码和文档不同步问题。\n\n如果可以从代码或者规范文档(例如OpenAPI这类API描述规范)中生成接口文档，可以解决实现和文档不一致问题, 同时也可以减少文档编写和维护的投入.\n\n**项目采用Coding提供的API文档来自动生成API文档。**\n\n\n"
  },
  {
    "path": "nest-cli.json",
    "content": "{\n  \"collection\": \"@nestjs/schematics\",\n  \"sourceRoot\": \"src\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"sf-nest-admin\",\n  \"version\": \"2.3.3\",\n  \"description\": \"simple and efficient authority management system with separation of front and backends\",\n  \"author\": \"hackycy\",\n  \"private\": true,\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"prebuild\": \"rimraf dist\",\n    \"build\": \"nest build\",\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"start\": \"cross-env NODE_ENV=development nest start\",\n    \"dev\": \"cross-env NODE_ENV=development nest start --watch\",\n    \"start:debug\": \"cross-env NODE_ENV=development nest start --debug --watch\",\n    \"start:prod\": \"cross-env NODE_ENV=production node dist/main.js\",\n    \"lint\": \"eslint \\\"{src,apps,libs,test}/**/*.ts\\\" --fix\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"test:cov\": \"jest --coverage\",\n    \"test:debug\": \"node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand\",\n    \"test:e2e\": \"jest --config ./test/jest-e2e.json\",\n    \"cleanlog\": \"rimraf logs\"\n  },\n  \"dependencies\": {\n    \"@nestjs/axios\": \"^0.0.2\",\n    \"@nestjs/bull\": \"^0.4.1\",\n    \"@nestjs/common\": \"^8.4.6\",\n    \"@nestjs/config\": \"^1.1.5\",\n    \"@nestjs/core\": \"^8.4.6\",\n    \"@nestjs/jwt\": \"^8.0.0\",\n    \"@nestjs/platform-fastify\": \"^8.4.6\",\n    \"@nestjs/platform-socket.io\": \"^8.4.6\",\n    \"@nestjs/swagger\": \"^5.1.3\",\n    \"@nestjs/typeorm\": \"^8.1.2\",\n    \"@nestjs/websockets\": \"^8.4.6\",\n    \"bull\": \"^3.22.5\",\n    \"cache-manager\": \"^3.4.3\",\n    \"class-transformer\": \"^0.5.1\",\n    \"class-validator\": \"^0.13.2\",\n    \"cross-env\": \"^7.0.3\",\n    \"crypto-js\": \"^4.0.0\",\n    \"date-fns\": \"^2.21.3\",\n    \"fastify-swagger\": \"^5.2.0\",\n    \"ioredis\": \"^4.27.6\",\n    \"lodash\": \"^4.17.21\",\n    \"mysql2\": \"^2.2.5\",\n    \"nanoid\": \"^3.1.22\",\n    \"qiniu\": \"^7.3.2\",\n    \"reflect-metadata\": \"^0.1.13\",\n    \"rxjs\": \"^7.2.0\",\n    \"svg-captcha\": \"^1.4.0\",\n    \"systeminformation\": \"^5.9.4\",\n    \"typeorm\": \"^0.3.6\",\n    \"ua-parser-js\": \"^0.7.28\",\n    \"winston\": \"^3.3.3\",\n    \"winston-daily-rotate-file\": \"^4.5.5\"\n  },\n  \"devDependencies\": {\n    \"@nestjs/cli\": \"^8.1.1\",\n    \"@nestjs/schematics\": \"^8.0.2\",\n    \"@nestjs/testing\": \"^8.4.6\",\n    \"@types/bull\": \"^3.15.1\",\n    \"@types/cache-manager\": \"^3.4.0\",\n    \"@types/jest\": \"^28.1.0\",\n    \"@types/lodash\": \"^4.14.168\",\n    \"@types/node\": \"^14.14.36\",\n    \"@types/supertest\": \"^2.0.10\",\n    \"@types/ua-parser-js\": \"^0.7.35\",\n    \"@typescript-eslint/eslint-plugin\": \"^4.19.0\",\n    \"@typescript-eslint/parser\": \"^4.19.0\",\n    \"eslint\": \"^7.22.0\",\n    \"eslint-config-prettier\": \"^8.1.0\",\n    \"eslint-plugin-prettier\": \"^3.3.1\",\n    \"jest\": \"^28.1.0\",\n    \"prettier\": \"^2.2.1\",\n    \"rimraf\": \"^3.0.2\",\n    \"supertest\": \"^6.1.3\",\n    \"ts-jest\": \"^28.0.4\",\n    \"ts-loader\": \"^9.3.0\",\n    \"ts-node\": \"^10.8.0\",\n    \"tsconfig-paths\": \"^3.9.0\",\n    \"typescript\": \"^4.2.3\"\n  },\n  \"jest\": {\n    \"moduleFileExtensions\": [\n      \"js\",\n      \"json\",\n      \"ts\"\n    ],\n    \"rootDir\": \"src\",\n    \"testRegex\": \".*\\\\.spec\\\\.ts$\",\n    \"transform\": {\n      \"^.+\\\\.(t|j)s$\": \"ts-jest\"\n    },\n    \"collectCoverageFrom\": [\n      \"**/*.(t|j)s\"\n    ],\n    \"coverageDirectory\": \"../coverage\",\n    \"testEnvironment\": \"node\"\n  }\n}\n"
  },
  {
    "path": "sql/init.sql",
    "content": "/*\n Date: 26/10/2020 17:30:38\n*/\n\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n-- ----------------------------\n-- Table structure for sys_department\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_department`;\nCREATE TABLE `sys_department` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `parent_id` int(11) DEFAULT NULL,\n  `name` varchar(255) NOT NULL,\n  `order_num` int(11) DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Records of sys_department\n-- ----------------------------\nBEGIN;\nINSERT INTO `sys_department` VALUES ('2020-08-27 03:33:19.000000', '2020-08-27 03:33:19.000000', 1, NULL, '思忆技术', 0);\nINSERT INTO `sys_department` VALUES ('2020-09-08 05:31:32.426851', '2020-10-07 04:25:31.000000', 2, 1, '管理部门', 0);\nCOMMIT;\n\n-- ----------------------------\n-- Table structure for sys_login_log\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_login_log`;\nCREATE TABLE `sys_login_log` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `user_id` int(20) DEFAULT NULL,\n  `ip` varchar(255) DEFAULT NULL,\n  `time` datetime DEFAULT NULL,\n  `ua` varchar(500) DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Table structure for sys_menu\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_menu`;\nCREATE TABLE `sys_menu` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `parent_id` int(11) DEFAULT NULL,\n  `name` varchar(255) NOT NULL,\n  `router` varchar(255) DEFAULT NULL,\n  `perms` varchar(255) DEFAULT NULL,\n  `type` tinyint(4) NOT NULL DEFAULT '0',\n  `icon` varchar(255) DEFAULT NULL,\n  `order_num` int(11) DEFAULT '0',\n  `view_path` varchar(255) DEFAULT NULL,\n  `keepalive` tinyint(4) DEFAULT '1',\n  `is_show` tinyint(4) DEFAULT '1',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=69 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Records of sys_menu\n-- ----------------------------\nBEGIN;\nINSERT INTO `sys_menu` VALUES ('2020-08-28 10:09:26.322745', '2020-10-12 06:35:18.000000', 1, NULL, '系统', '/sys', NULL, 0, 'system', 255, NULL, 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-08-01 00:00:00.000000', '2020-09-14 03:53:31.000000', 3, 1, '权限管理', '/sys/permssion', NULL, 0, 'permission', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-08-08 00:00:00.000000', '2020-09-08 06:54:45.000000', 4, 3, '用户列表', '/sys/permssion/user', NULL, 1, 'peoples', 0, 'views/system/permission/user', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-08-15 00:00:00.000000', '2020-09-11 06:11:52.000000', 5, 4, '新增', NULL, 'sys:user:add', 2, NULL, 0, NULL, 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-08-15 00:00:00.000000', '2020-09-11 06:13:03.000000', 6, 4, '删除', NULL, 'sys:user:delete', 2, NULL, 0, NULL, 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-08-08 00:00:00.000000', '2020-09-24 09:51:40.000000', 7, 3, '菜单列表', '/sys/permssion/menu', NULL, 1, 'menu', 0, 'views/system/permission/menu', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-08-15 00:00:00.000000', '2020-08-15 00:00:00.000000', 8, 7, '新增', NULL, 'sys:menu:add', 2, NULL, 0, NULL, 1, 0);\nINSERT INTO `sys_menu` VALUES ('2020-08-15 00:00:00.000000', '2020-08-15 00:00:00.000000', 9, 7, '删除', NULL, 'sys:menu:delete', 2, NULL, 0, NULL, 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-02 08:22:27.548410', '2020-09-02 08:22:27.548410', 10, 7, '查询', NULL, 'sys:menu:list,sys:menu:info', 2, NULL, 0, NULL, 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-04 06:26:36.408290', '2020-09-04 07:13:30.000000', 17, 16, '测试', '', 'sys:menu:list,sys:menu:update,sys:menu:info,sys:menu:add', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-04 08:08:53.621419', '2020-09-04 08:08:53.621419', 19, 7, '修改', '', 'sys:menu:update', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-04 09:41:43.133191', '2020-09-24 09:16:56.000000', 23, 3, '角色列表', '/sys/permission/role', '', 1, 'role', 0, 'views/system/permission/role', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-07 02:44:27.663925', '2020-09-07 08:51:18.000000', 25, 23, '删除', '', 'sys:role:delete', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-07 02:49:36.058795', '2020-09-14 03:56:56.000000', 26, 44, '饿了么文档', 'http://element-cn.eleme.io/#/zh-CN/component/installation', '', 1, 'international', 0, 'views/charts/keyboard', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-07 02:50:03.345817', '2020-09-14 03:56:47.000000', 27, 44, 'TypeORM中文文档', 'https://www.bookstack.cn/read/TypeORM-0.2.20-zh/README.md', '', 1, 'international', 2, 'views/error-log/components/ErrorTestB', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-07 07:08:18.106272', '2020-09-14 10:26:58.000000', 28, 23, '新增', '', 'sys:role:add', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-07 08:51:48.319938', '2020-09-07 08:51:58.000000', 29, 23, '修改', '', 'sys:role:update', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-07 10:39:50.396350', '2020-09-09 06:34:13.000000', 32, 23, '查询', '', 'sys:role:list,sys:role:page,sys:role:info', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-08 05:29:40.117403', '2020-09-11 06:03:43.000000', 33, 4, '部门查询', '', 'sys:dept:list,sys:dept:info', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-09 07:10:08.435753', '2020-09-10 03:41:32.000000', 34, 4, '查询', '', 'sys:user:page,sys:user:info', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-10 05:09:31.904519', '2020-09-10 05:09:31.904519', 35, 4, '更新', '', 'sys:user:update', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-10 08:02:29.853643', '2020-09-10 08:02:40.000000', 36, 4, '部门转移', '', 'sys:dept:transfer', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-11 04:34:00.379002', '2020-09-14 03:29:59.000000', 37, 1, '系统监控', '/sys/monitor', '', 0, 'monitor', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-11 06:12:14.621531', '2020-09-11 06:12:14.621531', 39, 4, '部门新增', '', 'sys:dept:add', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-11 06:13:23.752133', '2020-09-11 06:13:23.752133', 40, 4, '部门删除', '', 'sys:dept:delete', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-11 06:29:52.437621', '2020-09-11 06:29:52.437621', 41, 4, '部门更新', '', 'sys:dept:update', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2021-04-12 04:28:03.312443', '2021-04-20 10:18:22', 20, 4, '部门移动排序', NULL, 'sys:dept:move', 2, NULL, 255, NULL, 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-09-14 03:56:24.740870', '2020-10-09 07:47:05.000000', 44, NULL, '文档', '/document', '', 0, 'documentation', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-12 10:00:49.463487', '2020-10-12 10:00:49.463487', 51, 37, '在线用户', '/sys/monitor/online', NULL, 1, 'people', 0, 'views/system/monitor/online', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-13 03:01:13.787832', '2020-10-13 03:01:13.787832', 52, 51, '查询', '', 'sys:online:list', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-13 03:01:51.480667', '2020-10-13 03:01:51.480667', 53, 51, '下线', '', 'sys:online:kick', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-13 09:52:08.932501', '2020-10-13 09:53:44.000000', 55, 37, '登录日志', '/sys/monitor/login-log', NULL, 1, 'guide', 0, 'views/system/monitor/login-log', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-13 09:56:13.285772', '2020-10-13 09:56:13.285772', 56, 55, '查询', '', 'sys:log:login:page', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 03:07:18.221647', '2020-10-19 07:26:37.000000', 57, 1, '任务调度', '/sys/schedule', NULL, 0, 'task', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 03:08:15.925726', '2020-10-19 07:21:04.000000', 58, 57, '定时任务', '/sys/schedule/task', NULL, 1, 'schedule', 0, 'views/system/schedule/task', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 03:08:36.247678', '2020-10-19 03:08:36.247678', 59, 58, '查询', '', 'sys:task:page,sys:task:info', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 03:09:09.436949', '2020-10-19 03:09:09.436949', 60, 58, '新增', '', 'sys:task:add', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 03:09:42.895534', '2020-10-19 03:09:42.895534', 61, 58, '更新', '', 'sys:task:update', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 05:45:30.512641', '2020-10-19 05:45:30.512641', 62, 58, '执行一次', '', 'sys:task:once', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 05:46:01.910857', '2020-10-19 05:46:01.910857', 63, 58, '运行', '', 'sys:task:start', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 05:46:23.694028', '2020-10-19 05:46:23.694028', 64, 58, '暂停', '', 'sys:task:stop', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 06:25:52.225518', '2020-10-19 06:25:52.225518', 65, 58, '删除', '', 'sys:task:delete', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 07:30:18.456330', '2020-10-19 07:30:18.456330', 66, 57, '任务日志', '/sys/schedule/log', NULL, 1, 'schedule-log', 0, 'views/system/schedule/log', 1, 1);\nINSERT INTO `sys_menu` VALUES ('2020-10-19 08:09:49.063343', '2020-10-19 08:09:49.063343', 67, 66, '查询', '', 'sys:log:task:page', 2, '', 0, '', 1, 1);\nINSERT INTO `sys_menu` VALUES('2021-04-21 08:54:41.018924000', '2021-04-21 08:54:41.018924000', 68, 4, '更改密码', NULL, 'sys:user:password', 2, NULL, 255, NULL, 1, 1);\nCOMMIT;\n\n-- ----------------------------\n-- Table structure for sys_req_log\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_req_log`;\nCREATE TABLE `sys_req_log` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `ip` varchar(255) DEFAULT NULL,\n  `user_id` int(11) DEFAULT NULL,\n  `params` text,\n  `action` varchar(100) DEFAULT NULL,\n  `method` varchar(15) DEFAULT NULL,\n  `status` int(11) DEFAULT NULL,\n  `consume_time` int(11) DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Table structure for sys_role\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_role`;\nCREATE TABLE `sys_role` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `user_id` varchar(255) NOT NULL,\n  `name` varchar(255) NOT NULL,\n  `label` varchar(50) NOT NULL,\n  `remark` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `IDX_223de54d6badbe43a5490450c3` (`name`),\n  UNIQUE KEY `IDX_f2d07943355da93c3a8a1c411a` (`label`)\n) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Records of sys_role\n-- ----------------------------\nBEGIN;\nINSERT INTO `sys_role` VALUES ('2020-08-27 03:35:05.000000', '2020-08-27 03:35:05.000000', 1, 'root', 'root', '超级管理员', NULL);\nCOMMIT;\n\n-- ----------------------------\n-- Table structure for sys_role_department\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_role_department`;\nCREATE TABLE `sys_role_department` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `role_id` int(11) NOT NULL,\n  `department_id` int(11) NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Table structure for sys_role_menu\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_role_menu`;\nCREATE TABLE `sys_role_menu` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `role_id` int(11) NOT NULL,\n  `menu_id` int(11) NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Table structure for sys_task\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_task`;\nCREATE TABLE `sys_task` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `name` varchar(50) NOT NULL,\n  `service` varchar(255) NOT NULL,\n  `type` tinyint(4) NOT NULL DEFAULT '0',\n  `status` tinyint(4) NOT NULL DEFAULT '1',\n  `start_time` datetime DEFAULT NULL,\n  `end_time` datetime DEFAULT NULL,\n  `limit` int(11) DEFAULT '0',\n  `cron` varchar(255) DEFAULT NULL,\n  `every` int(11) DEFAULT NULL,\n  `data` text,\n  `job_opts` text,\n  `remark` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `IDX_ef8e5ab5ef2fe0ddb1428439ef` (`name`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Records of sys_task\n-- ----------------------------\nBEGIN;\n-- INSERT INTO `sys_task` VALUES ('2020-10-19 08:53:44.732338', '2020-10-26 09:28:23.000000', 1, '定时清空请求追踪日志', 'SysLogClearJob.clearReqLog', 0, 1, NULL, NULL, 0, '0 0 3 ? * 1', 1000, '', '{\\\"count\\\":1,\\\"cron\\\":\\\"0 0 3 ? * 1\\\",\\\"jobId\\\":1}', '');\nINSERT INTO `sys_task` VALUES ('2020-10-19 08:54:42.760785', '2020-10-26 09:28:23.000000', 2, '定时清空登录日志', 'SysLogClearJob.clearLoginLog', 0, 1, NULL, NULL, 0, '0 0 3 ? * 1', 0, '', '{\\\"count\\\":1,\\\"cron\\\":\\\"0 0 3 ? * 1\\\",\\\"jobId\\\":2}', '');\nINSERT INTO `sys_task` VALUES ('2020-10-19 08:55:06.050711', '2020-10-26 09:28:23.000000', 3, '定时清空任务日志', 'SysLogClearJob.clearTaskLog', 0, 1, NULL, NULL, 0, '0 0 3 ? * 1', 0, '', '{\\\"count\\\":1,\\\"cron\\\":\\\"0 0 3 ? * 1\\\",\\\"jobId\\\":3}', '');\nCOMMIT;\n\n-- ----------------------------\n-- Table structure for sys_task_log\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_task_log`;\nCREATE TABLE `sys_task_log` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `task_id` int(11) NOT NULL,\n  `status` tinyint(4) NOT NULL DEFAULT '0',\n  `detail` text,\n  `consume_time` int(11) DEFAULT '0',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Table structure for sys_user\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_user`;\nCREATE TABLE `sys_user` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `department_id` int(11) NOT NULL,\n  `name` varchar(255) NOT NULL,\n  `username` varchar(255) NOT NULL,\n  `password` varchar(255) NOT NULL,\n  `nick_name` varchar(255) DEFAULT NULL,\n  `head_img` varchar(255) DEFAULT NULL,\n  `email` varchar(255) DEFAULT NULL,\n  `phone` varchar(255) DEFAULT NULL,\n  `remark` varchar(255) DEFAULT NULL,\n  `psalt` varchar(32) NOT NULL,\n  `status` tinyint(4) DEFAULT '1',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `IDX_9e7164b2f1ea1348bc0eb0a7da` (`username`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Records of sys_user\n-- ----------------------------\nBEGIN;\nINSERT INTO `sys_user` VALUES ('2020-08-27 03:38:30.000000', '2020-10-07 07:17:14.000000', 1, 1, 'hackycy', 'rootadmin', 'ccdb5f7e5be14fe0c0528974428f79f9', '', 'http://image.si-yee.com/思忆/20200924_021100.png', 'qa894178522@qq.com', '15622472425', NULL, 'xQYCspvFb8cAW6GG1pOoUGTLqsuUSO3d',1);\nCOMMIT;\n\n-- ----------------------------\n-- Table structure for sys_user_role\n-- ----------------------------\nDROP TABLE IF EXISTS `sys_user_role`;\nCREATE TABLE `sys_user_role` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `user_id` int(11) NOT NULL,\n  `role_id` int(11) NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;\n\n-- ----------------------------\n-- Records of sys_user_role\n-- ----------------------------\nBEGIN;\nINSERT INTO `sys_user_role` VALUES ('2020-09-14 04:10:34.371646', '2020-09-14 04:10:34.371646', 1, 1, 1);\nCOMMIT;\n\nSET FOREIGN_KEY_CHECKS = 1;\n"
  },
  {
    "path": "sql/upgrade_20210508.sql",
    "content": "-- ----------------------------\n-- 增加七牛文件空间\n-- ----------------------------\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n--\n-- 转存表中的数据 `sys_menu`\n--\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 01:38:40.990691','2021-05-16 01:38:40.990691',72,null,'网盘空间','/netdisk',null,0,'netdisk',255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 01:39:06.265986','2021-05-16 01:39:06.265986',73,72,'空间管理','/netdisk/manage',null,1,'netdisk-manage',255,'views/netdisk/manage',1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 01:40:03.423681','2021-05-16 01:40:03.423681',74,73,'查询',null,'netdisk:manage:list',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 01:40:27.605473','2021-05-16 01:40:27.605473',75,73,'创建文件夹',null,'netdisk:manage:mkdir',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 01:40:42.986572','2021-05-16 01:40:42.986572',76,73,'上传',null,'netdisk:manage:token',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 01:40:57.687251','2021-05-16 01:41:36.000000',77,73,'重命名',null,'netdisk:manage:rename,netdisk:manage:check',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 01:41:15.070191','2021-05-16 01:41:15.070191',78,73,'下载',null,'netdisk:manage:download',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 01:41:56.637858','2021-05-16 01:41:56.637858',79,73,'删除',null,'netdisk:manage:delete,netdisk:manage:check',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 01:42:17.793185','2021-05-16 01:42:17.793185',80,73,'预览',null,'netdisk:manage:info',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-16 23:42:36.775883','2021-05-16 23:42:36.775883',81,73,'备注',null,'netdisk:manage:mark',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-20 21:53:56.574672','2021-05-20 21:53:56.574672',82,73,'复制',null,'netdisk:manage:check,netdisk:manage:copy',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-20 21:54:18.770632','2021-05-20 21:54:18.770632',83,73,'剪切',null,'netdisk:manage:check,netdisk:manage:cut',2,null,255,null,1,1);\nINSERT INTO `sys_menu` (`created_at`,`updated_at`,`id`,`parent_id`,`name`,`router`,`perms`,`type`,`icon`,`order_num`,`view_path`,`keepalive`,`is_show`) VALUES ('2021-05-27 15:30:32.119664','2021-05-27 15:30:32.119664',84,72,'网盘概览','/netdisk/overview',null,1,'disk-overview',255,'views/netdisk/overview',1,1);"
  },
  {
    "path": "sql/upgrade_20210914.sql.migrate",
    "content": "-- 用于旧版本兼容迁移脚本，请手动执行\n\nDROP TABLE IF EXISTS `sys_req_log`;\n\nALTER TABLE `sys_department` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_department` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_department` CHANGE parend_id parent_id int(11) DEFAULT NULL;\n\nALTER TABLE `sys_login_log` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_login_log` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\n\nALTER TABLE `sys_menu` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_menu` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_menu` CHANGE isShow is_show tinyint(4) DEFAULT '1';\n\nALTER TABLE `sys_role` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_role` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_role` CHANGE userId user_id varchar(255) NOT NULL;\n\nALTER TABLE `sys_role_department` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_role_department` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\n\nALTER TABLE `sys_role_menu` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_role_menu` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\n\nALTER TABLE `sys_task` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_task` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\n\nALTER TABLE `sys_task_log` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_task_log` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\n\nALTER TABLE `sys_user` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_user` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\n\nALTER TABLE `sys_user_role` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\nALTER TABLE `sys_user_role` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;\n\nDELETE FROM `sys_menu` WHERE id = 38;\nDELETE FROM `sys_role_menu` WHERE `menu_id` = 38;\nDELETE FROM `sys_task` WHERE id = 1;\n"
  },
  {
    "path": "sql/upgrade_20210927.sql",
    "content": "-- `sf-admin`.sys_config definition\n\nDROP TABLE IF EXISTS `sys_config`;\nCREATE TABLE `sys_config` (\n  `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `key` varchar(50) NOT NULL,\n  `name` varchar(50) NOT NULL,\n  `value` varchar(255) DEFAULT NULL,\n  `remark` varchar(255) DEFAULT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `IDX_2c363c25cf99bcaab3a7f389ba` (`key`)\n) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;\n\nBEGIN;\n--\n-- 转存表中的数据 `sys_menu`\n--\nINSERT INTO `sys_menu` (`created_at`, `updated_at`, `id`, `parent_id`, `name`, `router`, `perms`, `type`, `icon`, `order_num`, `view_path`, `keepalive`, `is_show`) VALUES\n('2021-09-28 03:22:42.570291', '2021-09-28 03:22:42.570291', 85, 1, '参数配置', '/sys/param-config', NULL, 0, 'param-config', 255, NULL, 1, 1),\n('2021-09-28 03:25:47.197582', '2021-09-28 03:25:47.197582', 86, 85, '参数列表', '/sys/param-config/list', NULL, 1, 'param-config-list', 255, 'views/system/param-config/config-list', 1, 1),\n('2021-09-28 03:26:27.243134', '2021-09-28 07:55:44.000000', 87, 86, '查询', NULL, 'sys:param-config:page,sys:param-config:info', 2, NULL, 255, NULL, 1, 1),\n('2021-09-28 07:56:03.132765', '2021-09-28 07:56:03.132765', 88, 86, '新增', NULL, 'sys:param-config:add', 2, NULL, 255, NULL, 1, 1),\n('2021-09-28 07:56:26.180445', '2021-09-28 07:56:26.180445', 89, 86, '删除', NULL, 'sys:param-config:delete', 2, NULL, 255, NULL, 1, 1),\n('2021-09-28 07:56:47.269451', '2021-09-28 07:56:47.269451', 90, 86, '更新', NULL, 'sys:param-config:update', 2, NULL, 255, NULL, 1, 1),\n('2021-10-11 09:53:38.305927', '2021-10-12 07:20:18.000000', 91, 37, '服务监控', '/sys/monitor/serve', NULL, 1, 'serve', 255, 'views/system/monitor/serve', 1, 1);\n\n--\n-- 转存表中的数据 `sys_config`\n--\nINSERT INTO `sys_config` (created_at,updated_at,`key`,name,value,remark) VALUES\n\t ('2021-09-28 03:14:05.256120000','2021-09-28 03:14:05.256120000','sys_user_initPassword','初始密码','123456','创建管理员账号的初始密码');\nCOMMIT;"
  },
  {
    "path": "src/app.module.ts",
    "content": "import './polyfill';\n\nimport { Module } from '@nestjs/common';\nimport { ConfigModule, ConfigService } from '@nestjs/config';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { BullModule } from '@nestjs/bull';\nimport Configuration from './config/configuration';\nimport { AdminModule } from './modules/admin/admin.module';\nimport { SharedModule } from './shared/shared.module';\nimport { MissionModule } from './mission/mission.module';\nimport { WSModule } from './modules/ws/ws.module';\nimport { LoggerModule } from './shared/logger/logger.module';\nimport {\n  LoggerModuleOptions,\n  WinstonLogLevel,\n} from './shared/logger/logger.interface';\nimport { TypeORMLoggerService } from './shared/logger/typeorm-logger.service';\nimport { LOGGER_MODULE_OPTIONS } from './shared/logger/logger.constants';\n\n@Module({\n  imports: [\n    ConfigModule.forRoot({\n      isGlobal: true,\n      load: [Configuration],\n    }),\n    TypeOrmModule.forRootAsync({\n      imports: [ConfigModule, LoggerModule],\n      useFactory: (\n        configService: ConfigService,\n        loggerOptions: LoggerModuleOptions,\n      ) => ({\n        autoLoadEntities: true,\n        type: configService.get<any>('database.type'),\n        host: configService.get<string>('database.host'),\n        port: configService.get<number>('database.port'),\n        username: configService.get<string>('database.username'),\n        password: configService.get<string>('database.password'),\n        database: configService.get<string>('database.database'),\n        synchronize: configService.get<boolean>('database.synchronize'),\n        logging: configService.get('database.logging'),\n        // 自定义日志\n        logger: new TypeORMLoggerService(\n          configService.get('database.logging'),\n          loggerOptions,\n        ),\n      }),\n      inject: [ConfigService, LOGGER_MODULE_OPTIONS],\n    }),\n    BullModule.forRoot({}),\n    // custom logger\n    LoggerModule.forRootAsync(\n      {\n        imports: [ConfigModule],\n        useFactory: (configService: ConfigService) => {\n          return {\n            level: configService.get<WinstonLogLevel>('logger.level'),\n            consoleLevel: configService.get<WinstonLogLevel>(\n              'logger.consoleLevel',\n            ),\n            timestamp: configService.get<boolean>('logger.timestamp'),\n            maxFiles: configService.get<string>('logger.maxFiles'),\n            maxFileSize: configService.get<string>('logger.maxFileSize'),\n            disableConsoleAtProd: configService.get<boolean>(\n              'logger.disableConsoleAtProd',\n            ),\n            dir: configService.get<string>('logger.dir'),\n            errorLogName: configService.get<string>('logger.errorLogName'),\n            appLogName: configService.get<string>('logger.appLogName'),\n          };\n        },\n        inject: [ConfigService],\n      },\n      // global module\n      true,\n    ),\n    // custom module\n    SharedModule,\n    // mission module\n    MissionModule.forRoot(),\n    // application modules import\n    AdminModule,\n    // websocket module\n    WSModule,\n  ],\n})\nexport class AppModule {}\n"
  },
  {
    "path": "src/common/class/res.class.ts",
    "content": "export class ResOp {\n  readonly data: any;\n  readonly code: number;\n  readonly message: string;\n\n  constructor(code: number, data?: any, message = 'success') {\n    this.code = code;\n    this.data = data;\n    this.message = message;\n  }\n\n  static success(data?: any) {\n    return new ResOp(200, data);\n  }\n}\n\nexport class Pagination {\n  total: number;\n  page: number;\n  size: number;\n}\n\nexport class PageResult<T> {\n  list?: Array<T>;\n  pagination: Pagination;\n}\n"
  },
  {
    "path": "src/common/contants/decorator.contants.ts",
    "content": "// @Keep\nexport const TRANSFORM_KEEP_KEY_METADATA = 'common:transform_keep';\n\n// @Mission\nexport const MISSION_KEY_METADATA = 'common:mission';\n"
  },
  {
    "path": "src/common/contants/error-code.contants.ts",
    "content": "/**\n * 统一错误代码定义\n */\nexport const ErrorCodeMap = {\n  // 10000 - 99999 业务操作错误\n  10000: '参数校验异常',\n  10001: '系统用户已存在',\n  10002: '填写验证码有误',\n  10003: '用户名密码有误',\n  10004: '节点路由已存在',\n  10005: '权限必须包含父节点',\n  10006: '非法操作：该节点仅支持目录类型父节点',\n  10007: '非法操作：节点类型无法直接转换',\n  10008: '该角色存在关联用户，请先删除关联用户',\n  10009: '该部门存在关联用户，请先删除关联用户',\n  10010: '该部门存在关联角色，请先删除关联角色',\n  10015: '该部门存在子部门，请先删除子部门',\n  10011: '旧密码与原密码不一致',\n  10012: '如想下线自身可右上角退出',\n  10013: '不允许下线该用户',\n  10014: '父级菜单不存在',\n  10016: '系统内置功能不允许操作',\n  10017: '用户不存在',\n  10018: '无法查找当前用户所属部门',\n  10019: '部门不存在',\n  10020: '任务不存在',\n  10021: '参数配置键值对已存在',\n  10101: '不安全的任务，确保执行的加入@Mission注解',\n  10102: '所执行的任务不存在',\n\n  // token相关\n  11001: '登录无效或无权限访问',\n  11002: '登录身份已过期',\n  11003: '无权限，请联系管理员申请权限',\n\n  // OSS相关\n  20001: '当前创建的文件或目录已存在',\n  20002: '无需操作',\n  20003: '已超出支持的最大处理数量',\n};\n"
  },
  {
    "path": "src/common/contants/param-config.contants.ts",
    "content": "export const SYS_USER_INITPASSWORD = 'sys_user_initPassword';\n"
  },
  {
    "path": "src/common/decorators/keep.decorator.ts",
    "content": "import { SetMetadata } from '@nestjs/common';\nimport { TRANSFORM_KEEP_KEY_METADATA } from '../contants/decorator.contants';\n\n/**\n * 不转化成JSON结构，保留原有返回\n */\nexport const Keep = () => SetMetadata(TRANSFORM_KEEP_KEY_METADATA, true);\n"
  },
  {
    "path": "src/common/dto/page.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport { IsInt, Min } from 'class-validator';\n\nexport class PageOptionsDto {\n  @ApiProperty({\n    description: '当前页包含数量',\n    required: false,\n    default: 10,\n  })\n  @Type(() => Number)\n  @IsInt()\n  @Min(1)\n  readonly limit: number = 10;\n\n  @ApiProperty({\n    description: '当前页包含数量',\n    required: false,\n    default: 1,\n  })\n  @Type(() => Number)\n  @IsInt()\n  @Min(1)\n  readonly page: number = 1;\n}\n"
  },
  {
    "path": "src/common/exceptions/api.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport { ErrorCodeMap } from '../contants/error-code.contants';\n\n/**\n * Api业务异常均抛出该异常\n */\nexport class ApiException extends HttpException {\n  /**\n   * 业务类型错误代码，非Http code\n   */\n  private errorCode: number;\n\n  constructor(errorCode: number) {\n    super(ErrorCodeMap[errorCode], 200);\n    this.errorCode = errorCode;\n  }\n\n  getErrorCode(): number {\n    return this.errorCode;\n  }\n}\n"
  },
  {
    "path": "src/common/exceptions/socket.exception.ts",
    "content": "import { WsException } from '@nestjs/websockets';\nimport { ErrorCodeMap } from '../contants/error-code.contants';\n\nexport class SocketException extends WsException {\n  private errorCode: number;\n\n  constructor(errorCode: number) {\n    super(ErrorCodeMap[errorCode]);\n    this.errorCode = errorCode;\n  }\n\n  getErrorCode(): number {\n    return this.errorCode;\n  }\n}\n"
  },
  {
    "path": "src/common/filters/api-exception.filter.ts",
    "content": "import {\n  ArgumentsHost,\n  Catch,\n  ExceptionFilter,\n  HttpException,\n  HttpStatus,\n} from '@nestjs/common';\nimport { FastifyReply } from 'fastify';\nimport { isDev } from 'src/config/env';\nimport { ApiException } from '../exceptions/api.exception';\nimport { ResOp } from '../class/res.class';\nimport { LoggerService } from 'src/shared/logger/logger.service';\n\n/**\n * 异常接管，统一异常返回数据\n */\n@Catch()\nexport class ApiExceptionFilter implements ExceptionFilter {\n  constructor(private logger: LoggerService) {}\n\n  catch(exception: unknown, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse<FastifyReply>();\n\n    // check api exection\n    const status =\n      exception instanceof HttpException\n        ? exception.getStatus()\n        : HttpStatus.INTERNAL_SERVER_ERROR;\n    // set json response\n    response.header('Content-Type', 'application/json; charset=utf-8');\n    // prod env will not return internal error message\n    const code =\n      exception instanceof ApiException\n        ? (exception as ApiException).getErrorCode()\n        : status;\n    let message = '服务器异常，请稍后再试';\n    // 开发模式下提示500类型错误，生产模式下屏蔽500内部错误提示\n    if (isDev() || status < 500) {\n      message =\n        exception instanceof HttpException ? exception.message : `${exception}`;\n    }\n    // 记录 500 日志\n    if (status >= 500) {\n      this.logger.error(exception, ApiExceptionFilter.name);\n    }\n    const result = new ResOp(code, null, message);\n    response.status(status).send(result);\n  }\n}\n"
  },
  {
    "path": "src/common/interceptors/api-transform.interceptor.ts",
    "content": "import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { Observable } from 'rxjs';\nimport { FastifyReply } from 'fastify';\nimport { map } from 'rxjs/operators';\nimport { TRANSFORM_KEEP_KEY_METADATA } from '../contants/decorator.contants';\nimport { ResOp } from '../class/res.class';\n\n/**\n * 统一处理返回接口结果，如果不需要则添加@Keep装饰器\n */\nexport class ApiTransformInterceptor implements NestInterceptor {\n  constructor(private readonly reflector: Reflector) {}\n  intercept(\n    context: ExecutionContext,\n    next: CallHandler<any>,\n  ): Observable<any> {\n    return next.handle().pipe(\n      map((data) => {\n        const keep = this.reflector.get<boolean>(\n          TRANSFORM_KEEP_KEY_METADATA,\n          context.getHandler(),\n        );\n        if (keep) {\n          return data;\n        } else {\n          const response = context.switchToHttp().getResponse<FastifyReply>();\n          response.header('Content-Type', 'application/json; charset=utf-8');\n          return new ResOp(200, data);\n        }\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "src/config/config.default.ts",
    "content": "import { defineConfig } from './defineConfig';\n\n/**\n * 项目通用默认配置，但优先级最低\n */\nexport default defineConfig({\n  rootRoleId: parseInt(process.env.ROOT_ROLE_ID || '1'),\n});\n"
  },
  {
    "path": "src/config/config.production.ts",
    "content": "import * as qiniu from 'qiniu';\nimport { defineConfig } from './defineConfig';\n\nconst parseZone = (zone: string) => {\n  switch (zone) {\n    case 'Zone_as0':\n      return qiniu.zone.Zone_as0;\n    case 'Zone_na0':\n      return qiniu.zone.Zone_na0;\n    case 'Zone_z0':\n      return qiniu.zone.Zone_z0;\n    case 'Zone_z1':\n      return qiniu.zone.Zone_z1;\n    case 'Zone_z2':\n      return qiniu.zone.Zone_z2;\n  }\n};\n\nexport default defineConfig({\n  jwt: {\n    secret: process.env.JWT_SECRET || '123456',\n  },\n  // typeorm config\n  database: {\n    type: 'mysql',\n    host: process.env.MYSQL_HOST || '127.0.0.1',\n    port: process.env.MYSQL_PORT || 3306,\n    username: process.env.MYSQL_USERNAME || 'root',\n    password: process.env.MYSQL_PASSWORD || '123456',\n    database: process.env.MYSQL_DATABASE || 'sf-admin',\n    synchronize: false,\n    logging: ['error'],\n  },\n  // redis cache config\n  redis: {\n    host: process.env.REDIS_HOST || '127.0.0.1', // default value\n    port: parseInt(process.env.REDIS_PORT) || 6379, // default value\n    password: process.env.REDIS_PASSWORD || '123456',\n    db: 0,\n  },\n  // qiniu config\n  qiniu: {\n    accessKey: process.env.QINIU_ACCESSKEY,\n    secretKey: process.env.QINIU_SECRETKEY,\n    domain: process.env.QINIU_DOMAIN,\n    bucket: process.env.QINIU_BUCKET,\n    zone: parseZone(process.env.QINIU_ZONE || 'Zone_z2'),\n    access: (process.env.QINIU_ACCESS_TYPE as any) || 'public',\n  },\n  // logger config\n  logger: {\n    timestamp: false,\n    dir: process.env.LOGGER_DIR,\n    maxFileSize: process.env.LOGGER_MAX_SIZE,\n    maxFiles: process.env.LOGGER_MAX_FILES,\n    errorLogName: process.env.LOGGER_ERROR_FILENAME,\n    appLogName: process.env.LOGGER_APP_FILENAME,\n  },\n  // swagger\n  swagger: {\n    enable: process.env.SWAGGER_ENABLE === 'true',\n    path: process.env.SWAGGER_PATH,\n    title: process.env.SWAGGER_TITLE,\n    desc: process.env.SWAGGER_DESC,\n    version: process.env.SWAGGER_VERSION,\n  },\n});\n"
  },
  {
    "path": "src/config/configuration.ts",
    "content": "import { merge } from 'lodash';\nimport DefaultConfig from './config.default';\nimport { IConfig } from './defineConfig';\n\n/**\n * 根据环境变量判断使用配置\n */\nexport default () => {\n  let envConfig: IConfig = {};\n  try {\n    // eslint-disable-next-line @typescript-eslint/no-var-requires\n    envConfig = require(`./config.${process.env.NODE_ENV}`).default;\n  } catch (e) {\n    // 无效配置则自动忽略\n  }\n  // 合并配置\n  return merge(DefaultConfig, envConfig);\n};\n"
  },
  {
    "path": "src/config/defineConfig.ts",
    "content": "import { conf } from 'qiniu';\nimport { LoggerModuleOptions as LoggerConfigOptions } from 'src/shared/logger/logger.interface';\nimport { LoggerOptions } from 'typeorm';\n\n/**\n * 用于智能提示\n */\nexport function defineConfig(config: IConfig): IConfig {\n  return config;\n}\n\n/**\n * sf-admin 配置\n */\nexport interface IConfig {\n  /**\n   * 管理员角色ID，一旦分配，该角色下分配的管理员都为超级管理员\n   */\n  rootRoleId?: number;\n  /**\n   * 用户鉴权Token密钥\n   */\n  jwt?: JwtConfigOptions;\n  /**\n   * Mysql数据库配置\n   */\n  database?: DataBaseConfigOptions;\n  /**\n   * Redis配置\n   */\n  redis?: RedisConfigOptions;\n  /**\n   * 七牛云配置\n   */\n  qiniu?: QiniuConfigOptions;\n  /**\n   * 应用级别日志配置\n   */\n  logger?: LoggerConfigOptions;\n  /**\n   * Swagger文档配置\n   */\n  swagger?: SwaggerConfigOptions;\n}\n\n//--------- config interface ------------\n\nexport interface JwtConfigOptions {\n  secret: string;\n}\n\nexport interface QiniuConfigOptions {\n  accessKey?: string;\n  secretKey?: string;\n  bucket?: string;\n  zone?: conf.Zone;\n  domain?: string;\n  access?: string;\n}\n\nexport interface RedisConfigOptions {\n  host?: string;\n  port?: number | string;\n  password?: string;\n  db?: number;\n}\n\nexport interface DataBaseConfigOptions {\n  type?: string;\n  host?: string;\n  port?: number | string;\n  username?: string;\n  password?: string;\n  database?: string;\n  synchronize?: boolean;\n  logging?: LoggerOptions;\n}\n\nexport interface SwaggerConfigOptions {\n  enable?: boolean;\n  path?: string;\n  title?: string;\n  desc?: string;\n  version?: string;\n}\n"
  },
  {
    "path": "src/config/env.ts",
    "content": "/**\n * check dev env\n * @returns boolean true is dev\n */\nexport function isDev(): boolean {\n  return process.env.NODE_ENV === 'development';\n}\n"
  },
  {
    "path": "src/entities/admin/sys-config.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_config' })\nexport default class SysConfig extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ type: 'varchar', length: 50, unique: true })\n  @ApiProperty()\n  key: string;\n\n  @Column({ type: 'varchar', length: 50 })\n  @ApiProperty()\n  name: string;\n\n  @Column({ type: 'varchar', nullable: true })\n  @ApiProperty()\n  value: string;\n\n  @Column({ type: 'varchar', nullable: true })\n  @ApiProperty()\n  remark: string;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-department.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_department' })\nexport default class SysDepartment extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ name: 'parent_id', nullable: true })\n  @ApiProperty()\n  parentId: number;\n\n  @Column()\n  @ApiProperty()\n  name: string;\n\n  @Column({ name: 'order_num', type: 'int', nullable: true, default: 0 })\n  @ApiProperty()\n  orderNum: number;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-login-log.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_login_log' })\nexport default class SysLoginLog extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ nullable: true, name: 'user_id' })\n  @ApiProperty()\n  userId: number;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  ip: string;\n\n  @Column({ type: 'datetime', nullable: true })\n  @ApiProperty()\n  time: Date;\n\n  @Column({ length: 500, nullable: true })\n  @ApiProperty()\n  ua: string;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-menu.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_menu' })\nexport default class SysMenu extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ name: 'parent_id', nullable: true })\n  @ApiProperty()\n  parentId: number;\n\n  @Column()\n  @ApiProperty()\n  name: string;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  router: string;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  perms: string;\n\n  @Column({ type: 'tinyint', default: 0 })\n  @ApiProperty()\n  type: number;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  icon: string;\n\n  @Column({ name: 'order_num', type: 'int', default: 0, nullable: true })\n  @ApiProperty()\n  orderNum: number;\n\n  @Column({ name: 'view_path', nullable: true })\n  @ApiProperty()\n  viewPath: string;\n\n  @Column({ type: 'boolean', nullable: true, default: true })\n  @ApiProperty()\n  keepalive: boolean;\n\n  @Column({ name: 'is_show', type: 'boolean', nullable: true, default: true })\n  @ApiProperty()\n  isShow: boolean;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-role-department.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_role_department' })\nexport default class SysRoleDepartment extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ name: 'role_id' })\n  @ApiProperty()\n  roleId: number;\n\n  @Column({ name: 'department_id' })\n  @ApiProperty()\n  departmentId: number;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-role-menu.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_role_menu' })\nexport default class SysRoleMenu extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ name: 'role_id' })\n  @ApiProperty()\n  roleId: number;\n\n  @Column({ name: 'menu_id' })\n  @ApiProperty()\n  menuId: number;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-role.entity.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_role' })\nexport default class SysRole extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ name: 'user_id' })\n  @ApiProperty()\n  userId: string;\n\n  @Column({ unique: true })\n  @ApiProperty()\n  name: string;\n\n  @Column({ length: 50, unique: true })\n  @ApiProperty()\n  label: string;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  remark: string;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-task-log.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_task_log' })\nexport default class SysTaskLog extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ name: 'task_id' })\n  @ApiProperty()\n  taskId: number;\n\n  @Column({ type: 'tinyint', default: 0 })\n  @ApiProperty()\n  status: number;\n\n  @Column({ type: 'text', nullable: true })\n  @ApiProperty()\n  detail: string;\n\n  @Column({ type: 'int', nullable: true, name: 'consume_time', default: 0 })\n  @ApiProperty()\n  consumeTime: number;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-task.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_task' })\nexport default class SysTask extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ type: 'varchar', length: 50, unique: true })\n  @ApiProperty()\n  name: string;\n\n  @Column()\n  @ApiProperty()\n  service: string;\n\n  @Column({ type: 'tinyint', default: 0 })\n  @ApiProperty()\n  type: number;\n\n  @Column({ type: 'tinyint', default: 1 })\n  @ApiProperty()\n  status: number;\n\n  @Column({ name: 'start_time', type: 'datetime', nullable: true })\n  @ApiProperty()\n  startTime: Date;\n\n  @Column({ name: 'end_time', type: 'datetime', nullable: true })\n  @ApiProperty()\n  endTime: Date;\n\n  @Column({ type: 'int', nullable: true, default: 0 })\n  @ApiProperty()\n  limit: number;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  cron: string;\n\n  @Column({ type: 'int', nullable: true })\n  @ApiProperty()\n  every: number;\n\n  @Column({ type: 'text', nullable: true })\n  @ApiProperty()\n  data: string;\n\n  @Column({ name: 'job_opts', type: 'text', nullable: true })\n  @ApiProperty()\n  jobOpts: string;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  remark: string;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-user-role.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_user_role' })\nexport default class SysUserRole extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ name: 'user_id' })\n  @ApiProperty()\n  userId: number;\n\n  @Column({ name: 'role_id' })\n  @ApiProperty()\n  roleId: number;\n}\n"
  },
  {
    "path": "src/entities/admin/sys-user.entity.ts",
    "content": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { BaseEntity } from '../base.entity';\n\n@Entity({ name: 'sys_user' })\nexport default class SysUser extends BaseEntity {\n  @PrimaryGeneratedColumn()\n  @ApiProperty()\n  id: number;\n\n  @Column({ name: 'department_id' })\n  @ApiProperty()\n  departmentId: number;\n\n  @Column()\n  @ApiProperty()\n  name: string;\n\n  @Column({ unique: true })\n  @ApiProperty()\n  username: string;\n\n  @Column()\n  @ApiProperty()\n  password: string;\n\n  @Column({ length: 32 })\n  @ApiProperty()\n  psalt: string;\n\n  @Column({ name: 'nick_name', nullable: true })\n  @ApiProperty()\n  nickName: string;\n\n  @Column({ name: 'head_img', nullable: true })\n  @ApiProperty()\n  headImg: string;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  email: string;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  phone: string;\n\n  @Column({ nullable: true })\n  @ApiProperty()\n  remark: string;\n\n  @Column({ type: 'tinyint', nullable: true, default: 1 })\n  @ApiProperty()\n  status: number;\n}\n"
  },
  {
    "path": "src/entities/base.entity.ts",
    "content": "import { CreateDateColumn, UpdateDateColumn } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport abstract class BaseEntity {\n  @CreateDateColumn({ name: 'created_at' })\n  @ApiProperty()\n  createdAt: Date;\n\n  @UpdateDateColumn({ name: 'updated_at' })\n  @ApiProperty()\n  updatedAt: Date;\n}\n"
  },
  {
    "path": "src/main.ts",
    "content": "import {\n  HttpStatus,\n  UnprocessableEntityException,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { NestFactory, Reflector } from '@nestjs/core';\nimport {\n  FastifyAdapter,\n  NestFastifyApplication,\n} from '@nestjs/platform-fastify';\nimport { IoAdapter } from '@nestjs/platform-socket.io';\nimport { ValidationError } from 'class-validator';\nimport { flatten } from 'lodash';\nimport { AppModule } from './app.module';\nimport { ApiExceptionFilter } from './common/filters/api-exception.filter';\nimport { ApiTransformInterceptor } from './common/interceptors/api-transform.interceptor';\nimport { setupSwagger } from './setup-swagger';\nimport { LoggerService } from './shared/logger/logger.service';\n\nasync function bootstrap() {\n  const app = await NestFactory.create<NestFastifyApplication>(\n    AppModule,\n    new FastifyAdapter(),\n    {\n      bufferLogs: true,\n    },\n  );\n  // custom logger\n  app.useLogger(app.get(LoggerService));\n  // validate\n  app.useGlobalPipes(\n    new ValidationPipe({\n      transform: true,\n      whitelist: true,\n      forbidNonWhitelisted: true,\n      errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,\n      exceptionFactory: (errors: ValidationError[]) => {\n        return new UnprocessableEntityException(\n          flatten(\n            errors\n              .filter((item) => !!item.constraints)\n              .map((item) => Object.values(item.constraints)),\n          ).join('; '),\n        );\n      },\n    }),\n  );\n  // execption\n  app.useGlobalFilters(new ApiExceptionFilter(app.get(LoggerService)));\n  // api interceptor\n  app.useGlobalInterceptors(new ApiTransformInterceptor(new Reflector()));\n  // websocket\n  app.useWebSocketAdapter(new IoAdapter());\n  // swagger\n  setupSwagger(app);\n  // start\n  await app.listen(process.env.PORT || 7001, '0.0.0.0');\n}\n\nbootstrap();\n"
  },
  {
    "path": "src/mission/README.md",
    "content": "### 任务注册\n\n在jobs下定义任务，并在`mission.module.ts`中的providers中注册即可。\n\n### 添加任务\n\n在后台页面中的定时任务页面中进行添加，主要识别为**服务路径**的定义：`Job类名.方法名`，填写任务参数则会在调用方法时自动传递该参数，参数会被`JSON.parse`"
  },
  {
    "path": "src/mission/jobs/http-request.job.ts",
    "content": "import { HttpService } from '@nestjs/axios';\nimport { Injectable } from '@nestjs/common';\nimport { LoggerService } from 'src/shared/logger/logger.service';\nimport { Mission } from '../mission.decorator';\n\n/**\n * Api接口请求类型任务\n */\n@Injectable()\n@Mission()\nexport class HttpRequestJob {\n  constructor(\n    private readonly httpService: HttpService,\n    private readonly logger: LoggerService,\n  ) {}\n\n  /**\n   * 发起请求\n   * @param config {AxiosRequestConfig}\n   */\n  async handle(config: unknown): Promise<void> {\n    if (config) {\n      const result = await this.httpService.axiosRef.request(config);\n      this.logger.log(result, HttpRequestJob.name);\n    } else {\n      throw new Error('Http request job param is empty');\n    }\n  }\n}\n"
  },
  {
    "path": "src/mission/jobs/sys-log-clear.job.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { SysLogService } from 'src/modules/admin/system/log/log.service';\nimport { Mission } from '../mission.decorator';\n\n/**\n * 管理后台日志清理任务\n */\n@Injectable()\n@Mission()\nexport class SysLogClearJob {\n  constructor(private sysLogService: SysLogService) {}\n\n  async clearLoginLog(): Promise<void> {\n    await this.sysLogService.clearLoginLog();\n  }\n\n  async clearTaskLog(): Promise<void> {\n    await this.sysLogService.clearTaskLog();\n  }\n}\n"
  },
  {
    "path": "src/mission/mission.decorator.ts",
    "content": "import { SetMetadata } from '@nestjs/common';\nimport { MISSION_KEY_METADATA } from '../common/contants/decorator.contants';\n\n/**\n * 定时任务标记，没有该任务标记的任务不会被执行，保证全局获取下的模块被安全执行\n */\nexport const Mission = () => SetMetadata(MISSION_KEY_METADATA, true);\n"
  },
  {
    "path": "src/mission/mission.module.ts",
    "content": "import { DynamicModule, ExistingProvider, Module } from '@nestjs/common';\nimport { AdminModule } from 'src/modules/admin/admin.module';\nimport { SysLogService } from 'src/modules/admin/system/log/log.service';\nimport { HttpRequestJob } from './jobs/http-request.job';\nimport { SysLogClearJob } from './jobs/sys-log-clear.job';\n\nconst providers = [SysLogClearJob, HttpRequestJob];\n\n/**\n * auto create alias\n * {\n *    provide: 'SysLogClearMissionService',\n *    useExisting: SysLogClearMissionService,\n *  }\n */\nfunction createAliasProviders(): ExistingProvider[] {\n  const aliasProviders: ExistingProvider[] = [];\n  for (const p of providers) {\n    aliasProviders.push({\n      provide: p.name,\n      useExisting: p,\n    });\n  }\n  return aliasProviders;\n}\n\n/**\n * 所有需要执行的定时任务都需要在这里注册\n */\n@Module({})\nexport class MissionModule {\n  static forRoot(): DynamicModule {\n    // 使用Alias定义别名，使得可以通过字符串类型获取定义的Service，否则无法获取\n    const aliasProviders = createAliasProviders();\n    return {\n      global: true,\n      module: MissionModule,\n      imports: [AdminModule],\n      providers: [...providers, ...aliasProviders, SysLogService],\n      exports: aliasProviders,\n    };\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/account/account.controller.ts",
    "content": "import { Body, Controller, Get, Post } from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { ADMIN_PREFIX } from 'src/modules/admin/admin.constants';\nimport { IAdminUser } from '../admin.interface';\nimport { AdminUser } from '../core/decorators/admin-user.decorator';\nimport { PermissionOptional } from '../core/decorators/permission-optional.decorator';\nimport { PermMenuInfo } from '../login/login.class';\nimport { LoginService } from '../login/login.service';\nimport { AccountInfo } from '../system/user/user.class';\nimport { UpdatePasswordDto } from '../system/user/user.dto';\nimport { SysUserService } from '../system/user/user.service';\nimport { UpdatePersonInfoDto } from './account.dto';\n\n@ApiTags('账户模块')\n@ApiSecurity(ADMIN_PREFIX)\n@Controller()\nexport class AccountController {\n  constructor(\n    private userService: SysUserService,\n    private loginService: LoginService,\n  ) {}\n\n  @ApiOperation({ summary: '获取管理员资料' })\n  @ApiOkResponse({ type: AccountInfo })\n  @PermissionOptional()\n  @Get('info')\n  async info(@AdminUser() user: IAdminUser): Promise<AccountInfo> {\n    return await this.userService.getAccountInfo(user.uid);\n  }\n\n  @ApiOperation({ summary: '更改管理员资料' })\n  @PermissionOptional()\n  @Post('update')\n  async update(\n    @Body() dto: UpdatePersonInfoDto,\n    @AdminUser() user: IAdminUser,\n  ): Promise<void> {\n    await this.userService.updatePersonInfo(user.uid, dto);\n  }\n\n  @ApiOperation({ summary: '更改管理员密码' })\n  @PermissionOptional()\n  @Post('password')\n  async password(\n    @Body() dto: UpdatePasswordDto,\n    @AdminUser() user: IAdminUser,\n  ): Promise<void> {\n    await this.userService.updatePassword(user.uid, dto);\n  }\n\n  @ApiOperation({ summary: '管理员登出' })\n  @PermissionOptional()\n  @Post('logout')\n  async logout(@AdminUser() user: IAdminUser): Promise<void> {\n    await this.loginService.clearLoginStatus(user.uid);\n  }\n\n  @ApiOperation({ summary: '获取菜单列表及权限列表' })\n  @ApiOkResponse({ type: PermMenuInfo })\n  @PermissionOptional()\n  @Get('permmenu')\n  async permmenu(@AdminUser() user: IAdminUser): Promise<PermMenuInfo> {\n    return await this.loginService.getPermMenu(user.uid);\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/account/account.dto.ts",
    "content": "import { Optional } from '@nestjs/common';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsString } from 'class-validator';\n\nexport class UpdatePersonInfoDto {\n  @ApiProperty({ description: '管理员昵称', required: false })\n  @IsString()\n  @Optional()\n  nickName: string;\n\n  @ApiProperty({ description: '邮箱', required: false })\n  @IsString()\n  @Optional()\n  email: string;\n\n  @ApiProperty({ description: '手机', required: false })\n  @IsString()\n  @Optional()\n  phone: string;\n\n  @ApiProperty({ description: '备注', required: false })\n  @IsString()\n  @Optional()\n  remark: string;\n}\n"
  },
  {
    "path": "src/modules/admin/account/account.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { LoginModule } from '../login/login.module';\nimport { SystemModule } from '../system/system.module';\nimport { AccountController } from './account.controller';\n\n@Module({\n  imports: [SystemModule, LoginModule],\n  controllers: [AccountController],\n})\nexport class AccountModule {}\n"
  },
  {
    "path": "src/modules/admin/admin.constants.ts",
    "content": "export const ADMIN_USER = 'adminUser';\nexport const AUTHORIZE_KEY_METADATA = 'admin_module:authorize';\nexport const PERMISSION_OPTIONAL_KEY_METADATA =\n  'admin_module:permission_optional';\nexport const LOG_DISABLED_KEY_METADATA = 'admin_module:log_disabled';\n\nexport const ROOT_ROLE_ID = 'admin_module:root_role_id';\nexport const QINIU_CONFIG = 'admin_module:qiniu_config';\n\nexport const SYS_TASK_QUEUE_NAME = 'admin_module:sys-task';\nexport const SYS_TASK_QUEUE_PREFIX = 'admin:sys:task';\n\nexport const FORBIDDEN_OP_MENU_ID_INDEX = 91;\n\nexport const ADMIN_PREFIX = 'admin';\nexport const QINIU_API = 'http://api.qiniu.com';\n\nexport const NETDISK_TASK_PREFIX = 'admin:netdisk:';\n\n// 目录分隔符\nexport const NETDISK_DELIMITER = '/';\nexport const NETDISK_LIMIT = 100;\nexport const NETDISK_HANDLE_MAX_ITEM = 1000;\nexport const NETDISK_COPY_SUFFIX = '的副本';\n"
  },
  {
    "path": "src/modules/admin/admin.interface.ts",
    "content": "import { conf } from 'qiniu';\n\nexport interface IAdminUser {\n  uid: number;\n  pv: number;\n}\n\nexport type QINIU_ACCESS_CONTROL = 'private' | 'public';\n\nexport interface IQiniuConfig {\n  accessKey: string;\n  secretKey: string;\n  bucket: string;\n  zone: conf.Zone;\n  domain: string;\n  access: QINIU_ACCESS_CONTROL;\n}\n"
  },
  {
    "path": "src/modules/admin/admin.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { APP_GUARD, RouterModule } from '@nestjs/core';\nimport { AccountModule } from './account/account.module';\nimport { ADMIN_PREFIX } from './admin.constants';\nimport { AuthGuard } from './core/guards/auth.guard';\nimport { LoginModule } from './login/login.module';\nimport { NetdiskModule } from './netdisk/netdisk.module';\nimport { SystemModule } from './system/system.module';\n\n/**\n * Admin模块，所有API都需要加入/admin前缀\n */\n@Module({\n  imports: [\n    // register prefix\n    RouterModule.register([\n      {\n        path: ADMIN_PREFIX,\n        children: [\n          { path: 'netdisk', module: NetdiskModule },\n          { path: 'account', module: AccountModule },\n          { path: 'sys', module: SystemModule },\n        ],\n      },\n      // like this url /admin/captcha/img\n      {\n        path: ADMIN_PREFIX,\n        module: LoginModule,\n      },\n    ]),\n    // component module\n    LoginModule,\n    SystemModule,\n    AccountModule,\n    NetdiskModule,\n  ],\n  providers: [\n    {\n      provide: APP_GUARD,\n      useClass: AuthGuard,\n    },\n  ],\n  exports: [SystemModule],\n})\nexport class AdminModule {}\n"
  },
  {
    "path": "src/modules/admin/core/decorators/admin-user.decorator.ts",
    "content": "import { createParamDecorator, ExecutionContext } from '@nestjs/common';\nimport { ADMIN_USER } from '../../admin.constants';\n\nexport const AdminUser = createParamDecorator(\n  (data: string, ctx: ExecutionContext) => {\n    const request = ctx.switchToHttp().getRequest();\n    // auth guard will mount this\n    const user = request[ADMIN_USER];\n\n    return data ? user?.[data] : user;\n  },\n);\n"
  },
  {
    "path": "src/modules/admin/core/decorators/authorize.decorator.ts",
    "content": "import { SetMetadata } from '@nestjs/common';\nimport { AUTHORIZE_KEY_METADATA } from '../../admin.constants';\n\n/**\n * 开放授权Api，使用该注解则无需校验Token及权限\n */\nexport const Authorize = () => SetMetadata(AUTHORIZE_KEY_METADATA, true);\n"
  },
  {
    "path": "src/modules/admin/core/decorators/log-disabled.decorator.ts",
    "content": "import { SetMetadata } from '@nestjs/common';\nimport { LOG_DISABLED_KEY_METADATA } from '../../admin.constants';\n\n/**\n * 日志记录禁用\n */\nexport const LogDisabled = () => SetMetadata(LOG_DISABLED_KEY_METADATA, true);\n"
  },
  {
    "path": "src/modules/admin/core/decorators/permission-optional.decorator.ts",
    "content": "import { SetMetadata } from '@nestjs/common';\nimport { PERMISSION_OPTIONAL_KEY_METADATA } from '../../admin.constants';\n\n/**\n * 使用该注解可开放当前Api权限，无需权限访问，但是仍然需要校验身份Token\n */\nexport const PermissionOptional = () =>\n  SetMetadata(PERMISSION_OPTIONAL_KEY_METADATA, true);\n"
  },
  {
    "path": "src/modules/admin/core/guards/auth.guard.ts",
    "content": "import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { FastifyRequest } from 'fastify';\nimport { isEmpty } from 'lodash';\nimport { JwtService } from '@nestjs/jwt';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport {\n  ADMIN_PREFIX,\n  ADMIN_USER,\n  PERMISSION_OPTIONAL_KEY_METADATA,\n  AUTHORIZE_KEY_METADATA,\n} from 'src/modules/admin/admin.constants';\nimport { LoginService } from 'src/modules/admin/login/login.service';\n\n/**\n * admin perm check guard\n */\n@Injectable()\nexport class AuthGuard implements CanActivate {\n  constructor(\n    private reflector: Reflector,\n    private jwtService: JwtService,\n    private loginService: LoginService,\n  ) {}\n\n  async canActivate(context: ExecutionContext): Promise<boolean> {\n    // 检测是否是开放类型的，例如获取验证码类型的接口不需要校验，可以加入@Authorize可自动放过\n    const authorize = this.reflector.get<boolean>(\n      AUTHORIZE_KEY_METADATA,\n      context.getHandler(),\n    );\n    if (authorize) {\n      return true;\n    }\n    const request = context.switchToHttp().getRequest<FastifyRequest>();\n    const url = request.url;\n    const path = url.split('?')[0];\n    const token = request.headers['authorization'] as string;\n    if (isEmpty(token)) {\n      throw new ApiException(11001);\n    }\n    try {\n      // 挂载对象到当前请求上\n      request[ADMIN_USER] = this.jwtService.verify(token);\n    } catch (e) {\n      // 无法通过token校验\n      throw new ApiException(11001);\n    }\n    if (isEmpty(request[ADMIN_USER])) {\n      throw new ApiException(11001);\n    }\n    const pv = await this.loginService.getRedisPasswordVersionById(\n      request[ADMIN_USER].uid,\n    );\n    if (pv !== `${request[ADMIN_USER].pv}`) {\n      // 密码版本不一致，登录期间已更改过密码\n      throw new ApiException(11002);\n    }\n    const redisToken = await this.loginService.getRedisTokenById(\n      request[ADMIN_USER].uid,\n    );\n    if (token !== redisToken) {\n      // 与redis保存不一致\n      throw new ApiException(11002);\n    }\n    // 注册该注解，Api则放行检测\n    const notNeedPerm = this.reflector.get<boolean>(\n      PERMISSION_OPTIONAL_KEY_METADATA,\n      context.getHandler(),\n    );\n    // Token校验身份通过，判断是否需要权限的url，不需要权限则pass\n    if (notNeedPerm) {\n      return true;\n    }\n    const perms: string = await this.loginService.getRedisPermsById(\n      request[ADMIN_USER].uid,\n    );\n    // 安全判空\n    if (isEmpty(perms)) {\n      throw new ApiException(11001);\n    }\n    // 将sys:admin:user等转换成sys/admin/user\n    const permArray: string[] = (JSON.parse(perms) as string[]).map((e) => {\n      return e.replace(/:/g, '/');\n    });\n    // 遍历权限是否包含该url，不包含则无访问权限\n    if (!permArray.includes(path.replace(`/${ADMIN_PREFIX}/`, ''))) {\n      throw new ApiException(11003);\n    }\n    // pass\n    return true;\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/core/provider/qiniu.provider.ts",
    "content": "import { FactoryProvider } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { QINIU_CONFIG } from 'src/modules/admin/admin.constants';\nimport { IQiniuConfig } from '../../admin.interface';\n\n/**\n * 提供使用 @Inject(QINIU_CONFIG) 直接获取七牛配置\n */\nexport function qiniuProvider(): FactoryProvider {\n  return {\n    provide: QINIU_CONFIG,\n    useFactory: (configService: ConfigService): IQiniuConfig => ({\n      accessKey: configService.get('qiniu.accessKey'),\n      secretKey: configService.get('qiniu.secretKey'),\n      domain: configService.get('qiniu.domain'),\n      bucket: configService.get('qiniu.bucket'),\n      zone: configService.get('qiniu.zone'),\n      access: configService.get('qiniu.access'),\n    }),\n    inject: [ConfigService],\n  };\n}\n"
  },
  {
    "path": "src/modules/admin/core/provider/root-role-id.provider.ts",
    "content": "import { FactoryProvider } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { ROOT_ROLE_ID } from 'src/modules/admin/admin.constants';\n\n/**\n * 提供使用 @Inject(ROOT_ROLE_ID) 直接获取RootRoleId\n */\nexport function rootRoleIdProvider(): FactoryProvider {\n  return {\n    provide: ROOT_ROLE_ID,\n    useFactory: (configService: ConfigService) => {\n      return configService.get<number>('rootRoleId', 1);\n    },\n    inject: [ConfigService],\n  };\n}\n"
  },
  {
    "path": "src/modules/admin/login/login.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport SysMenu from 'src/entities/admin/sys-menu.entity';\n\nexport class ImageCaptcha {\n  @ApiProperty({\n    description: 'base64格式的svg图片',\n  })\n  img: string;\n\n  @ApiProperty({\n    description: '验证码对应的唯一ID',\n  })\n  id: string;\n}\n\nexport class LoginToken {\n  @ApiProperty({ description: 'JWT身份Token' })\n  token: string;\n}\n\nexport class PermMenuInfo {\n  @ApiProperty({ description: '菜单列表', type: [SysMenu] })\n  menus: SysMenu[];\n\n  @ApiProperty({ description: '权限列表', type: [String] })\n  perms: string[];\n}\n"
  },
  {
    "path": "src/modules/admin/login/login.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Get,\n  Headers,\n  Post,\n  Query,\n  Req,\n} from '@nestjs/common';\nimport { FastifyRequest } from 'fastify';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { Authorize } from '../core/decorators/authorize.decorator';\nimport { ImageCaptchaDto, LoginInfoDto } from './login.dto';\nimport { ImageCaptcha, LoginToken } from './login.class';\nimport { LoginService } from './login.service';\nimport { LogDisabled } from '../core/decorators/log-disabled.decorator';\nimport { UtilService } from 'src/shared/services/util.service';\n\n@ApiTags('登录模块')\n@Controller()\nexport class LoginController {\n  constructor(private loginService: LoginService, private utils: UtilService) {}\n\n  @ApiOperation({\n    summary: '获取登录图片验证码',\n  })\n  @ApiOkResponse({ type: ImageCaptcha })\n  @Get('captcha/img')\n  @Authorize()\n  async captchaByImg(@Query() dto: ImageCaptchaDto): Promise<ImageCaptcha> {\n    return await this.loginService.createImageCaptcha(dto);\n  }\n\n  @ApiOperation({\n    summary: '管理员登录',\n  })\n  @ApiOkResponse({ type: LoginToken })\n  @Post('login')\n  @LogDisabled()\n  @Authorize()\n  async login(\n    @Body() dto: LoginInfoDto,\n    @Req() req: FastifyRequest,\n    @Headers('user-agent') ua: string,\n  ): Promise<LoginToken> {\n    await this.loginService.checkImgCaptcha(dto.captchaId, dto.verifyCode);\n    const token = await this.loginService.getLoginSign(\n      dto.username,\n      dto.password,\n      this.utils.getReqIP(req),\n      ua,\n    );\n    return { token };\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/login/login.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n  IsInt,\n  IsOptional,\n  IsString,\n  MaxLength,\n  MinLength,\n} from 'class-validator';\n\nexport class ImageCaptchaDto {\n  @ApiProperty({\n    required: false,\n    default: 100,\n    description: '验证码宽度',\n  })\n  @Type(() => Number)\n  @IsInt()\n  @IsOptional()\n  readonly width: number = 100;\n\n  @ApiProperty({\n    required: false,\n    default: 50,\n    description: '验证码宽度',\n  })\n  @Type(() => Number)\n  @IsInt()\n  @IsOptional()\n  readonly height: number = 50;\n}\n\nexport class LoginInfoDto {\n  @ApiProperty({ description: '管理员用户名' })\n  @IsString()\n  @MinLength(1)\n  username: string;\n\n  @ApiProperty({ description: '管理员密码' })\n  @IsString()\n  @MinLength(4)\n  password: string;\n\n  @ApiProperty({ description: '验证码标识' })\n  @IsString()\n  captchaId: string;\n\n  @ApiProperty({ description: '用户输入的验证码' })\n  @IsString()\n  @MinLength(4)\n  @MaxLength(4)\n  verifyCode: string;\n}\n"
  },
  {
    "path": "src/modules/admin/login/login.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { SystemModule } from '../system/system.module';\nimport { LoginController } from './login.controller';\nimport { LoginService } from './login.service';\n\n@Module({\n  imports: [SystemModule],\n  controllers: [LoginController],\n  providers: [LoginService],\n  exports: [LoginService],\n})\nexport class LoginModule {}\n"
  },
  {
    "path": "src/modules/admin/login/login.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport * as svgCaptcha from 'svg-captcha';\nimport { ImageCaptcha, PermMenuInfo } from './login.class';\nimport { isEmpty } from 'lodash';\nimport { ImageCaptchaDto } from './login.dto';\nimport { JwtService } from '@nestjs/jwt';\nimport { UtilService } from 'src/shared/services/util.service';\nimport { SysMenuService } from '../system/menu/menu.service';\nimport { SysUserService } from '../system/user/user.service';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport { SysLogService } from '../system/log/log.service';\nimport { RedisService } from 'src/shared/services/redis.service';\n\n@Injectable()\nexport class LoginService {\n  constructor(\n    private redisService: RedisService,\n    private menuService: SysMenuService,\n    private userService: SysUserService,\n    private logService: SysLogService,\n    private util: UtilService,\n    private jwtService: JwtService,\n  ) {}\n\n  /**\n   * 创建验证码并缓存加入redis缓存\n   * @param captcha 验证码长宽\n   * @returns svg & id obj\n   */\n  async createImageCaptcha(captcha: ImageCaptchaDto): Promise<ImageCaptcha> {\n    const svg = svgCaptcha.create({\n      size: 4,\n      color: true,\n      noise: 4,\n      width: isEmpty(captcha.width) ? 100 : captcha.width,\n      height: isEmpty(captcha.height) ? 50 : captcha.height,\n      charPreset: '1234567890',\n    });\n    const result = {\n      img: `data:image/svg+xml;base64,${Buffer.from(svg.data).toString(\n        'base64',\n      )}`,\n      id: this.util.generateUUID(), // this.utils.generateUUID()\n    };\n    // 5分钟过期时间\n    await this.redisService\n      .getRedis()\n      .set(`admin:captcha:img:${result.id}`, svg.text, 'EX', 60 * 5);\n    return result;\n  }\n\n  /**\n   * 校验验证码\n   */\n  async checkImgCaptcha(id: string, code: string): Promise<void> {\n    const result = await this.redisService\n      .getRedis()\n      .get(`admin:captcha:img:${id}`);\n    if (isEmpty(result) || code.toLowerCase() !== result.toLowerCase()) {\n      throw new ApiException(10002);\n    }\n    // 校验成功后移除验证码\n    await this.redisService.getRedis().del(`admin:captcha:img:${id}`);\n  }\n\n  /**\n   * 获取登录JWT\n   * 返回null则账号密码有误，不存在该用户\n   */\n  async getLoginSign(\n    username: string,\n    password: string,\n    ip: string,\n    ua: string,\n  ): Promise<string> {\n    const user = await this.userService.findUserByUserName(username);\n    if (isEmpty(user)) {\n      throw new ApiException(10003);\n    }\n    const comparePassword = this.util.md5(`${password}${user.psalt}`);\n    if (user.password !== comparePassword) {\n      throw new ApiException(10003);\n    }\n    const perms = await this.menuService.getPerms(user.id);\n    const jwtSign = this.jwtService.sign(\n      {\n        uid: parseInt(user.id.toString()),\n        pv: 1,\n      },\n      // {\n      //   expiresIn: '24h',\n      // },\n    );\n    await this.redisService\n      .getRedis()\n      .set(`admin:passwordVersion:${user.id}`, 1);\n    // Token设置过期时间 24小时\n    await this.redisService\n      .getRedis()\n      .set(`admin:token:${user.id}`, jwtSign, 'EX', 60 * 60 * 24);\n    await this.redisService\n      .getRedis()\n      .set(`admin:perms:${user.id}`, JSON.stringify(perms));\n    await this.logService.saveLoginLog(user.id, ip, ua);\n    return jwtSign;\n  }\n\n  /**\n   * 清除登录状态信息\n   */\n  async clearLoginStatus(uid: number): Promise<void> {\n    await this.userService.forbidden(uid);\n  }\n\n  /**\n   * 获取权限菜单\n   */\n  async getPermMenu(uid: number): Promise<PermMenuInfo> {\n    const menus = await this.menuService.getMenus(uid);\n    const perms = await this.menuService.getPerms(uid);\n    return { menus, perms };\n  }\n\n  async getRedisPasswordVersionById(id: number): Promise<string> {\n    return this.redisService.getRedis().get(`admin:passwordVersion:${id}`);\n  }\n\n  async getRedisTokenById(id: number): Promise<string> {\n    return this.redisService.getRedis().get(`admin:token:${id}`);\n  }\n\n  async getRedisPermsById(id: number): Promise<string> {\n    return this.redisService.getRedis().get(`admin:perms:${id}`);\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/netdisk/manager/manage.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport type FileType = 'file' | 'dir';\n\nexport class SFileInfo {\n  @ApiProperty({ description: '文件id' })\n  id: string;\n\n  @ApiProperty({ description: '文件类型', enum: ['file', 'dir'] })\n  type: FileType;\n\n  @ApiProperty({ description: '文件名称' })\n  name: string;\n\n  @ApiProperty({ description: '存入时间', type: Date })\n  putTime?: Date;\n\n  @ApiProperty({ description: '文件大小, byte单位' })\n  fsize?: string;\n\n  @ApiProperty({ description: '文件的mime-type' })\n  mimeType?: string;\n\n  @ApiProperty({ description: '所属目录' })\n  belongTo?: string;\n}\n\nexport class SFileList {\n  @ApiProperty({ description: '文件列表', type: [SFileInfo] })\n  list: SFileInfo[];\n\n  @ApiProperty({ description: '分页标志，空则代表加载完毕' })\n  marker?: string;\n}\n\nexport class UploadToken {\n  @ApiProperty({ description: '上传token' })\n  token: string;\n}\n\nexport class SFileInfoDetail {\n  @ApiProperty({ description: '文件大小，int64类型，单位为字节（Byte）' })\n  fsize: number;\n\n  @ApiProperty({ description: '文件HASH值' })\n  hash: string;\n\n  @ApiProperty({ description: '文件MIME类型，string类型' })\n  mimeType: string;\n\n  @ApiProperty({\n    description:\n      '文件存储类型，2 表示归档存储，1 表示低频存储，0表示普通存储。',\n  })\n  type: number;\n\n  @ApiProperty({ description: '文件上传时间', type: Date })\n  putTime: Date;\n\n  @ApiProperty({ description: '文件md5值' })\n  md5: string;\n\n  @ApiProperty({ description: '上传人' })\n  uploader: string;\n\n  @ApiProperty({ description: '文件备注' })\n  mark?: string;\n}\n"
  },
  {
    "path": "src/modules/admin/netdisk/manager/manage.controller.ts",
    "content": "import {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { NetDiskManageService } from './manage.service';\nimport { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n  DeleteDto,\n  FileInfoDto,\n  FileOpDto,\n  GetFileListDto,\n  MarkFileDto,\n  MKDirDto,\n  RenameDto,\n} from './manage.dto';\nimport { SFileInfoDetail, SFileList, UploadToken } from './manage.class';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport { AdminUser } from '../../core/decorators/admin-user.decorator';\nimport { IAdminUser } from '../../admin.interface';\nimport { ADMIN_PREFIX } from '../../admin.constants';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('网盘管理模块')\n@Controller('manage')\nexport class NetDiskManageController {\n  constructor(private manageService: NetDiskManageService) {}\n\n  @ApiOperation({ summary: '获取文件列表' })\n  @ApiOkResponse({ type: SFileList })\n  @Get('list')\n  async list(@Query() dto: GetFileListDto): Promise<SFileList> {\n    return await this.manageService.getFileList(dto.path, dto.marker, dto.key);\n  }\n\n  @ApiOperation({ summary: '创建文件夹，支持多级' })\n  @Post('mkdir')\n  async mkdir(@Body() dto: MKDirDto): Promise<void> {\n    const result = await this.manageService.checkFileExist(\n      `${dto.path}${dto.dirName}/`,\n    );\n    if (result) {\n      throw new ApiException(20001);\n    }\n    await this.manageService.createDir(`${dto.path}${dto.dirName}`);\n  }\n\n  @ApiOperation({ summary: '获取上传Token，无Token前端无法上传' })\n  @ApiOkResponse({ type: UploadToken })\n  @Get('token')\n  async token(@AdminUser() user: IAdminUser): Promise<UploadToken> {\n    return {\n      token: this.manageService.createUploadToken(`${user.uid}`),\n    };\n  }\n\n  @ApiOperation({ summary: '获取文件详细信息' })\n  @ApiOkResponse({ type: SFileInfoDetail })\n  @Post('info')\n  async info(@Body() dto: FileInfoDto): Promise<SFileInfoDetail> {\n    return await this.manageService.getFileInfo(dto.name, dto.path);\n  }\n\n  @ApiOperation({ summary: '添加文件备注' })\n  @Post('mark')\n  async mark(@Body() dto: MarkFileDto): Promise<void> {\n    await this.manageService.changeFileHeaders(dto.name, dto.path, {\n      mark: dto.mark,\n    });\n  }\n\n  @ApiOperation({ summary: '获取下载链接，不支持下载文件夹' })\n  @ApiOkResponse({ type: String })\n  @Post('download')\n  async download(@Body() dto: FileInfoDto): Promise<string> {\n    return this.manageService.getDownloadLink(`${dto.path}${dto.name}`);\n  }\n\n  @ApiOperation({ summary: '重命名文件或文件夹' })\n  @Post('rename')\n  async rename(@Body() dto: RenameDto): Promise<void> {\n    const result = await this.manageService.checkFileExist(\n      `${dto.path}${dto.toName}${dto.type === 'dir' ? '/' : ''}`,\n    );\n    if (result) {\n      throw new ApiException(20001);\n    }\n    if (dto.type === 'file') {\n      await this.manageService.renameFile(dto.path, dto.name, dto.toName);\n    } else {\n      await this.manageService.renameDir(dto.path, dto.name, dto.toName);\n    }\n  }\n\n  @ApiOperation({ summary: '删除文件或文件夹' })\n  @Post('delete')\n  async delete(@Body() dto: DeleteDto): Promise<void> {\n    await this.manageService.deleteMultiFileOrDir(dto.files, dto.path);\n  }\n\n  @ApiOperation({ summary: '剪切文件或文件夹，支持批量' })\n  @Post('cut')\n  async cut(@Body() dto: FileOpDto): Promise<void> {\n    if (dto.originPath === dto.toPath) {\n      throw new ApiException(20002);\n    }\n    await this.manageService.moveMultiFileOrDir(\n      dto.files,\n      dto.originPath,\n      dto.toPath,\n    );\n  }\n\n  @ApiOperation({ summary: '复制文件或文件夹，支持批量' })\n  @Post('copy')\n  async copy(@Body() dto: FileOpDto): Promise<void> {\n    await this.manageService.copyMultiFileOrDir(\n      dto.files,\n      dto.originPath,\n      dto.toPath,\n    );\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/netdisk/manager/manage.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n  ArrayMaxSize,\n  IsNotEmpty,\n  IsOptional,\n  IsString,\n  Matches,\n  Validate,\n  ValidateIf,\n  ValidateNested,\n  ValidationArguments,\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from 'class-validator';\nimport { isEmpty } from 'lodash';\nimport { NETDISK_HANDLE_MAX_ITEM } from '../../admin.constants';\n\n@ValidatorConstraint({ name: 'IsLegalNameExpression', async: false })\nexport class IsLegalNameExpression implements ValidatorConstraintInterface {\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  validate(value: string, args: ValidationArguments) {\n    try {\n      if (isEmpty(value)) {\n        throw new Error('dir name is empty');\n      }\n      if (value.includes('/')) {\n        throw new Error('dir name not allow /');\n      }\n      return true;\n    } catch (e) {\n      return false;\n    }\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  defaultMessage(_args: ValidationArguments) {\n    // here you can provide default error message if validation failed\n    return 'file or dir name invalid';\n  }\n}\n\nexport class FileOpItem {\n  @ApiProperty({ description: '文件类型', enum: ['file', 'dir'] })\n  @IsString()\n  @Matches(/(^file$)|(^dir$)/)\n  type: string;\n\n  @ApiProperty({ description: '文件名称' })\n  @IsString()\n  @IsNotEmpty()\n  @Validate(IsLegalNameExpression)\n  name: string;\n}\n\nexport class GetFileListDto {\n  @ApiProperty({ description: '分页标识' })\n  @IsOptional()\n  @IsString()\n  marker: string;\n\n  @ApiProperty({ description: '当前路径' })\n  @IsString()\n  path: string;\n\n  @ApiPropertyOptional({ description: '搜索关键字' })\n  @Validate(IsLegalNameExpression)\n  @ValidateIf((o) => !isEmpty(o.key))\n  @IsString()\n  key: string;\n}\n\nexport class MKDirDto {\n  @ApiProperty({ description: '文件夹名称' })\n  @IsNotEmpty()\n  @IsString()\n  @Validate(IsLegalNameExpression)\n  dirName: string;\n\n  @ApiProperty({ description: '所属路径' })\n  @IsString()\n  path: string;\n}\n\nexport class RenameDto {\n  @ApiProperty({ description: '文件类型' })\n  @IsString()\n  @Matches(/(^file$)|(^dir$)/)\n  type: string;\n\n  @ApiProperty({ description: '更改的名称' })\n  @IsString()\n  @IsNotEmpty()\n  @Validate(IsLegalNameExpression)\n  toName: string;\n\n  @ApiProperty({ description: '原来的名称' })\n  @IsString()\n  @IsNotEmpty()\n  @Validate(IsLegalNameExpression)\n  name: string;\n\n  @ApiProperty({ description: '路径' })\n  @IsString()\n  path: string;\n}\n\nexport class FileInfoDto {\n  @ApiProperty({ description: '文件名' })\n  @IsString()\n  @IsNotEmpty()\n  @Validate(IsLegalNameExpression)\n  name: string;\n\n  @ApiProperty({ description: '文件所在路径' })\n  @IsString()\n  path: string;\n}\n\nexport class DeleteDto {\n  @ApiProperty({ description: '需要操作的文件或文件夹', type: [FileOpItem] })\n  @Type(() => FileOpItem)\n  @ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM)\n  @ValidateNested({ each: true })\n  files: FileOpItem[];\n\n  @ApiProperty({ description: '所在目录' })\n  @IsString()\n  path: string;\n}\n\nexport class MarkFileDto {\n  @ApiProperty({ description: '文件名' })\n  @IsString()\n  @IsNotEmpty()\n  @Validate(IsLegalNameExpression)\n  name: string;\n\n  @ApiProperty({ description: '文件所在路径' })\n  @IsString()\n  path: string;\n\n  @ApiProperty({ description: '备注信息' })\n  @IsString()\n  mark: string;\n}\n\nexport class FileOpDto {\n  @ApiProperty({ description: '需要操作的文件或文件夹', type: [FileOpItem] })\n  @Type(() => FileOpItem)\n  @ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM)\n  @ValidateNested({ each: true })\n  files: FileOpItem[];\n\n  @ApiProperty({ description: '操作前的目录' })\n  @IsString()\n  originPath: string;\n\n  @ApiProperty({ description: '操作后的目录' })\n  @IsString()\n  toPath: string;\n}\n"
  },
  {
    "path": "src/modules/admin/netdisk/manager/manage.service.ts",
    "content": "import { Inject, Injectable } from '@nestjs/common';\nimport {\n  NETDISK_COPY_SUFFIX,\n  NETDISK_DELIMITER,\n  NETDISK_HANDLE_MAX_ITEM,\n  NETDISK_LIMIT,\n  QINIU_CONFIG,\n} from '../../admin.constants';\nimport { IQiniuConfig } from '../../admin.interface';\nimport * as qiniu from 'qiniu';\nimport { rs, conf, auth } from 'qiniu';\nimport { UtilService } from 'src/shared/services/util.service';\nimport { isEmpty } from 'lodash';\nimport { SFileInfo, SFileInfoDetail, SFileList } from './manage.class';\nimport { SysUserService } from '../../system/user/user.service';\nimport { AccountInfo } from '../../system/user/user.class';\nimport { extname, basename } from 'path';\nimport { FileOpItem } from './manage.dto';\n\n@Injectable()\nexport class NetDiskManageService {\n  private config: conf.ConfigOptions;\n  private mac: auth.digest.Mac;\n  private bucketManager: rs.BucketManager;\n\n  constructor(\n    @Inject(QINIU_CONFIG) private qiniuConfig: IQiniuConfig,\n    private userService: SysUserService,\n    private util: UtilService,\n  ) {\n    this.mac = new qiniu.auth.digest.Mac(\n      this.qiniuConfig.accessKey,\n      this.qiniuConfig.secretKey,\n    );\n    this.config = new qiniu.conf.Config({\n      zone: this.qiniuConfig.zone,\n    });\n    // bucket manager\n    this.bucketManager = new qiniu.rs.BucketManager(this.mac, this.config);\n  }\n\n  /**\n   * 获取文件列表\n   * @param prefix 当前文件夹路径，搜索模式下会被忽略\n   * @param marker 下一页标识\n   * @returns iFileListResult\n   */\n  async getFileList(prefix = '', marker = '', skey = ''): Promise<SFileList> {\n    // 是否需要搜索\n    const searching = !isEmpty(skey);\n    return new Promise<SFileList>((resolve, reject) => {\n      this.bucketManager.listPrefix(\n        this.qiniuConfig.bucket,\n        {\n          prefix: searching ? '' : prefix,\n          limit: NETDISK_LIMIT,\n          delimiter: searching ? '' : NETDISK_DELIMITER,\n          marker,\n        },\n        (err, respBody, respInfo) => {\n          if (err) {\n            reject(err);\n            return;\n          }\n          if (respInfo.statusCode === 200) {\n            // 如果这个nextMarker不为空，那么还有未列举完毕的文件列表，下次调用listPrefix的时候，\n            // 指定options里面的marker为这个值\n            const fileList: SFileInfo[] = [];\n            // 处理目录，但只有非搜索模式下可用\n            if (!searching && !isEmpty(respBody.commonPrefixes)) {\n              // dir\n              for (const dirPath of respBody.commonPrefixes) {\n                const name = (dirPath as string)\n                  .substr(0, dirPath.length - 1)\n                  .replace(prefix, '');\n                if (isEmpty(skey) || name.includes(skey)) {\n                  fileList.push({\n                    name: (dirPath as string)\n                      .substr(0, dirPath.length - 1)\n                      .replace(prefix, ''),\n                    type: 'dir',\n                    id: this.util.generateRandomValue(10),\n                  });\n                }\n              }\n            }\n            // handle items\n            if (!isEmpty(respBody.items)) {\n              // file\n              for (const item of respBody.items) {\n                // 搜索模式下处理\n                if (searching) {\n                  const pathList: string[] = item.key.split(NETDISK_DELIMITER);\n                  // dir is empty stirng, file is key string\n                  const name = pathList.pop();\n                  if (\n                    item.key.endsWith(NETDISK_DELIMITER) &&\n                    pathList[pathList.length - 1].includes(skey)\n                  ) {\n                    // 结果是目录\n                    const ditName = pathList.pop();\n                    fileList.push({\n                      id: this.util.generateRandomValue(10),\n                      name: ditName,\n                      type: 'dir',\n                      belongTo: pathList.join(NETDISK_DELIMITER),\n                    });\n                  } else if (name.includes(skey)) {\n                    // 文件\n                    fileList.push({\n                      id: this.util.generateRandomValue(10),\n                      name,\n                      type: 'file',\n                      fsize: item.fsize,\n                      mimeType: item.mimeType,\n                      putTime: new Date(parseInt(item.putTime) / 10000),\n                      belongTo: pathList.join(NETDISK_DELIMITER),\n                    });\n                  }\n                } else {\n                  // 正常获取列表\n                  const fileKey = item.key.replace(prefix, '') as string;\n                  if (!isEmpty(fileKey)) {\n                    fileList.push({\n                      id: this.util.generateRandomValue(10),\n                      name: fileKey,\n                      type: 'file',\n                      fsize: item.fsize,\n                      mimeType: item.mimeType,\n                      putTime: new Date(parseInt(item.putTime) / 10000),\n                    });\n                  }\n                }\n              }\n            }\n            resolve({\n              list: fileList,\n              marker: respBody.marker || null,\n            });\n          } else {\n            reject(\n              new Error(\n                `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n              ),\n            );\n          }\n        },\n      );\n    });\n  }\n\n  /**\n   * 获取文件信息\n   */\n  async getFileInfo(name: string, path: string): Promise<SFileInfoDetail> {\n    return new Promise((resolve, reject) => {\n      this.bucketManager.stat(\n        this.qiniuConfig.bucket,\n        `${path}${name}`,\n        (err, respBody, respInfo) => {\n          if (err) {\n            reject(err);\n            return;\n          }\n          if (respInfo.statusCode == 200) {\n            const detailInfo: SFileInfoDetail = {\n              fsize: respBody.fsize,\n              hash: respBody.hash,\n              md5: respBody.md5,\n              mimeType: respBody.mimeType.split('/x-qn-meta')[0],\n              putTime: new Date(parseInt(respBody.putTime) / 10000),\n              type: respBody.type,\n              uploader: '',\n              mark: respBody?.['x-qn-meta']?.['!mark'] ?? '',\n            };\n            if (!respBody.endUser) {\n              resolve(detailInfo);\n            } else {\n              this.userService\n                .getAccountInfo(parseInt(respBody.endUser))\n                .then((user: AccountInfo) => {\n                  if (isEmpty(user)) {\n                    resolve(detailInfo);\n                  } else {\n                    detailInfo.uploader = user.name;\n                    resolve(detailInfo);\n                  }\n                });\n            }\n          } else {\n            reject(\n              new Error(\n                `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n              ),\n            );\n          }\n        },\n      );\n    });\n  }\n\n  /**\n   * 修改文件MimeType\n   */\n  async changeFileHeaders(\n    name: string,\n    path: string,\n    headers: { [k: string]: string },\n  ): Promise<void> {\n    return new Promise((resolve, reject) => {\n      this.bucketManager.changeHeaders(\n        this.qiniuConfig.bucket,\n        `${path}${name}`,\n        headers,\n        (err, _, respInfo) => {\n          if (err) {\n            reject();\n            return;\n          }\n          if (respInfo.statusCode == 200) {\n            resolve();\n          } else {\n            reject(\n              new Error(\n                `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n              ),\n            );\n          }\n        },\n      );\n    });\n  }\n\n  /**\n   * 创建文件夹\n   * @returns true创建成功\n   */\n  async createDir(dirName: string): Promise<void> {\n    const safeDirName = dirName.endsWith('/') ? dirName : `${dirName}/`;\n    return new Promise((resolve, reject) => {\n      // 上传一个空文件以用于显示文件夹效果\n      const formUploader = new qiniu.form_up.FormUploader(this.config);\n      const putExtra = new qiniu.form_up.PutExtra();\n      formUploader.put(\n        this.createUploadToken(''),\n        safeDirName,\n        ' ',\n        putExtra,\n        (respErr, respBody, respInfo) => {\n          if (respErr) {\n            reject(respErr);\n            return;\n          }\n          if (respInfo.statusCode === 200) {\n            resolve();\n          } else {\n            reject(\n              new Error(\n                `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n              ),\n            );\n          }\n        },\n      );\n    });\n  }\n\n  /**\n   * 检查文件是否存在，同可检查目录\n   */\n  async checkFileExist(filePath: string): Promise<boolean> {\n    return new Promise((resolve, reject) => {\n      // fix path end must a /\n\n      // 检测文件夹是否存在\n      this.bucketManager.stat(\n        this.qiniuConfig.bucket,\n        filePath,\n        (respErr, respBody, respInfo) => {\n          if (respErr) {\n            reject(respErr);\n            return;\n          }\n          if (respInfo.statusCode === 200) {\n            // 文件夹存在\n            resolve(true);\n          } else if (respInfo.statusCode === 612) {\n            // 文件夹不存在\n            resolve(false);\n          } else {\n            reject(\n              new Error(\n                `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n              ),\n            );\n          }\n        },\n      );\n    });\n  }\n\n  /**\n   * 创建Upload Token, 默认过期时间一小时\n   * @returns upload token\n   */\n  createUploadToken(endUser: string): string {\n    const policy = new qiniu.rs.PutPolicy({\n      scope: this.qiniuConfig.bucket,\n      insertOnly: 1,\n      endUser,\n    });\n    const uploadToken = policy.uploadToken(this.mac);\n    return uploadToken;\n  }\n\n  /**\n   * 重命名文件\n   * @param dir 文件路径\n   * @param name 文件名称\n   */\n  async renameFile(dir: string, name: string, toName: string): Promise<void> {\n    const fileName = `${dir}${name}`;\n    const toFileName = `${dir}${toName}`;\n    const op = {\n      force: true,\n    };\n    return new Promise((resolve, reject) => {\n      this.bucketManager.move(\n        this.qiniuConfig.bucket,\n        fileName,\n        this.qiniuConfig.bucket,\n        toFileName,\n        op,\n        (err, respBody, respInfo) => {\n          if (err) {\n            reject(err);\n          } else {\n            if (respInfo.statusCode === 200) {\n              resolve();\n            } else {\n              reject(\n                new Error(\n                  `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n                ),\n              );\n            }\n          }\n        },\n      );\n    });\n  }\n\n  /**\n   * 移动文件\n   */\n  async moveFile(dir: string, toDir: string, name: string): Promise<void> {\n    const fileName = `${dir}${name}`;\n    const toFileName = `${toDir}${name}`;\n    const op = {\n      force: true,\n    };\n    return new Promise((resolve, reject) => {\n      this.bucketManager.move(\n        this.qiniuConfig.bucket,\n        fileName,\n        this.qiniuConfig.bucket,\n        toFileName,\n        op,\n        (err, respBody, respInfo) => {\n          if (err) {\n            reject(err);\n          } else {\n            if (respInfo.statusCode === 200) {\n              resolve();\n            } else {\n              reject(\n                new Error(\n                  `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n                ),\n              );\n            }\n          }\n        },\n      );\n    });\n  }\n\n  /**\n   * 复制文件\n   */\n  async copyFile(dir: string, toDir: string, name: string): Promise<void> {\n    const fileName = `${dir}${name}`;\n    // 拼接文件名\n    const ext = extname(name);\n    const bn = basename(name, ext);\n    const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`;\n    const op = {\n      force: true,\n    };\n    return new Promise((resolve, reject) => {\n      this.bucketManager.copy(\n        this.qiniuConfig.bucket,\n        fileName,\n        this.qiniuConfig.bucket,\n        toFileName,\n        op,\n        (err, respBody, respInfo) => {\n          if (err) {\n            reject(err);\n          } else {\n            if (respInfo.statusCode === 200) {\n              resolve();\n            } else {\n              reject(\n                new Error(\n                  `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n                ),\n              );\n            }\n          }\n        },\n      );\n    });\n  }\n\n  /**\n   * 重命名文件夹\n   */\n  async renameDir(path: string, name: string, toName: string): Promise<void> {\n    const dirName = `${path}${name}`;\n    const toDirName = `${path}${toName}`;\n    let hasFile = true;\n    let marker = '';\n    const op = {\n      force: true,\n    };\n    const bucketName = this.qiniuConfig.bucket;\n    while (hasFile) {\n      await new Promise<void>((resolve, reject) => {\n        // 列举当前目录下的所有文件\n        this.bucketManager.listPrefix(\n          this.qiniuConfig.bucket,\n          {\n            prefix: dirName,\n            limit: NETDISK_HANDLE_MAX_ITEM,\n            marker,\n          },\n          (err, respBody, respInfo) => {\n            if (err) {\n              reject(err);\n              return;\n            }\n            if (respInfo.statusCode === 200) {\n              const moveOperations = respBody.items.map((item) => {\n                const { key } = item;\n                const destKey = key.replace(dirName, toDirName);\n                return qiniu.rs.moveOp(\n                  bucketName,\n                  key,\n                  bucketName,\n                  destKey,\n                  op,\n                );\n              });\n              this.bucketManager.batch(\n                moveOperations,\n                (err2, respBody2, respInfo2) => {\n                  if (err2) {\n                    reject(err2);\n                    return;\n                  }\n                  if (respInfo2.statusCode === 200) {\n                    if (isEmpty(respBody.marker)) {\n                      hasFile = false;\n                    } else {\n                      marker = respBody.marker;\n                    }\n                    resolve();\n                  } else {\n                    reject(\n                      new Error(\n                        `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`,\n                      ),\n                    );\n                  }\n                },\n              );\n            } else {\n              reject(\n                new Error(\n                  `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n                ),\n              );\n            }\n          },\n        );\n      });\n    }\n  }\n\n  /**\n   * 获取七牛下载的文件url链接\n   * @param key 文件路径\n   * @returns 连接\n   */\n  getDownloadLink(key: string): string {\n    if (this.qiniuConfig.access === 'public') {\n      return this.bucketManager.publicDownloadUrl(this.qiniuConfig.domain, key);\n    } else if (this.qiniuConfig.access === 'private') {\n      return this.bucketManager.privateDownloadUrl(\n        this.qiniuConfig.domain,\n        key,\n        Date.now() / 1000 + 36000,\n      );\n    }\n    throw new Error('qiniu config access type not support');\n  }\n\n  /**\n   * 删除文件\n   * @param dir 删除的文件夹目录\n   * @param name 文件名\n   */\n  async deleteFile(dir: string, name: string): Promise<void> {\n    return new Promise((resolve, reject) => {\n      this.bucketManager.delete(\n        this.qiniuConfig.bucket,\n        `${dir}${name}`,\n        (err, respBody, respInfo) => {\n          if (err) {\n            reject(err);\n            return;\n          }\n          if (respInfo.statusCode === 200) {\n            resolve();\n          } else {\n            reject(\n              new Error(\n                `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n              ),\n            );\n          }\n        },\n      );\n    });\n  }\n\n  /**\n   * 删除文件夹\n   * @param dir 文件夹所在的上级目录\n   * @param name 文件目录名称\n   */\n  async deleteMultiFileOrDir(\n    fileList: FileOpItem[],\n    dir: string,\n  ): Promise<void> {\n    const files = fileList.filter((item) => item.type === 'file');\n    if (files.length > 0) {\n      // 批处理文件\n      const copyOperations = files.map((item) => {\n        const fileName = `${dir}${item.name}`;\n        return qiniu.rs.deleteOp(this.qiniuConfig.bucket, fileName);\n      });\n      await new Promise<void>((resolve, reject) => {\n        this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => {\n          if (err) {\n            reject(err);\n            return;\n          }\n          if (respInfo.statusCode === 200) {\n            resolve();\n          } else if (respInfo.statusCode === 298) {\n            reject(new Error('操作异常，但部分文件夹删除成功'));\n          } else {\n            reject(\n              new Error(\n                `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n              ),\n            );\n          }\n        });\n      });\n    }\n    // 处理文件夹\n    const dirs = fileList.filter((item) => item.type === 'dir');\n    if (dirs.length > 0) {\n      // 处理文件夹的复制\n      for (let i = 0; i < dirs.length; i++) {\n        const dirName = `${dir}${dirs[i].name}/`;\n        let hasFile = true;\n        let marker = '';\n        while (hasFile) {\n          await new Promise<void>((resolve, reject) => {\n            // 列举当前目录下的所有文件\n            this.bucketManager.listPrefix(\n              this.qiniuConfig.bucket,\n              {\n                prefix: dirName,\n                limit: NETDISK_HANDLE_MAX_ITEM,\n                marker,\n              },\n              (err, respBody, respInfo) => {\n                if (err) {\n                  reject(err);\n                  return;\n                }\n                if (respInfo.statusCode === 200) {\n                  const moveOperations = respBody.items.map((item) => {\n                    const { key } = item;\n                    return qiniu.rs.deleteOp(this.qiniuConfig.bucket, key);\n                  });\n                  this.bucketManager.batch(\n                    moveOperations,\n                    (err2, respBody2, respInfo2) => {\n                      if (err2) {\n                        reject(err2);\n                        return;\n                      }\n                      if (respInfo2.statusCode === 200) {\n                        if (isEmpty(respBody.marker)) {\n                          hasFile = false;\n                        } else {\n                          marker = respBody.marker;\n                        }\n                        resolve();\n                      } else {\n                        reject(\n                          new Error(\n                            `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`,\n                          ),\n                        );\n                      }\n                    },\n                  );\n                } else {\n                  reject(\n                    new Error(\n                      `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n                    ),\n                  );\n                }\n              },\n            );\n          });\n        }\n      }\n    }\n  }\n\n  /**\n   * 复制文件，含文件夹\n   */\n  async copyMultiFileOrDir(\n    fileList: FileOpItem[],\n    dir: string,\n    toDir: string,\n  ): Promise<void> {\n    const files = fileList.filter((item) => item.type === 'file');\n    const op = {\n      force: true,\n    };\n    if (files.length > 0) {\n      // 批处理文件\n      const copyOperations = files.map((item) => {\n        const fileName = `${dir}${item.name}`;\n        // 拼接文件名\n        const ext = extname(item.name);\n        const bn = basename(item.name, ext);\n        const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`;\n        return qiniu.rs.copyOp(\n          this.qiniuConfig.bucket,\n          fileName,\n          this.qiniuConfig.bucket,\n          toFileName,\n          op,\n        );\n      });\n      await new Promise<void>((resolve, reject) => {\n        this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => {\n          if (err) {\n            reject(err);\n            return;\n          }\n          if (respInfo.statusCode === 200) {\n            resolve();\n          } else if (respInfo.statusCode === 298) {\n            reject(new Error('操作异常，但部分文件夹删除成功'));\n          } else {\n            reject(\n              new Error(\n                `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n              ),\n            );\n          }\n        });\n      });\n    }\n    // 处理文件夹\n    const dirs = fileList.filter((item) => item.type === 'dir');\n    if (dirs.length > 0) {\n      // 处理文件夹的复制\n      for (let i = 0; i < dirs.length; i++) {\n        const dirName = `${dir}${dirs[i].name}/`;\n        const copyDirName = `${toDir}${dirs[i].name}${NETDISK_COPY_SUFFIX}/`;\n        let hasFile = true;\n        let marker = '';\n        while (hasFile) {\n          await new Promise<void>((resolve, reject) => {\n            // 列举当前目录下的所有文件\n            this.bucketManager.listPrefix(\n              this.qiniuConfig.bucket,\n              {\n                prefix: dirName,\n                limit: NETDISK_HANDLE_MAX_ITEM,\n                marker,\n              },\n              (err, respBody, respInfo) => {\n                if (err) {\n                  reject(err);\n                  return;\n                }\n                if (respInfo.statusCode === 200) {\n                  const moveOperations = respBody.items.map((item) => {\n                    const { key } = item;\n                    const destKey = key.replace(dirName, copyDirName);\n                    return qiniu.rs.copyOp(\n                      this.qiniuConfig.bucket,\n                      key,\n                      this.qiniuConfig.bucket,\n                      destKey,\n                      op,\n                    );\n                  });\n                  this.bucketManager.batch(\n                    moveOperations,\n                    (err2, respBody2, respInfo2) => {\n                      if (err2) {\n                        reject(err2);\n                        return;\n                      }\n                      if (respInfo2.statusCode === 200) {\n                        if (isEmpty(respBody.marker)) {\n                          hasFile = false;\n                        } else {\n                          marker = respBody.marker;\n                        }\n                        resolve();\n                      } else {\n                        reject(\n                          new Error(\n                            `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`,\n                          ),\n                        );\n                      }\n                    },\n                  );\n                } else {\n                  reject(\n                    new Error(\n                      `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n                    ),\n                  );\n                }\n              },\n            );\n          });\n        }\n      }\n    }\n  }\n\n  /**\n   * 移动文件，含文件夹\n   */\n  async moveMultiFileOrDir(\n    fileList: FileOpItem[],\n    dir: string,\n    toDir: string,\n  ): Promise<void> {\n    const files = fileList.filter((item) => item.type === 'file');\n    const op = {\n      force: true,\n    };\n    if (files.length > 0) {\n      // 批处理文件\n      const copyOperations = files.map((item) => {\n        const fileName = `${dir}${item.name}`;\n        const toFileName = `${toDir}${item.name}`;\n        return qiniu.rs.moveOp(\n          this.qiniuConfig.bucket,\n          fileName,\n          this.qiniuConfig.bucket,\n          toFileName,\n          op,\n        );\n      });\n      await new Promise<void>((resolve, reject) => {\n        this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => {\n          if (err) {\n            reject(err);\n            return;\n          }\n          if (respInfo.statusCode === 200) {\n            resolve();\n          } else if (respInfo.statusCode === 298) {\n            reject(new Error('操作异常，但部分文件夹删除成功'));\n          } else {\n            reject(\n              new Error(\n                `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n              ),\n            );\n          }\n        });\n      });\n    }\n    // 处理文件夹\n    const dirs = fileList.filter((item) => item.type === 'dir');\n    if (dirs.length > 0) {\n      // 处理文件夹的复制\n      for (let i = 0; i < dirs.length; i++) {\n        const dirName = `${dir}${dirs[i].name}/`;\n        const toDirName = `${toDir}${dirs[i].name}/`;\n        // 移动的目录不是是自己\n        if (toDirName.startsWith(dirName)) {\n          continue;\n        }\n        let hasFile = true;\n        let marker = '';\n        while (hasFile) {\n          await new Promise<void>((resolve, reject) => {\n            // 列举当前目录下的所有文件\n            this.bucketManager.listPrefix(\n              this.qiniuConfig.bucket,\n              {\n                prefix: dirName,\n                limit: NETDISK_HANDLE_MAX_ITEM,\n                marker,\n              },\n              (err, respBody, respInfo) => {\n                if (err) {\n                  reject(err);\n                  return;\n                }\n                if (respInfo.statusCode === 200) {\n                  const moveOperations = respBody.items.map((item) => {\n                    const { key } = item;\n                    const destKey = key.replace(dirName, toDirName);\n                    return qiniu.rs.moveOp(\n                      this.qiniuConfig.bucket,\n                      key,\n                      this.qiniuConfig.bucket,\n                      destKey,\n                      op,\n                    );\n                  });\n                  this.bucketManager.batch(\n                    moveOperations,\n                    (err2, respBody2, respInfo2) => {\n                      if (err2) {\n                        reject(err2);\n                        return;\n                      }\n                      if (respInfo2.statusCode === 200) {\n                        if (isEmpty(respBody.marker)) {\n                          hasFile = false;\n                        } else {\n                          marker = respBody.marker;\n                        }\n                        resolve();\n                      } else {\n                        reject(\n                          new Error(\n                            `Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`,\n                          ),\n                        );\n                      }\n                    },\n                  );\n                } else {\n                  reject(\n                    new Error(\n                      `Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,\n                    ),\n                  );\n                }\n              },\n            );\n          });\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/netdisk/netdisk.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { qiniuProvider } from '../core/provider/qiniu.provider';\nimport { SystemModule } from '../system/system.module';\nimport { NetDiskManageController } from './manager/manage.controller';\nimport { NetDiskManageService } from './manager/manage.service';\nimport { NetDiskOverviewController } from './overview/overview.controller';\nimport { NetDiskOverviewService } from './overview/overview.service';\n\n@Module({\n  imports: [SystemModule],\n  controllers: [NetDiskManageController, NetDiskOverviewController],\n  providers: [NetDiskManageService, NetDiskOverviewService, qiniuProvider()],\n})\nexport class NetdiskModule {}\n"
  },
  {
    "path": "src/modules/admin/netdisk/overview/overview.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class SpaceInfo {\n  @ApiProperty({ description: '当月的X号', type: [Number] })\n  times: number[];\n\n  @ApiProperty({ description: '对应天数的容量, byte单位', type: [Number] })\n  datas: number[];\n}\n\nexport class CountInfo {\n  @ApiProperty({ description: '当月的X号', type: [Number] })\n  times: number[];\n\n  @ApiProperty({ description: '对应天数的文件数量', type: [Number] })\n  datas: number[];\n}\n\nexport class FlowInfo {\n  @ApiProperty({ description: '当月的X号', type: [Number] })\n  times: number[];\n\n  @ApiProperty({ description: '对应天数的耗费流量', type: [Number] })\n  datas: number[];\n}\n\nexport class HitInfo {\n  @ApiProperty({ description: '当月的X号', type: [Number] })\n  times: number[];\n\n  @ApiProperty({ description: '对应天数的Get请求次数', type: [Number] })\n  datas: number[];\n}\n\nexport class OverviewSpaceInfo {\n  @ApiProperty({ description: '当前使用容量' })\n  spaceSize: number;\n\n  @ApiProperty({ description: '当前文件数量' })\n  fileSize: number;\n\n  @ApiProperty({ description: '当天使用流量' })\n  flowSize: number;\n\n  @ApiProperty({ description: '当天请求次数' })\n  hitSize: number;\n\n  @ApiProperty({ description: '流量趋势，从当月1号开始计算', type: FlowInfo })\n  flowTrend: FlowInfo;\n\n  @ApiProperty({ description: '容量趋势，从当月1号开始计算', type: SpaceInfo })\n  sizeTrend: SpaceInfo;\n}\n"
  },
  {
    "path": "src/modules/admin/netdisk/overview/overview.controller.ts",
    "content": "import {\n  CacheInterceptor,\n  CacheKey,\n  CacheTTL,\n  Controller,\n  Get,\n  UseInterceptors,\n} from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { ADMIN_PREFIX } from '../../admin.constants';\nimport { PermissionOptional } from '../../core/decorators/permission-optional.decorator';\nimport { OverviewSpaceInfo } from './overview.class';\nimport { NetDiskOverviewService } from './overview.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('网盘概览模块')\n@Controller('overview')\nexport class NetDiskOverviewController {\n  constructor(private overviewService: NetDiskOverviewService) {}\n\n  @CacheKey('netdisk_overview_desc')\n  @CacheTTL(3600)\n  @UseInterceptors(CacheInterceptor)\n  @ApiOperation({ summary: '获取网盘空间数据统计' })\n  @ApiOkResponse({ type: OverviewSpaceInfo })\n  @PermissionOptional()\n  @Get('desc')\n  async space(): Promise<OverviewSpaceInfo> {\n    const date = this.overviewService.getZeroHourAnd1Day(new Date());\n    const hit = await this.overviewService.getHit(date);\n    const flow = await this.overviewService.getFlow(date);\n    const space = await this.overviewService.getSpace(date);\n    const count = await this.overviewService.getCount(date);\n    return {\n      fileSize: count.datas[count.datas.length - 1],\n      flowSize: flow.datas[flow.datas.length - 1],\n      hitSize: hit.datas[hit.datas.length - 1],\n      spaceSize: space.datas[space.datas.length - 1],\n      flowTrend: flow,\n      sizeTrend: space,\n    };\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/netdisk/overview/overview.service.ts",
    "content": "import { Inject, Injectable } from '@nestjs/common';\nimport { QINIU_API, QINIU_CONFIG } from '../../admin.constants';\nimport { IQiniuConfig } from '../../admin.interface';\nimport * as qiniu from 'qiniu';\nimport {\n  getMonth,\n  getYear,\n  format,\n  getDate,\n  fromUnixTime,\n  parseISO,\n} from 'date-fns';\nimport { CountInfo, FlowInfo, HitInfo, SpaceInfo } from './overview.class';\nimport { HttpService } from '@nestjs/axios';\n\n@Injectable()\nexport class NetDiskOverviewService {\n  private mac: qiniu.auth.digest.Mac;\n  private readonly FORMAT = 'yyyyMMddHHmmss';\n\n  constructor(\n    @Inject(QINIU_CONFIG) private qiniuConfig: IQiniuConfig,\n    private readonly httpService: HttpService,\n  ) {\n    this.mac = new qiniu.auth.digest.Mac(\n      this.qiniuConfig.accessKey,\n      this.qiniuConfig.secretKey,\n    );\n  }\n\n  /**\n   * 获取当天零时\n   */\n  getZeroHourToDay(current: Date): Date {\n    const month = getMonth(current);\n    const year = getYear(current);\n    const date = getDate(current);\n    return new Date(year, month, date, 0);\n  }\n\n  /**\n   * 获取当月1号零时\n   */\n  getZeroHourAnd1Day(current: Date): Date {\n    const month = getMonth(current);\n    const year = getYear(current);\n    return new Date(year, month, 1, 0);\n  }\n\n  /**\n   * 该接口可以获取标准存储的当前存储量。可查询当天计量，统计延迟大概 5 分钟。\n   * https://developer.qiniu.com/kodo/3908/statistic-space\n   */\n  async getSpace(start: Date, end = new Date()): Promise<SpaceInfo> {\n    const beginDate = format(start, this.FORMAT);\n    const endDate = format(end, this.FORMAT);\n    const url = `${QINIU_API}/v6/space?bucket=${this.qiniuConfig.bucket}&g=day&begin=${beginDate}&end=${endDate}`;\n    const accessToken = qiniu.util.generateAccessTokenV2(\n      this.mac,\n      url,\n      'GET',\n      'application/x-www-form-urlencoded',\n    );\n    const { data } = await this.httpService.axiosRef.get(url, {\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n        Authorization: `${accessToken}`,\n      },\n    });\n    return {\n      datas: data.datas,\n      times: data.times.map((e) => {\n        return getDate(fromUnixTime(e));\n      }),\n    };\n  }\n\n  /**\n   * 该接口可以获取标准存储的文件数量。可查询当天计量，统计延迟大概 5 分钟。\n   * https://developer.qiniu.com/kodo/3914/count\n   */\n  async getCount(start: Date, end = new Date()): Promise<CountInfo> {\n    const beginDate = format(start, this.FORMAT);\n    const endDate = format(end, this.FORMAT);\n    const url = `${QINIU_API}/v6/count?bucket=${this.qiniuConfig.bucket}&g=day&begin=${beginDate}&end=${endDate}`;\n    const accessToken = qiniu.util.generateAccessTokenV2(\n      this.mac,\n      url,\n      'GET',\n      'application/x-www-form-urlencoded',\n    );\n    const { data } = await this.httpService.axiosRef.get(url, {\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n        Authorization: `${accessToken}`,\n      },\n    });\n    return {\n      times: data.times.map((e) => {\n        return getDate(fromUnixTime(e));\n      }),\n      datas: data.datas,\n    };\n  }\n\n  /**\n   * 外网流出流量统计\n   * 该接口可以获取外网流出流量、CDN回源流量统计和 GET 请求次数。可查询当天计量，统计延迟大概 5 分钟。\n   * https://developer.qiniu.com/kodo/3820/blob-io\n   */\n  async getFlow(start: Date, end = new Date()): Promise<FlowInfo> {\n    const beginDate = format(start, this.FORMAT);\n    const endDate = format(end, this.FORMAT);\n    const url = `${QINIU_API}/v6/blob_io?$bucket=${this.qiniuConfig.bucket}&g=day&$ftype=0&begin=${beginDate}&end=${endDate}&$src=origin&select=flow`;\n    const accessToken = qiniu.util.generateAccessTokenV2(\n      this.mac,\n      url,\n      'GET',\n      'application/x-www-form-urlencoded',\n    );\n    const { data } = await this.httpService.axiosRef.get(url, {\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n        Authorization: `${accessToken}`,\n      },\n    });\n    const times = [];\n    const datas = [];\n    data.forEach((e) => {\n      times.push(getDate(parseISO(e.time)));\n      datas.push(e.values.flow);\n    });\n    return {\n      times,\n      datas,\n    };\n  }\n\n  /**\n   * GET 请求次数统计\n   * 该接口可以获取外网流出流量、CDN回源流量统计和 GET 请求次数。可查询当天计量，统计延迟大概 5 分钟。\n   * https://developer.qiniu.com/kodo/3820/blob-io\n   */\n  async getHit(start: Date, end = new Date()): Promise<HitInfo> {\n    const beginDate = format(start, this.FORMAT);\n    const endDate = format(end, this.FORMAT);\n    const url = `${QINIU_API}/v6/blob_io?$bucket=${this.qiniuConfig.bucket}&g=day&$ftype=0&begin=${beginDate}&end=${endDate}&$src=origin&$src=inner&select=hit`;\n    const accessToken = qiniu.util.generateAccessTokenV2(\n      this.mac,\n      url,\n      'GET',\n      'application/x-www-form-urlencoded',\n    );\n    const { data } = await this.httpService.axiosRef.get(url, {\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n        Authorization: `${accessToken}`,\n      },\n    });\n    const times = [];\n    const datas = [];\n    data.forEach((e) => {\n      times.push(getDate(parseISO(e.time)));\n      datas.push(e.values.hit);\n    });\n    return {\n      times,\n      datas,\n    };\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/dept/dept.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport SysDepartment from 'src/entities/admin/sys-department.entity';\n\nexport class DeptDetailInfo {\n  @ApiProperty({ description: '当前查询的部门' })\n  department?: SysDepartment;\n\n  @ApiProperty({ description: '所属父级部门' })\n  parentDepartment?: SysDepartment;\n}\n"
  },
  {
    "path": "src/modules/admin/system/dept/dept.controller.ts",
    "content": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { ADMIN_PREFIX } from 'src/modules/admin/admin.constants';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport SysDepartment from 'src/entities/admin/sys-department.entity';\nimport { AdminUser } from '../../core/decorators/admin-user.decorator';\nimport { DeptDetailInfo } from './dept.class';\nimport {\n  CreateDeptDto,\n  DeleteDeptDto,\n  InfoDeptDto,\n  MoveDeptDto,\n  TransferDeptDto,\n  UpdateDeptDto,\n} from './dept.dto';\nimport { SysDeptService } from './dept.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('部门模块')\n@Controller('dept')\nexport class SysDeptController {\n  constructor(private deptService: SysDeptService) {}\n\n  @ApiOperation({ summary: '获取系统部门列表' })\n  @ApiOkResponse({ type: [SysDepartment] })\n  @Get('list')\n  async list(@AdminUser('uid') uid: number): Promise<SysDepartment[]> {\n    return await this.deptService.getDepts(uid);\n  }\n\n  @ApiOperation({ summary: '创建系统部门' })\n  @Post('add')\n  async add(@Body() createDeptDto: CreateDeptDto): Promise<void> {\n    await this.deptService.add(createDeptDto.name, createDeptDto.parentId);\n  }\n\n  @ApiOperation({ summary: '删除系统部门' })\n  @Post('delete')\n  async delete(@Body() deleteDeptDto: DeleteDeptDto): Promise<void> {\n    // 查询是否有关联用户或者部门，如果含有则无法删除\n    const count = await this.deptService.countUserByDeptId(\n      deleteDeptDto.departmentId,\n    );\n    if (count > 0) {\n      throw new ApiException(10009);\n    }\n    const count2 = await this.deptService.countRoleByDeptId(\n      deleteDeptDto.departmentId,\n    );\n    if (count2 > 0) {\n      throw new ApiException(10010);\n    }\n    const count3 = await this.deptService.countChildDept(\n      deleteDeptDto.departmentId,\n    );\n    if (count3 > 0) {\n      throw new ApiException(10015);\n    }\n    await this.deptService.delete(deleteDeptDto.departmentId);\n  }\n\n  @ApiOperation({ summary: '查询单个系统部门信息' })\n  @ApiOkResponse({ type: DeptDetailInfo })\n  @Get('info')\n  async info(@Query() infoDeptDto: InfoDeptDto): Promise<DeptDetailInfo> {\n    return await this.deptService.info(infoDeptDto.departmentId);\n  }\n\n  @ApiOperation({ summary: '更新系统部门' })\n  @Post('update')\n  async update(@Body() updateDeptDto: UpdateDeptDto): Promise<void> {\n    await this.deptService.update(updateDeptDto);\n  }\n\n  @ApiOperation({ summary: '管理员部门转移' })\n  @Post('transfer')\n  async transfer(@Body() transferDeptDto: TransferDeptDto): Promise<void> {\n    await this.deptService.transfer(\n      transferDeptDto.userIds,\n      transferDeptDto.departmentId,\n    );\n  }\n\n  @ApiOperation({ summary: '部门移动排序' })\n  @Post('move')\n  async move(@Body() dto: MoveDeptDto): Promise<void> {\n    await this.deptService.move(dto.depts);\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/dept/dept.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsInt,\n  IsOptional,\n  IsString,\n  Min,\n  MinLength,\n  ValidateNested,\n} from 'class-validator';\n\nexport class CreateDeptDto {\n  @ApiProperty({ description: '部门名称' })\n  @IsString()\n  @MinLength(1)\n  name: string;\n\n  @ApiProperty({ description: '父级部门id' })\n  @IsInt()\n  parentId: number;\n\n  @ApiProperty({ description: '排序编号', required: false })\n  @IsOptional()\n  @IsInt()\n  @Min(0)\n  orderNum: number;\n}\n\nexport class UpdateDeptDto extends CreateDeptDto {\n  @ApiProperty({ description: '需要更新的部门id' })\n  @IsInt()\n  @Min(0)\n  id: number;\n}\n\nexport class DeleteDeptDto {\n  @ApiProperty({ description: '删除的系统部门ID' })\n  @IsInt()\n  @Min(0)\n  departmentId: number;\n}\n\nexport class InfoDeptDto {\n  @ApiProperty({ description: '查询的系统部门ID' })\n  @Type(() => Number)\n  @IsInt()\n  @Min(0)\n  departmentId: number;\n}\n\nexport class TransferDeptDto {\n  @ApiProperty({ description: '需要转移的管理员列表编号', type: [Number] })\n  @IsArray()\n  @ArrayNotEmpty()\n  userIds: number[];\n\n  @ApiProperty({ description: '需要转移过去的系统部门ID' })\n  @IsInt()\n  @Min(0)\n  departmentId: number;\n}\n\nexport class MoveDept {\n  @ApiProperty({ description: '当前部门ID' })\n  @IsInt()\n  @Min(0)\n  id: number;\n\n  @ApiProperty({ description: '移动到指定父级部门的ID' })\n  @IsInt()\n  @Min(0)\n  @IsOptional()\n  parentId: number;\n}\n\nexport class MoveDeptDto {\n  @ApiProperty({ description: '部门列表', type: [MoveDept] })\n  @ValidateNested({ each: true })\n  @Type(() => MoveDept)\n  depts: MoveDept[];\n}\n"
  },
  {
    "path": "src/modules/admin/system/dept/dept.service.ts",
    "content": "import { Inject, Injectable } from '@nestjs/common';\nimport { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';\nimport { includes, isEmpty } from 'lodash';\nimport { ROOT_ROLE_ID } from 'src/modules/admin/admin.constants';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport SysDepartment from 'src/entities/admin/sys-department.entity';\nimport SysRoleDepartment from 'src/entities/admin/sys-role-department.entity';\nimport SysUser from 'src/entities/admin/sys-user.entity';\nimport { EntityManager, In, Repository } from 'typeorm';\nimport { SysRoleService } from '../role/role.service';\nimport { DeptDetailInfo } from './dept.class';\nimport { MoveDept, UpdateDeptDto } from './dept.dto';\n\n@Injectable()\nexport class SysDeptService {\n  constructor(\n    @InjectRepository(SysUser) private userRepositoty: Repository<SysUser>,\n    @InjectRepository(SysDepartment)\n    private deptRepositoty: Repository<SysDepartment>,\n    @InjectRepository(SysRoleDepartment)\n    private roleDeptRepositoty: Repository<SysUser>,\n    @InjectEntityManager() private entityManager: EntityManager,\n    @Inject(ROOT_ROLE_ID) private rootRoleId: number,\n    private roleService: SysRoleService,\n  ) {}\n\n  /**\n   * 获取所有部门\n   */\n  async list(): Promise<SysDepartment[]> {\n    return await this.deptRepositoty.find({ order: { orderNum: 'DESC' } });\n  }\n\n  /**\n   * 根据ID查找部门信息\n   */\n  async info(id: number): Promise<DeptDetailInfo> {\n    const department = await this.deptRepositoty.findOne({ where: { id } });\n    if (isEmpty(department)) {\n      throw new ApiException(10019);\n    }\n    let parentDepartment = null;\n    if (department.parentId) {\n      parentDepartment = await this.deptRepositoty.findOne({\n        where: { id: department.parentId },\n      });\n    }\n    return { department, parentDepartment };\n  }\n\n  /**\n   * 更新部门信息\n   */\n  async update(param: UpdateDeptDto): Promise<void> {\n    await this.deptRepositoty.update(param.id, {\n      parentId: param.parentId === -1 ? undefined : param.parentId,\n      name: param.name,\n      orderNum: param.orderNum,\n    });\n  }\n\n  /**\n   * 转移部门\n   */\n  async transfer(userIds: number[], deptId: number): Promise<void> {\n    await this.userRepositoty.update(\n      { id: In(userIds) },\n      { departmentId: deptId },\n    );\n  }\n\n  /**\n   * 新增部门\n   */\n  async add(deptName: string, parentDeptId: number): Promise<void> {\n    await this.deptRepositoty.insert({\n      name: deptName,\n      parentId: parentDeptId === -1 ? null : parentDeptId,\n    });\n  }\n\n  /**\n   * 移动排序\n   */\n  async move(depts: MoveDept[]): Promise<void> {\n    await this.entityManager.transaction(async (manager) => {\n      for (let i = 0; i < depts.length; i++) {\n        await manager.update(\n          SysDepartment,\n          { id: depts[i].id },\n          { parentId: depts[i].parentId },\n        );\n      }\n    });\n  }\n\n  /**\n   * 根据ID删除部门\n   */\n  async delete(departmentId: number): Promise<void> {\n    await this.deptRepositoty.delete(departmentId);\n  }\n\n  /**\n   * 根据部门查询关联的用户数量\n   */\n  async countUserByDeptId(id: number): Promise<number> {\n    return await this.userRepositoty.count({ where: { departmentId: id } });\n  }\n\n  /**\n   * 根据部门查询关联的角色数量\n   */\n  async countRoleByDeptId(id: number): Promise<number> {\n    return await this.roleDeptRepositoty.count({ where: { departmentId: id } });\n  }\n\n  /**\n   * 查找当前部门下的子部门数量\n   */\n  async countChildDept(id: number): Promise<number> {\n    return await this.deptRepositoty.count({ where: { parentId: id } });\n  }\n\n  /**\n   * 根据当前角色id获取部门列表\n   */\n  async getDepts(uid: number): Promise<SysDepartment[]> {\n    const roleIds = await this.roleService.getRoleIdByUser(uid);\n    let depts: any = [];\n    if (includes(roleIds, this.rootRoleId)) {\n      // root find all\n      depts = await this.deptRepositoty.find();\n    } else {\n      // [ 1, 2, 3 ] role find\n      depts = await this.deptRepositoty\n        .createQueryBuilder('dept')\n        .innerJoinAndSelect(\n          'sys_role_department',\n          'role_dept',\n          'dept.id = role_dept.department_id',\n        )\n        .andWhere('role_dept.role_id IN (:...roldIds)', { roldIds: roleIds })\n        .orderBy('dept.order_num', 'ASC')\n        .getMany();\n    }\n    return depts;\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/log/log.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class LoginLogInfo {\n  @ApiProperty({ description: '日志编号' })\n  id: number;\n\n  @ApiProperty({ description: '登录ip' })\n  ip: string;\n\n  @ApiProperty({ description: '系统' })\n  os: string;\n\n  @ApiProperty({ description: '浏览器' })\n  browser: string;\n\n  @ApiProperty({ description: '时间' })\n  time: string;\n\n  @ApiProperty({ description: '登录用户名' })\n  username: string;\n}\n\nexport class TaskLogInfo {\n  @ApiProperty({ description: '日志编号' })\n  id: number;\n\n  @ApiProperty({ description: '任务编号' })\n  taskId: number;\n\n  @ApiProperty({ description: '任务名称' })\n  name: string;\n\n  @ApiProperty({ description: '创建时间' })\n  createdAt: string;\n\n  @ApiProperty({ description: '耗时' })\n  consumeTime: number;\n\n  @ApiProperty({ description: '执行信息' })\n  detail: string;\n\n  @ApiProperty({ description: '任务执行状态' })\n  status: number;\n}\n"
  },
  {
    "path": "src/modules/admin/system/log/log.controller.ts",
    "content": "import { Controller, Get, Query } from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { PageResult } from 'src/common/class/res.class';\nimport { PageOptionsDto } from 'src/common/dto/page.dto';\nimport { ADMIN_PREFIX } from '../../admin.constants';\nimport { LogDisabled } from '../../core/decorators/log-disabled.decorator';\nimport { LoginLogInfo, TaskLogInfo } from './log.class';\nimport { SysLogService } from './log.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('日志模块')\n@Controller('log')\nexport class SysLogController {\n  constructor(private logService: SysLogService) {}\n\n  @ApiOperation({ summary: '分页查询登录日志' })\n  @ApiOkResponse({ type: [LoginLogInfo] })\n  @LogDisabled()\n  @Get('login/page')\n  async loginLogPage(\n    @Query() dto: PageOptionsDto,\n  ): Promise<PageResult<LoginLogInfo>> {\n    const list = await this.logService.pageGetLoginLog(dto.page - 1, dto.limit);\n    const count = await this.logService.countLoginLog();\n    return {\n      list,\n      pagination: {\n        total: count,\n        size: dto.limit,\n        page: dto.page,\n      },\n    };\n  }\n\n  @ApiOperation({ summary: '分页查询任务日志' })\n  @ApiOkResponse({ type: [TaskLogInfo] })\n  @LogDisabled()\n  @Get('task/page')\n  async taskPage(\n    @Query() dto: PageOptionsDto,\n  ): Promise<PageResult<TaskLogInfo>> {\n    const list = await this.logService.page(dto.page - 1, dto.limit);\n    const count = await this.logService.countTaskLog();\n    return {\n      list,\n      pagination: {\n        total: count,\n        size: dto.limit,\n        page: dto.page,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/log/log.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport SysLoginLog from 'src/entities/admin/sys-login-log.entity';\nimport SysTaskLog from 'src/entities/admin/sys-task-log.entity';\nimport { Repository } from 'typeorm';\nimport { UAParser } from 'ua-parser-js';\nimport { LoginLogInfo, TaskLogInfo } from './log.class';\n\n@Injectable()\nexport class SysLogService {\n  constructor(\n    @InjectRepository(SysLoginLog)\n    private loginLogRepository: Repository<SysLoginLog>,\n    @InjectRepository(SysTaskLog)\n    private taskLogRepository: Repository<SysTaskLog>,\n  ) {}\n\n  /**\n   * 记录登录日志\n   */\n  async saveLoginLog(uid: number, ip: string, ua: string): Promise<void> {\n    await this.loginLogRepository.save({\n      ip,\n      userId: uid,\n      ua,\n    });\n  }\n\n  /**\n   * 计算登录日志日志总数\n   */\n  async countLoginLog(): Promise<number> {\n    return await this.loginLogRepository.count();\n  }\n\n  /**\n   * 分页加载日志信息\n   */\n  async pageGetLoginLog(page: number, count: number): Promise<LoginLogInfo[]> {\n    // const result = await this.getRepo().admin.sys.LoginLog.find({\n    //   order: {\n    //     id: 'DESC',\n    //   },\n    //   take: count,\n    //   skip: page * count,\n    // });\n    const result = await this.loginLogRepository\n      .createQueryBuilder('login_log')\n      .innerJoinAndSelect('sys_user', 'user', 'login_log.user_id = user.id')\n      .orderBy('login_log.created_at', 'DESC')\n      .offset(page * count)\n      .limit(count)\n      .getRawMany();\n    const parser = new UAParser();\n    return result.map((e) => {\n      const u = parser.setUA(e.login_log_ua).getResult();\n      return {\n        id: e.login_log_id,\n        ip: e.login_log_ip,\n        os: `${u.os.name} ${u.os.version}`,\n        browser: `${u.browser.name} ${u.browser.version}`,\n        time: e.login_log_created_at,\n        username: e.user_username,\n      };\n    });\n  }\n\n  /**\n   * 清空表中的所有数据\n   */\n  async clearLoginLog(): Promise<void> {\n    await this.loginLogRepository.clear();\n  }\n  // ----- task\n\n  /**\n   * 记录任务日志\n   */\n  async recordTaskLog(\n    tid: number,\n    status: number,\n    time?: number,\n    err?: string,\n  ): Promise<number> {\n    const result = await this.taskLogRepository.save({\n      taskId: tid,\n      status,\n      detail: err,\n    });\n    return result.id;\n  }\n\n  /**\n   * 计算日志总数\n   */\n  async countTaskLog(): Promise<number> {\n    return await this.taskLogRepository.count();\n  }\n\n  /**\n   * 分页加载日志信息\n   */\n  async page(page: number, count: number): Promise<TaskLogInfo[]> {\n    // const result = await this.getRepo().admin.sys.TaskLog.find({\n    //   order: {\n    //     id: 'DESC',\n    //   },\n    //   take: count,\n    //   skip: page * count,\n    // });\n    // return result;\n    const result = await this.taskLogRepository\n      .createQueryBuilder('task_log')\n      .leftJoinAndSelect('sys_task', 'task', 'task_log.task_id = task.id')\n      .orderBy('task_log.id', 'DESC')\n      .offset(page * count)\n      .limit(count)\n      .getRawMany();\n    return result.map((e) => {\n      return {\n        id: e.task_log_id,\n        taskId: e.task_id,\n        name: e.task_name,\n        createdAt: e.task_log_created_at,\n        consumeTime: e.task_log_consume_time,\n        detail: e.task_log_detail,\n        status: e.task_log_status,\n      };\n    });\n  }\n\n  /**\n   * 清空表中的所有数据\n   */\n  async clearTaskLog(): Promise<void> {\n    await this.taskLogRepository.clear();\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/menu/menu.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport SysMenu from 'src/entities/admin/sys-menu.entity';\n\nexport class MenuItemAndParentInfoResult {\n  @ApiProperty({ description: '菜单' })\n  menu?: SysMenu;\n\n  @ApiProperty({ description: '父级菜单' })\n  parentMenu?: SysMenu;\n}\n"
  },
  {
    "path": "src/modules/admin/system/menu/menu.controller.ts",
    "content": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { flattenDeep } from 'lodash';\nimport {\n  ADMIN_PREFIX,\n  FORBIDDEN_OP_MENU_ID_INDEX,\n} from 'src/modules/admin/admin.constants';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport SysMenu from 'src/entities/admin/sys-menu.entity';\nimport { IAdminUser } from '../../admin.interface';\nimport { AdminUser } from '../../core/decorators/admin-user.decorator';\nimport { MenuItemAndParentInfoResult } from './menu.class';\nimport {\n  CreateMenuDto,\n  DeleteMenuDto,\n  InfoMenuDto,\n  UpdateMenuDto,\n} from './menu.dto';\nimport { SysMenuService } from './menu.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('菜单权限模块')\n@Controller('menu')\nexport class SysMenuController {\n  constructor(private menuService: SysMenuService) {}\n\n  @ApiOperation({ summary: '获取对应权限的菜单列表' })\n  @ApiOkResponse({ type: [SysMenu] })\n  @Get('list')\n  async list(@AdminUser() user: IAdminUser): Promise<SysMenu[]> {\n    return await this.menuService.getMenus(user.uid);\n  }\n\n  @ApiOperation({ summary: '新增菜单或权限' })\n  @Post('add')\n  async add(@Body() dto: CreateMenuDto): Promise<void> {\n    // check\n    await this.menuService.check(dto);\n    if (dto.parentId === -1) {\n      dto.parentId = null;\n    }\n    await this.menuService.save(dto);\n    if (dto.type === 2) {\n      // 如果是权限发生更改，则刷新所有在线用户的权限\n      await this.menuService.refreshOnlineUserPerms();\n    }\n  }\n\n  @ApiOperation({ summary: '新增菜单或权限' })\n  @Post('update')\n  async update(@Body() dto: UpdateMenuDto): Promise<void> {\n    if (dto.menuId <= FORBIDDEN_OP_MENU_ID_INDEX) {\n      // 系统内置功能不提供删除\n      throw new ApiException(10016);\n    }\n    // check\n    await this.menuService.check(dto);\n    if (dto.parentId === -1) {\n      dto.parentId = null;\n    }\n    const insertData: CreateMenuDto & { id: number } = {\n      ...dto,\n      id: dto.menuId,\n    };\n    await this.menuService.save(insertData);\n    if (dto.type === 2) {\n      // 如果是权限发生更改，则刷新所有在线用户的权限\n      await this.menuService.refreshOnlineUserPerms();\n    }\n  }\n\n  @ApiOperation({ summary: '删除菜单或权限' })\n  @Post('delete')\n  async delete(@Body() dto: DeleteMenuDto): Promise<void> {\n    // 68为内置init.sql中插入最后的索引编号\n    if (dto.menuId <= FORBIDDEN_OP_MENU_ID_INDEX) {\n      // 系统内置功能不提供删除\n      throw new ApiException(10016);\n    }\n    // 如果有子目录，一并删除\n    const childMenus = await this.menuService.findChildMenus(dto.menuId);\n    await this.menuService.deleteMenuItem(\n      flattenDeep([dto.menuId, childMenus]),\n    );\n    // 刷新在线用户权限\n    await this.menuService.refreshOnlineUserPerms();\n  }\n\n  @ApiOperation({ summary: '菜单或权限信息' })\n  @ApiOkResponse({ type: MenuItemAndParentInfoResult })\n  @Get('info')\n  async info(@Query() dto: InfoMenuDto): Promise<MenuItemAndParentInfoResult> {\n    return await this.menuService.getMenuItemAndParentInfo(dto.menuId);\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/menu/menu.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n  IsBoolean,\n  IsIn,\n  IsInt,\n  IsOptional,\n  IsString,\n  Min,\n  MinLength,\n  ValidateIf,\n} from 'class-validator';\n\n/**\n * 增加菜单\n */\nexport class CreateMenuDto {\n  @ApiProperty({ description: '菜单类型' })\n  @IsIn([0, 1, 2])\n  type: number;\n\n  @ApiProperty({ description: '父级菜单' })\n  @IsInt()\n  parentId: number;\n\n  @ApiProperty({ description: '菜单或权限名称' })\n  @IsString()\n  @MinLength(2)\n  name: string;\n\n  @ApiProperty({ description: '排序' })\n  @IsInt()\n  @Min(0)\n  orderNum: number;\n\n  @ApiProperty({ description: '前端路由地址' })\n  @IsString()\n  @ValidateIf((o) => o.type !== 2)\n  router: string;\n\n  @ApiProperty({ description: '菜单是否显示', required: false, default: true })\n  @IsBoolean()\n  @ValidateIf((o) => o.type !== 2)\n  readonly isShow: boolean = true;\n\n  @ApiProperty({ description: '开启页面缓存', required: false, default: true })\n  @IsBoolean()\n  @ValidateIf((o) => o.type === 1)\n  readonly keepalive: boolean = true;\n\n  @ApiProperty({ description: '菜单图标', required: false })\n  @IsString()\n  @IsOptional()\n  @ValidateIf((o) => o.type !== 2)\n  icon: string;\n\n  @ApiProperty({ description: '对应权限' })\n  @IsString()\n  @IsOptional()\n  @ValidateIf((o) => o.type === 2)\n  perms: string;\n\n  @ApiProperty({ description: '菜单路由路径或外链' })\n  @ValidateIf((o) => o.type !== 2)\n  @IsString()\n  @IsOptional()\n  viewPath: string;\n}\n\nexport class UpdateMenuDto extends CreateMenuDto {\n  @ApiProperty({ description: '更新的菜单ID' })\n  @IsInt()\n  @Min(0)\n  menuId: number;\n}\n\n/**\n * 删除菜单\n */\nexport class DeleteMenuDto {\n  @ApiProperty({ description: '删除的菜单ID' })\n  @IsInt()\n  @Min(0)\n  menuId: number;\n}\n\n/**\n * 查询菜单\n */\nexport class InfoMenuDto {\n  @ApiProperty({ description: '查询的菜单ID' })\n  @IsInt()\n  @Min(0)\n  @Type(() => Number)\n  menuId: number;\n}\n"
  },
  {
    "path": "src/modules/admin/system/menu/menu.service.ts",
    "content": "import { Inject, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { concat, includes, isEmpty, uniq } from 'lodash';\nimport { ROOT_ROLE_ID } from 'src/modules/admin/admin.constants';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport SysMenu from 'src/entities/admin/sys-menu.entity';\nimport { IsNull, Not, Repository } from 'typeorm';\nimport { SysRoleService } from '../role/role.service';\nimport { MenuItemAndParentInfoResult } from './menu.class';\nimport { CreateMenuDto } from './menu.dto';\nimport { RedisService } from 'src/shared/services/redis.service';\n\n@Injectable()\nexport class SysMenuService {\n  constructor(\n    @InjectRepository(SysMenu) private menuRepository: Repository<SysMenu>,\n    private redisService: RedisService,\n    @Inject(ROOT_ROLE_ID) private rootRoleId: number,\n    private roleService: SysRoleService,\n  ) {}\n\n  /**\n   * 获取所有菜单\n   */\n  async list(): Promise<SysMenu[]> {\n    return await this.menuRepository.find();\n  }\n\n  /**\n   * 保存或新增菜单\n   */\n  async save(menu: CreateMenuDto & { id?: number }): Promise<void> {\n    await this.menuRepository.save(menu);\n  }\n\n  /**\n   * 根据角色获取所有菜单\n   */\n  async getMenus(uid: number): Promise<SysMenu[]> {\n    const roleIds = await this.roleService.getRoleIdByUser(uid);\n    let menus: SysMenu[] = [];\n    if (includes(roleIds, this.rootRoleId)) {\n      // root find all\n      menus = await this.menuRepository.find({\n        order: { id: 'ASC', orderNum: 'DESC' },\n      });\n    } else {\n      // [ 1, 2, 3 ] role find\n      menus = await this.menuRepository\n        .createQueryBuilder('menu')\n        .innerJoinAndSelect(\n          'sys_role_menu',\n          'role_menu',\n          'menu.id = role_menu.menu_id',\n        )\n        .andWhere('role_menu.role_id IN (:...roldIds)', { roldIds: roleIds })\n        .orderBy('menu.order_num', 'DESC')\n        .orderBy('menu.id', 'ASC')\n        .getMany();\n    }\n    return menus;\n  }\n\n  /**\n   * 检查菜单创建规则是否符合\n   */\n  async check(dto: CreateMenuDto): Promise<void | never> {\n    if (dto.type === 2 && dto.parentId === -1) {\n      // 无法直接创建权限，必须有ParentId\n      throw new ApiException(10005);\n    }\n    if (dto.type === 1 && dto.parentId !== -1) {\n      const parent = await this.getMenuItemInfo(dto.parentId);\n      if (isEmpty(parent)) {\n        throw new ApiException(10014);\n      }\n      if (parent && parent.type === 1) {\n        // 当前新增为菜单但父节点也为菜单时为非法操作\n        throw new ApiException(10006);\n      }\n    }\n  }\n\n  /**\n   * 查找当前菜单下的子菜单，目录以及菜单\n   */\n  async findChildMenus(mid: number): Promise<any> {\n    const allMenus: any = [];\n    const menus = await this.menuRepository.find({ where: { parentId: mid } });\n    // if (_.isEmpty(menus)) {\n    //   return allMenus;\n    // }\n    // const childMenus: any = [];\n    for (let i = 0; i < menus.length; i++) {\n      if (menus[i].type !== 2) {\n        // 子目录下是菜单或目录，继续往下级查找\n        const c = await this.findChildMenus(menus[i].id);\n        allMenus.push(c);\n      }\n      allMenus.push(menus[i].id);\n    }\n    return allMenus;\n  }\n\n  /**\n   * 获取某个菜单的信息\n   * @param mid menu id\n   */\n  async getMenuItemInfo(mid: number): Promise<SysMenu> {\n    const menu = await this.menuRepository.findOne({ where: { id: mid } });\n    return menu;\n  }\n\n  /**\n   * 获取某个菜单以及关联的父菜单的信息\n   */\n  async getMenuItemAndParentInfo(\n    mid: number,\n  ): Promise<MenuItemAndParentInfoResult> {\n    const menu = await this.menuRepository.findOne({ where: { id: mid } });\n    let parentMenu: SysMenu | undefined = undefined;\n    if (menu && menu.parentId) {\n      parentMenu = await this.menuRepository.findOne({\n        where: { id: menu.parentId },\n      });\n    }\n    return { menu, parentMenu };\n  }\n\n  /**\n   * 查找节点路由是否存在\n   */\n  async findRouterExist(router: string): Promise<boolean> {\n    const menus = await this.menuRepository.findOne({ where: { router } });\n    return !isEmpty(menus);\n  }\n\n  /**\n   * 获取当前用户的所有权限\n   */\n  async getPerms(uid: number): Promise<string[]> {\n    const roleIds = await this.roleService.getRoleIdByUser(uid);\n    let perms: any[] = [];\n    let result: any = null;\n    if (includes(roleIds, this.rootRoleId)) {\n      // root find all perms\n      result = await this.menuRepository.find({\n        where: { perms: Not(IsNull()), type: 2 },\n      });\n    } else {\n      result = await this.menuRepository\n        .createQueryBuilder('menu')\n        .innerJoinAndSelect(\n          'sys_role_menu',\n          'role_menu',\n          'menu.id = role_menu.menu_id',\n        )\n        .andWhere('role_menu.role_id IN (:...roldIds)', { roldIds: roleIds })\n        .andWhere('menu.type = 2')\n        .andWhere('menu.perms IS NOT NULL')\n        .getMany();\n    }\n    if (!isEmpty(result)) {\n      result.forEach((e) => {\n        perms = concat(perms, e.perms.split(','));\n      });\n      perms = uniq(perms);\n    }\n    return perms;\n  }\n\n  /**\n   * 删除多项菜单\n   */\n  async deleteMenuItem(mids: number[]): Promise<void> {\n    await this.menuRepository.delete(mids);\n  }\n\n  /**\n   * 刷新指定用户ID的权限\n   */\n  async refreshPerms(uid: number): Promise<void> {\n    const perms = await this.getPerms(uid);\n    const online = await this.redisService.getRedis().get(`admin:token:${uid}`);\n    if (online) {\n      // 判断是否在线\n      await this.redisService\n        .getRedis()\n        .set(`admin:perms:${uid}`, JSON.stringify(perms));\n    }\n  }\n\n  /**\n   * 刷新所有在线用户的权限\n   */\n  async refreshOnlineUserPerms(): Promise<void> {\n    const onlineUserIds: string[] = await this.redisService\n      .getRedis()\n      .keys('admin:token:*');\n    if (onlineUserIds && onlineUserIds.length > 0) {\n      for (let i = 0; i < onlineUserIds.length; i++) {\n        const uid = onlineUserIds[i].split('admin:token:')[1];\n        const perms = await this.getPerms(parseInt(uid));\n        await this.redisService\n          .getRedis()\n          .set(`admin:perms:${uid}`, JSON.stringify(perms));\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/online/online.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class OnlineUserInfo {\n  @ApiProperty({ description: '最近的一条登录日志ID' })\n  id: number;\n\n  @ApiProperty({ description: '登录IP' })\n  ip: string;\n\n  @ApiProperty({ description: '用户名' })\n  username: string;\n\n  @ApiProperty({ description: '是否当前' })\n  isCurrent: boolean;\n\n  @ApiProperty({ description: '登陆时间' })\n  time: string;\n\n  @ApiProperty({ description: '系统' })\n  os: string;\n\n  @ApiProperty({ description: '浏览器' })\n  browser: string;\n\n  @ApiProperty({ description: '是否禁用' })\n  disable: boolean;\n}\n"
  },
  {
    "path": "src/modules/admin/system/online/online.controller.ts",
    "content": "import { Body, Controller, Get, Post } from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport { ADMIN_PREFIX } from '../../admin.constants';\nimport { IAdminUser } from '../../admin.interface';\nimport { AdminUser } from '../../core/decorators/admin-user.decorator';\nimport { LogDisabled } from '../../core/decorators/log-disabled.decorator';\nimport { OnlineUserInfo } from './online.class';\nimport { KickDto } from './online.dto';\nimport { SysOnlineService } from './online.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('在线用户模块')\n@Controller('online')\nexport class SysOnlineController {\n  constructor(private onlineService: SysOnlineService) {}\n\n  @ApiOperation({ summary: '查询当前在线用户' })\n  @ApiOkResponse({ type: [OnlineUserInfo] })\n  @LogDisabled()\n  @Get('list')\n  async list(@AdminUser() user: IAdminUser): Promise<OnlineUserInfo[]> {\n    return await this.onlineService.listOnlineUser(user.uid);\n  }\n\n  @ApiOperation({ summary: '下线指定在线用户' })\n  @Post('kick')\n  async kick(\n    @Body() dto: KickDto,\n    @AdminUser() user: IAdminUser,\n  ): Promise<void> {\n    if (dto.id === user.uid) {\n      throw new ApiException(10012);\n    }\n    await this.onlineService.kickUser(dto.id, user.uid);\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/online/online.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsInt } from 'class-validator';\n\nexport class KickDto {\n  @ApiProperty({ description: '需要下线的角色ID' })\n  @IsInt()\n  id: number;\n}\n"
  },
  {
    "path": "src/modules/admin/system/online/online.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { JwtService } from '@nestjs/jwt';\nimport { InjectEntityManager } from '@nestjs/typeorm';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport { AdminWSGateway } from 'src/modules/ws/admin-ws.gateway';\nimport { EVENT_KICK } from 'src/modules/ws/ws.event';\nimport { EntityManager } from 'typeorm';\nimport { UAParser } from 'ua-parser-js';\nimport { SysUserService } from '../user/user.service';\nimport { OnlineUserInfo } from './online.class';\nimport { RemoteSocket } from 'socket.io';\n\n@Injectable()\nexport class SysOnlineService {\n  constructor(\n    @InjectEntityManager() private entityManager: EntityManager,\n    private userService: SysUserService,\n    private adminWsGateWay: AdminWSGateway,\n    private jwtService: JwtService,\n  ) {}\n\n  /**\n   * 罗列在线用户列表\n   */\n  async listOnlineUser(currentUid: number): Promise<OnlineUserInfo[]> {\n    const onlineSockets = await this.adminWsGateWay.socketServer.fetchSockets();\n    if (!onlineSockets || onlineSockets.length <= 0) {\n      return [];\n    }\n    const onlineIds = onlineSockets.map((socket) => {\n      const token = socket.handshake.query?.token as string;\n      return this.jwtService.verify(token).uid;\n    });\n    return await this.findLastLoginInfoList(onlineIds, currentUid);\n  }\n\n  /**\n   * 下线当前用户\n   */\n  async kickUser(uid: number, currentUid: number): Promise<void> {\n    const rootUserId = await this.userService.findRootUserId();\n    const currentUserInfo = await this.userService.getAccountInfo(currentUid);\n    if (uid === rootUserId) {\n      throw new ApiException(10013);\n    }\n    // reset redis keys\n    await this.userService.forbidden(uid);\n    // socket emit\n    const socket = await this.findSocketIdByUid(uid);\n    if (socket) {\n      // socket emit event\n      this.adminWsGateWay.socketServer\n        .to(socket.id)\n        .emit(EVENT_KICK, { operater: currentUserInfo.name });\n      // close socket\n      socket.disconnect();\n    }\n  }\n\n  /**\n   * 根据uid查找socketid\n   */\n  async findSocketIdByUid(uid: number): Promise<RemoteSocket<any, any>> {\n    const onlineSockets = await this.adminWsGateWay.socketServer.fetchSockets();\n    const socket = onlineSockets.find((socket) => {\n      const token = socket.handshake.query?.token as string;\n      const tokenUid = this.jwtService.verify(token).uid;\n      return tokenUid === uid;\n    });\n    return socket;\n  }\n\n  /**\n   * 根据用户id列表查找最近登录信息和用户信息\n   */\n  async findLastLoginInfoList(\n    ids: number[],\n    currentUid: number,\n  ): Promise<OnlineUserInfo[]> {\n    const rootUserId = await this.userService.findRootUserId();\n    const result = await this.entityManager.query(\n      `\n      SELECT sys_login_log.created_at, sys_login_log.ip, sys_login_log.ua, sys_user.id, sys_user.username, sys_user.name\n        FROM sys_login_log \n        INNER JOIN sys_user ON sys_login_log.user_id = sys_user.id \n        WHERE sys_login_log.created_at IN (SELECT MAX(created_at) as createdAt FROM sys_login_log GROUP BY user_id)\n          AND sys_user.id IN (?)\n      `,\n      [ids],\n    );\n    if (result) {\n      const parser = new UAParser();\n      return result.map((e) => {\n        const u = parser.setUA(e.ua).getResult();\n        return {\n          id: e.id,\n          ip: e.ip,\n          username: `${e.name}（${e.username}）`,\n          isCurrent: currentUid === e.id,\n          time: e.created_at,\n          os: `${u.os.name} ${u.os.version}`,\n          browser: `${u.browser.name} ${u.browser.version}`,\n          disable: currentUid === e.id || e.id === rootUserId,\n        };\n      });\n    }\n    return [];\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/param-config/param-config.controller.ts",
    "content": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { PageResult } from 'src/common/class/res.class';\nimport { PageOptionsDto } from 'src/common/dto/page.dto';\nimport SysConfig from 'src/entities/admin/sys-config.entity';\nimport { ADMIN_PREFIX } from '../../admin.constants';\nimport {\n  CreateParamConfigDto,\n  DeleteParamConfigDto,\n  InfoParamConfigDto,\n  UpdateParamConfigDto,\n} from './param-config.dto';\nimport { SysParamConfigService } from './param-config.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('参数配置模块')\n@Controller('param-config')\nexport class SysParamConfigController {\n  constructor(private paramConfigService: SysParamConfigService) {}\n\n  @ApiOperation({ summary: '分页获取参数配置列表' })\n  @ApiOkResponse({ type: [SysConfig] })\n  @Get('page')\n  async page(@Query() dto: PageOptionsDto): Promise<PageResult<SysConfig>> {\n    const list = await this.paramConfigService.getConfigListByPage(\n      dto.page - 1,\n      dto.limit,\n    );\n    const count = await this.paramConfigService.countConfigList();\n    return {\n      pagination: {\n        total: count,\n        size: dto.limit,\n        page: dto.page,\n      },\n      list,\n    };\n  }\n\n  @ApiOperation({ summary: '新增参数配置' })\n  @Post('add')\n  async add(@Body() dto: CreateParamConfigDto): Promise<void> {\n    await this.paramConfigService.isExistKey(dto.key);\n    await this.paramConfigService.add(dto);\n  }\n\n  @ApiOperation({ summary: '查询单个参数配置信息' })\n  @ApiOkResponse({ type: SysConfig })\n  @Get('info')\n  async info(@Query() dto: InfoParamConfigDto): Promise<SysConfig> {\n    return this.paramConfigService.findOne(dto.id);\n  }\n\n  @ApiOperation({ summary: '更新单个参数配置' })\n  @Post('update')\n  async update(@Body() dto: UpdateParamConfigDto): Promise<void> {\n    await this.paramConfigService.update(dto);\n  }\n\n  @ApiOperation({ summary: '删除指定的参数配置' })\n  @Post('delete')\n  async delete(@Body() dto: DeleteParamConfigDto): Promise<void> {\n    await this.paramConfigService.delete(dto.ids);\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/param-config/param-config.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsInt,\n  IsOptional,\n  IsString,\n  Min,\n  MinLength,\n} from 'class-validator';\n\nexport class CreateParamConfigDto {\n  @ApiProperty({ description: '参数名称' })\n  @IsString()\n  name: string;\n\n  @ApiProperty({ description: '参数键名' })\n  @IsString()\n  @MinLength(3)\n  key: string;\n\n  @ApiProperty({ description: '参数值' })\n  @IsString()\n  value: string;\n\n  @ApiProperty({ required: false, description: '备注' })\n  @IsString()\n  @IsOptional()\n  remark: string;\n}\n\nexport class UpdateParamConfigDto {\n  @ApiProperty({ description: '配置编号' })\n  @IsInt()\n  @Min(1)\n  id: number;\n\n  @ApiProperty({ description: '参数名称' })\n  @IsString()\n  name: string;\n\n  @ApiProperty({ description: '参数值' })\n  @IsString()\n  value: string;\n\n  @ApiProperty({ required: false, description: '备注' })\n  @IsString()\n  @IsOptional()\n  remark: string;\n}\n\nexport class DeleteParamConfigDto {\n  @ApiProperty({ description: '需要删除的配置id列表', type: [Number] })\n  @IsArray()\n  @ArrayNotEmpty()\n  ids: number[];\n}\n\nexport class InfoParamConfigDto {\n  @ApiProperty({ description: '需要查询的配置编号' })\n  @IsInt()\n  @Min(0)\n  @Type(() => Number)\n  id: number;\n}\n"
  },
  {
    "path": "src/modules/admin/system/param-config/param-config.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport SysConfig from 'src/entities/admin/sys-config.entity';\nimport { Repository } from 'typeorm';\nimport { CreateParamConfigDto, UpdateParamConfigDto } from './param-config.dto';\n\n@Injectable()\nexport class SysParamConfigService {\n  constructor(\n    @InjectRepository(SysConfig)\n    private configRepository: Repository<SysConfig>,\n  ) {}\n\n  /**\n   * 罗列所有配置\n   */\n  async getConfigListByPage(page: number, count: number): Promise<SysConfig[]> {\n    return this.configRepository.find({\n      order: {\n        id: 'ASC',\n      },\n      take: count,\n      skip: page * count,\n    });\n  }\n\n  /**\n   * 获取参数总数\n   */\n  async countConfigList(): Promise<number> {\n    return this.configRepository.count();\n  }\n\n  /**\n   * 新增\n   */\n  async add(dto: CreateParamConfigDto): Promise<void> {\n    await this.configRepository.insert(dto);\n  }\n\n  /**\n   * 更新\n   */\n  async update(dto: UpdateParamConfigDto): Promise<void> {\n    await this.configRepository.update(\n      { id: dto.id },\n      { name: dto.name, value: dto.value, remark: dto.remark },\n    );\n  }\n\n  /**\n   * 删除\n   */\n  async delete(ids: number[]): Promise<void> {\n    await this.configRepository.delete(ids);\n  }\n\n  /**\n   * 查询单个\n   */\n  async findOne(id: number): Promise<SysConfig> {\n    return await this.configRepository.findOne({ where: { id } });\n  }\n\n  async isExistKey(key: string): Promise<void | never> {\n    const result = await this.configRepository.findOne({ where: { key } });\n    if (result) {\n      throw new ApiException(10021);\n    }\n  }\n\n  async findValueByKey(key: string): Promise<string | null> {\n    const result = await this.configRepository.findOne({\n      where: { key },\n      select: ['value'],\n    });\n    if (result) {\n      return result.value;\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/role/role.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport SysRoleDepartment from 'src/entities/admin/sys-role-department.entity';\nimport SysRoleMenu from 'src/entities/admin/sys-role-menu.entity';\nimport SysRole from 'src/entities/admin/sys-role.entity';\n\nexport class RoleInfo {\n  @ApiProperty({\n    type: SysRole,\n  })\n  roleInfo: SysRole;\n\n  @ApiProperty({\n    type: [SysRoleMenu],\n  })\n  menus: SysRoleMenu[];\n\n  @ApiProperty({\n    type: [SysRoleDepartment],\n  })\n  depts: SysRoleDepartment[];\n}\n\nexport class CreatedRoleId {\n  roleId: number;\n}\n"
  },
  {
    "path": "src/modules/admin/system/role/role.controller.ts",
    "content": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n  ApiOperation,\n  ApiOkResponse,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { ADMIN_PREFIX } from 'src/modules/admin/admin.constants';\nimport { PageOptionsDto } from 'src/common/dto/page.dto';\nimport { PageResult } from 'src/common/class/res.class';\nimport SysRole from 'src/entities/admin/sys-role.entity';\nimport { SysRoleService } from './role.service';\nimport {\n  CreateRoleDto,\n  DeleteRoleDto,\n  InfoRoleDto,\n  UpdateRoleDto,\n} from './role.dto';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport { AdminUser } from '../../core/decorators/admin-user.decorator';\nimport { IAdminUser } from '../../admin.interface';\nimport { RoleInfo } from './role.class';\nimport { SysMenuService } from '../menu/menu.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('角色模块')\n@Controller('role')\nexport class SysRoleController {\n  constructor(\n    private roleService: SysRoleService,\n    private menuService: SysMenuService,\n  ) {}\n\n  @ApiOperation({ summary: '获取角色列表' })\n  @ApiOkResponse({ type: [SysRole] })\n  @Get('list')\n  async list(): Promise<SysRole[]> {\n    return await this.roleService.list();\n  }\n\n  @ApiOperation({ summary: '分页查询角色信息' })\n  @ApiOkResponse({ type: [SysRole] })\n  @Get('page')\n  async page(@Query() dto: PageOptionsDto): Promise<PageResult<SysRole>> {\n    const list = await this.roleService.page(dto.page - 1, dto.limit);\n    const count = await this.roleService.count();\n    return {\n      list,\n      pagination: {\n        size: dto.limit,\n        page: dto.page,\n        total: count,\n      },\n    };\n  }\n\n  @ApiOperation({ summary: '删除角色' })\n  @Post('delete')\n  async delete(@Body() dto: DeleteRoleDto): Promise<void> {\n    const count = await this.roleService.countUserIdByRole(dto.roleIds);\n    if (count > 0) {\n      throw new ApiException(10008);\n    }\n    await this.roleService.delete(dto.roleIds);\n    await this.menuService.refreshOnlineUserPerms();\n  }\n\n  @ApiOperation({ summary: '新增角色' })\n  @Post('add')\n  async add(\n    @Body() dto: CreateRoleDto,\n    @AdminUser() user: IAdminUser,\n  ): Promise<void> {\n    await this.roleService.add(dto, user.uid);\n  }\n\n  @ApiOperation({ summary: '更新角色' })\n  @Post('update')\n  async update(@Body() dto: UpdateRoleDto): Promise<void> {\n    await this.roleService.update(dto);\n    await this.menuService.refreshOnlineUserPerms();\n  }\n\n  @ApiOperation({ summary: '获取角色信息' })\n  @ApiOkResponse({ type: RoleInfo })\n  @Get('info')\n  async info(@Query() dto: InfoRoleDto): Promise<RoleInfo> {\n    return await this.roleService.info(dto.roleId);\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/role/role.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsInt,\n  IsOptional,\n  IsString,\n  Matches,\n  Min,\n  MinLength,\n} from 'class-validator';\n\nexport class DeleteRoleDto {\n  @ApiProperty({\n    description: '需要删除的角色ID列表',\n    type: [Number],\n  })\n  @IsArray()\n  @ArrayNotEmpty()\n  roleIds: number[];\n}\n\nexport class CreateRoleDto {\n  @ApiProperty({\n    description: '角色名称',\n  })\n  @IsString()\n  @MinLength(2)\n  name: string;\n\n  @ApiProperty({\n    description: '角色唯一标识',\n  })\n  @IsString()\n  @Matches(/^[a-z0-9A-Z]+$/)\n  label: string;\n\n  @ApiProperty({\n    description: '角色备注',\n    required: false,\n  })\n  @IsString()\n  @IsOptional()\n  remark: string;\n\n  @ApiProperty({\n    description: '关联菜单、权限编号',\n    required: false,\n  })\n  @IsOptional()\n  @IsArray()\n  menus: number[];\n\n  @ApiProperty({\n    description: '关联部门编号',\n    required: false,\n  })\n  @IsOptional()\n  @IsArray()\n  depts: number[];\n}\n\nexport class UpdateRoleDto extends CreateRoleDto {\n  @ApiProperty({\n    description: '关联部门编号',\n  })\n  @IsInt()\n  @Min(0)\n  roleId: number;\n}\n\nexport class InfoRoleDto {\n  @ApiProperty({\n    description: '需要查找的角色ID',\n  })\n  @IsInt()\n  @Min(0)\n  @Type(() => Number)\n  roleId: number;\n}\n"
  },
  {
    "path": "src/modules/admin/system/role/role.service.ts",
    "content": "import { Inject, Injectable } from '@nestjs/common';\nimport { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';\nimport { difference, filter, includes, isEmpty, map } from 'lodash';\nimport SysRoleDepartment from 'src/entities/admin/sys-role-department.entity';\nimport SysRoleMenu from 'src/entities/admin/sys-role-menu.entity';\nimport SysRole from 'src/entities/admin/sys-role.entity';\nimport SysUserRole from 'src/entities/admin/sys-user-role.entity';\nimport { EntityManager, In, Not, Repository } from 'typeorm';\nimport { CreateRoleDto, UpdateRoleDto } from './role.dto';\nimport { CreatedRoleId, RoleInfo } from './role.class';\nimport { ROOT_ROLE_ID } from 'src/modules/admin/admin.constants';\n\n@Injectable()\nexport class SysRoleService {\n  constructor(\n    @InjectRepository(SysRole) private roleRepository: Repository<SysRole>,\n    @InjectRepository(SysRoleMenu)\n    private roleMenuRepository: Repository<SysRoleMenu>,\n    @InjectRepository(SysRoleDepartment)\n    private roleDepartmentRepository: Repository<SysRoleDepartment>,\n    @InjectRepository(SysUserRole)\n    private userRoleRepository: Repository<SysUserRole>,\n    @InjectEntityManager() private entityManager: EntityManager,\n    @Inject(ROOT_ROLE_ID) private rootRoleId: number,\n  ) {}\n\n  /**\n   * 列举所有角色：除去超级管理员\n   */\n  async list(): Promise<SysRole[]> {\n    const result = await this.roleRepository.find({\n      where: { id: Not(this.rootRoleId) },\n    });\n    return result;\n  }\n\n  /**\n   * 列举所有角色条数：除去超级管理员\n   */\n  async count(): Promise<number> {\n    const count = await this.roleRepository.count({\n      where: { id: Not(this.rootRoleId) },\n    });\n    return count;\n  }\n\n  /**\n   * 根据角色获取角色信息\n   */\n  async info(rid: number): Promise<RoleInfo> {\n    const roleInfo = await this.roleRepository.findOne({ where: { id: rid } });\n    const menus = await this.roleMenuRepository.find({\n      where: { roleId: rid },\n    });\n    const depts = await this.roleDepartmentRepository.find({\n      where: { roleId: rid },\n    });\n    return { roleInfo, menus, depts };\n  }\n\n  /**\n   * 根据角色Id数组删除\n   */\n  async delete(roleIds: number[]): Promise<void> {\n    if (includes(roleIds, this.rootRoleId)) {\n      throw new Error('Not Support Delete Root');\n    }\n    await this.entityManager.transaction(async (manager) => {\n      await manager.delete(SysRole, roleIds);\n      await manager.delete(SysRoleMenu, { roleId: In(roleIds) });\n      await manager.delete(SysRoleDepartment, { roleId: In(roleIds) });\n    });\n  }\n\n  /**\n   * 增加角色\n   */\n  async add(param: CreateRoleDto, uid: number): Promise<CreatedRoleId> {\n    const { name, label, remark, menus, depts } = param;\n    const role = await this.roleRepository.insert({\n      name,\n      label,\n      remark,\n      userId: `${uid}`,\n    });\n    const { identifiers } = role;\n    const roleId = parseInt(identifiers[0].id);\n    if (menus && menus.length > 0) {\n      // 关联菜单\n      const insertRows = menus.map((m) => {\n        return {\n          roleId,\n          menuId: m,\n        };\n      });\n      await this.roleMenuRepository.insert(insertRows);\n    }\n    if (depts && depts.length > 0) {\n      // 关联部门\n      const insertRows = depts.map((d) => {\n        return {\n          roleId,\n          departmentId: d,\n        };\n      });\n      await this.roleDepartmentRepository.insert(insertRows);\n    }\n    return { roleId };\n  }\n\n  /**\n   * 更新角色信息\n   */\n  async update(param: UpdateRoleDto): Promise<SysRole> {\n    const { roleId, name, label, remark, menus, depts } = param;\n    const role = await this.roleRepository.save({\n      id: roleId,\n      name,\n      label,\n      remark,\n    });\n    const originDeptRows = await this.roleDepartmentRepository.find({\n      where: { roleId },\n    });\n    const originMenuRows = await this.roleMenuRepository.find({\n      where: { roleId },\n    });\n    const originMenuIds = originMenuRows.map((e) => {\n      return e.menuId;\n    });\n    const originDeptIds = originDeptRows.map((e) => {\n      return e.departmentId;\n    });\n    // 开始对比差异\n    const insertMenusRowIds = difference(menus, originMenuIds);\n    const deleteMenusRowIds = difference(originMenuIds, menus);\n    const insertDeptRowIds = difference(depts, originDeptIds);\n    const deleteDeptRowIds = difference(originDeptIds, depts);\n    // using transaction\n    await this.entityManager.transaction(async (manager) => {\n      // 菜单\n      if (insertMenusRowIds.length > 0) {\n        // 有条目更新\n        const insertRows = insertMenusRowIds.map((e) => {\n          return {\n            roleId,\n            menuId: e,\n          };\n        });\n        await manager.insert(SysRoleMenu, insertRows);\n      }\n      if (deleteMenusRowIds.length > 0) {\n        // 有条目需要删除\n        const realDeleteRowIds = filter(originMenuRows, (e) => {\n          return includes(deleteMenusRowIds, e.menuId);\n        }).map((e) => {\n          return e.id;\n        });\n        await manager.delete(SysRoleMenu, realDeleteRowIds);\n      }\n      // 部门\n      if (insertDeptRowIds.length > 0) {\n        // 有条目更新\n        const insertRows = insertDeptRowIds.map((e) => {\n          return {\n            roleId,\n            departmentId: e,\n          };\n        });\n        await manager.insert(SysRoleDepartment, insertRows);\n      }\n      if (deleteDeptRowIds.length > 0) {\n        // 有条目需要删除\n        const realDeleteRowIds = filter(originDeptRows, (e) => {\n          return includes(deleteDeptRowIds, e.departmentId);\n        }).map((e) => {\n          return e.id;\n        });\n        await manager.delete(SysRoleDepartment, realDeleteRowIds);\n      }\n    });\n    return role;\n  }\n\n  /**\n   * 分页加载角色信息\n   */\n  async page(page: number, count: number): Promise<SysRole[]> {\n    const result = await this.roleRepository.find({\n      where: {\n        id: Not(this.rootRoleId),\n      },\n      order: {\n        id: 'ASC',\n      },\n      take: count,\n      skip: page * count,\n    });\n    return result;\n  }\n\n  /**\n   * 根据用户id查找角色信息\n   */\n  async getRoleIdByUser(id: number): Promise<number[]> {\n    const result = await this.userRoleRepository.find({\n      where: {\n        userId: id,\n      },\n    });\n    if (!isEmpty(result)) {\n      return map(result, (v) => {\n        return v.roleId;\n      });\n    }\n    return [];\n  }\n\n  /**\n   * 根据角色ID列表查找关联用户ID\n   */\n  async countUserIdByRole(ids: number[]): Promise<number | never> {\n    if (includes(ids, this.rootRoleId)) {\n      throw new Error('Not Support Delete Root');\n    }\n    return await this.userRoleRepository.count({ where: { roleId: In(ids) } });\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/serve/serve.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class Runtime {\n  @ApiProperty({ description: '系统' })\n  os?: string;\n\n  @ApiProperty({ description: '服务器架构' })\n  arch?: string;\n\n  @ApiProperty({ description: 'Node版本' })\n  nodeVersion?: string;\n\n  @ApiProperty({ description: 'Npm版本' })\n  npmVersion?: string;\n}\n\nexport class CoreLoad {\n  @ApiProperty({ description: '当前CPU资源消耗' })\n  rawLoad?: number;\n\n  @ApiProperty({ description: '当前空闲CPU资源' })\n  rawLoadIdle?: number;\n}\n\n// Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz\nexport class Cpu {\n  @ApiProperty({ description: '制造商 e.g. Intel(R)' })\n  manufacturer?: string;\n\n  @ApiProperty({ description: '品牌\te.g. Core(TM)2 Duo' })\n  brand?: string;\n\n  @ApiProperty({ description: '物理核心数' })\n  physicalCores?: number;\n\n  @ApiProperty({ description: '型号' })\n  model?: string;\n\n  @ApiProperty({ description: '速度 in GHz e.g. 3.4' })\n  speed?: number;\n\n  @ApiProperty({ description: 'CPU资源消耗 原始滴答' })\n  rawCurrentLoad?: number;\n\n  @ApiProperty({ description: '空闲CPU资源 原始滴答' })\n  rawCurrentLoadIdle?: number;\n\n  @ApiProperty({ description: 'cpu资源消耗', type: [CoreLoad] })\n  coresLoad?: CoreLoad[];\n}\n\nexport class Disk {\n  @ApiProperty({ description: '磁盘空间大小 (bytes)' })\n  size?: number;\n\n  @ApiProperty({ description: '已使用磁盘空间 (bytes)' })\n  used?: number;\n\n  @ApiProperty({ description: '可用磁盘空间 (bytes)' })\n  available?: number;\n}\n\nexport class Memory {\n  @ApiProperty({ description: 'total memory in bytes' })\n  total?: number;\n\n  @ApiProperty({ description: '可用内存' })\n  available?: number;\n}\n\n/**\n * 系统信息\n */\nexport class ServeStatInfo {\n  @ApiProperty({ description: '运行环境', type: Runtime })\n  runtime?: Runtime;\n\n  @ApiProperty({ description: 'CPU信息', type: Cpu })\n  cpu?: Cpu;\n\n  @ApiProperty({ description: '磁盘信息', type: Disk })\n  disk?: Disk;\n\n  @ApiProperty({ description: '内存信息', type: Memory })\n  memory?: Memory;\n}\n"
  },
  {
    "path": "src/modules/admin/system/serve/serve.controller.ts",
    "content": "import { Controller, Get } from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { ADMIN_PREFIX } from '../../admin.constants';\nimport { PermissionOptional } from '../../core/decorators/permission-optional.decorator';\nimport { ServeStatInfo } from './serve.class';\nimport { SysServeService } from './serve.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('服务监控')\n@Controller('serve')\nexport class SysServeController {\n  constructor(private serveService: SysServeService) {}\n\n  @ApiOperation({ summary: '获取服务器运行信息' })\n  @ApiOkResponse({ type: ServeStatInfo })\n  @PermissionOptional()\n  @Get('stat')\n  async stat(): Promise<ServeStatInfo> {\n    return await this.serveService.getServeStat();\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/serve/serve.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport * as si from 'systeminformation';\nimport { Disk, ServeStatInfo } from './serve.class';\n\n@Injectable()\nexport class SysServeService {\n  /**\n   * 获取服务器信息\n   */\n  async getServeStat(): Promise<ServeStatInfo> {\n    const versions = await si.versions('node, npm');\n    const osinfo = await si.osInfo();\n    const cpuinfo = await si.cpu();\n    const currentLoadinfo = await si.currentLoad();\n\n    // 计算总空间\n    const diskListInfo = await si.fsSize();\n    const diskinfo = new Disk();\n    diskinfo.size = diskListInfo[0].size;\n    diskinfo.available = diskListInfo[0].available;\n    diskinfo.used = 0;\n    diskListInfo.forEach((d) => {\n      diskinfo.used += d.used;\n    });\n\n    const meminfo = await si.mem();\n\n    return {\n      runtime: {\n        npmVersion: versions.npm,\n        nodeVersion: versions.node,\n        os: osinfo.platform,\n        arch: osinfo.arch,\n      },\n      cpu: {\n        manufacturer: cpuinfo.manufacturer,\n        brand: cpuinfo.brand,\n        physicalCores: cpuinfo.physicalCores,\n        model: cpuinfo.model,\n        speed: cpuinfo.speed,\n        rawCurrentLoad: currentLoadinfo.rawCurrentLoad,\n        rawCurrentLoadIdle: currentLoadinfo.rawCurrentLoadIdle,\n        coresLoad: currentLoadinfo.cpus.map((e) => {\n          return {\n            rawLoad: e.rawLoad,\n            rawLoadIdle: e.rawLoadIdle,\n          };\n        }),\n      },\n      disk: diskinfo,\n      memory: {\n        total: meminfo.total,\n        available: meminfo.available,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/system.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport {\n  ROOT_ROLE_ID,\n  SYS_TASK_QUEUE_NAME,\n  SYS_TASK_QUEUE_PREFIX,\n} from 'src/modules/admin/admin.constants';\nimport SysDepartment from 'src/entities/admin/sys-department.entity';\nimport SysLoginLog from 'src/entities/admin/sys-login-log.entity';\nimport SysMenu from 'src/entities/admin/sys-menu.entity';\nimport SysRoleDepartment from 'src/entities/admin/sys-role-department.entity';\nimport SysRoleMenu from 'src/entities/admin/sys-role-menu.entity';\nimport SysRole from 'src/entities/admin/sys-role.entity';\nimport SysTaskLog from 'src/entities/admin/sys-task-log.entity';\nimport SysTask from 'src/entities/admin/sys-task.entity';\nimport SysUserRole from 'src/entities/admin/sys-user-role.entity';\nimport SysUser from 'src/entities/admin/sys-user.entity';\nimport { rootRoleIdProvider } from '../core/provider/root-role-id.provider';\nimport { SysDeptController } from './dept/dept.controller';\nimport { SysDeptService } from './dept/dept.service';\nimport { SysLogController } from './log/log.controller';\nimport { SysLogService } from './log/log.service';\nimport { SysMenuController } from './menu/menu.controller';\nimport { SysMenuService } from './menu/menu.service';\nimport { SysRoleController } from './role/role.controller';\nimport { SysRoleService } from './role/role.service';\nimport { SysUserController } from './user/user.controller';\nimport { SysUserService } from './user/user.service';\nimport { BullModule } from '@nestjs/bull';\nimport { SysTaskController } from './task/task.controller';\nimport { SysTaskService } from './task/task.service';\nimport { ConfigModule, ConfigService } from '@nestjs/config';\nimport { SysTaskConsumer } from './task/task.processor';\nimport { SysOnlineController } from './online/online.controller';\nimport { SysOnlineService } from './online/online.service';\nimport { WSModule } from 'src/modules/ws/ws.module';\nimport SysConfig from 'src/entities/admin/sys-config.entity';\nimport { SysParamConfigController } from './param-config/param-config.controller';\nimport { SysParamConfigService } from './param-config/param-config.service';\nimport { SysServeController } from './serve/serve.controller';\nimport { SysServeService } from './serve/serve.service';\n\n@Module({\n  imports: [\n    TypeOrmModule.forFeature([\n      SysUser,\n      SysDepartment,\n      SysUserRole,\n      SysMenu,\n      SysRoleMenu,\n      SysRole,\n      SysRoleDepartment,\n      SysLoginLog,\n      SysTask,\n      SysTaskLog,\n      SysConfig,\n    ]),\n    BullModule.registerQueueAsync({\n      name: SYS_TASK_QUEUE_NAME,\n      imports: [ConfigModule],\n      useFactory: (configService: ConfigService) => ({\n        redis: {\n          host: configService.get<string>('redis.host'),\n          port: configService.get<number>('redis.port'),\n          password: configService.get<string>('redis.password'),\n          db: configService.get<number>('redis.db'),\n        },\n        prefix: SYS_TASK_QUEUE_PREFIX,\n      }),\n      inject: [ConfigService],\n    }),\n    WSModule,\n  ],\n  controllers: [\n    SysUserController,\n    SysRoleController,\n    SysMenuController,\n    SysDeptController,\n    SysLogController,\n    SysTaskController,\n    SysOnlineController,\n    SysParamConfigController,\n    SysServeController,\n  ],\n  providers: [\n    rootRoleIdProvider(),\n    SysUserService,\n    SysRoleService,\n    SysMenuService,\n    SysDeptService,\n    SysLogService,\n    SysTaskService,\n    SysTaskConsumer,\n    SysOnlineService,\n    SysParamConfigService,\n    SysServeService,\n  ],\n  exports: [\n    ROOT_ROLE_ID,\n    TypeOrmModule,\n    SysUserService,\n    SysMenuService,\n    SysLogService,\n    SysOnlineService,\n  ],\n})\nexport class SystemModule {}\n"
  },
  {
    "path": "src/modules/admin/system/task/task.controller.ts",
    "content": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { isEmpty } from 'lodash';\nimport { PageResult } from 'src/common/class/res.class';\nimport { PageOptionsDto } from 'src/common/dto/page.dto';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport SysTask from 'src/entities/admin/sys-task.entity';\nimport { ADMIN_PREFIX } from '../../admin.constants';\nimport { CheckIdTaskDto, CreateTaskDto, UpdateTaskDto } from './task.dto';\nimport { SysTaskService } from './task.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('任务调度模块')\n@Controller('task')\nexport class SysTaskController {\n  constructor(private taskService: SysTaskService) {}\n\n  @ApiOperation({ summary: '获取任务列表' })\n  @ApiOkResponse({ type: [SysTask] })\n  @Get('page')\n  async page(@Query() dto: PageOptionsDto): Promise<PageResult<SysTask>> {\n    const list = await this.taskService.page(dto.page - 1, dto.limit);\n    const count = await this.taskService.count();\n    return {\n      list,\n      pagination: {\n        total: count,\n        size: dto.limit,\n        page: dto.page,\n      },\n    };\n  }\n\n  @ApiOperation({ summary: '添加任务' })\n  @Post('add')\n  async add(@Body() dto: CreateTaskDto): Promise<void> {\n    const serviceCall = dto.service.split('.');\n    await this.taskService.checkHasMissionMeta(serviceCall[0], serviceCall[1]);\n    await this.taskService.addOrUpdate(dto);\n  }\n\n  @ApiOperation({ summary: '更新任务' })\n  @Post('update')\n  async update(@Body() dto: UpdateTaskDto): Promise<void> {\n    const serviceCall = dto.service.split('.');\n    await this.taskService.checkHasMissionMeta(serviceCall[0], serviceCall[1]);\n    await this.taskService.addOrUpdate(dto);\n  }\n\n  @ApiOperation({ summary: '查询任务详细信息' })\n  @ApiOkResponse({ type: SysTask })\n  @Get('info')\n  async info(@Query() dto: CheckIdTaskDto): Promise<SysTask> {\n    return await this.taskService.info(dto.id);\n  }\n\n  @ApiOperation({ summary: '手动执行一次任务' })\n  @Post('once')\n  async once(@Body() dto: CheckIdTaskDto): Promise<void> {\n    const task = await this.taskService.info(dto.id);\n    if (!isEmpty(task)) {\n      await this.taskService.once(task);\n    } else {\n      throw new ApiException(10020);\n    }\n  }\n\n  @ApiOperation({ summary: '停止任务' })\n  @Post('stop')\n  async stop(@Body() dto: CheckIdTaskDto): Promise<void> {\n    const task = await this.taskService.info(dto.id);\n    if (!isEmpty(task)) {\n      await this.taskService.stop(task);\n    } else {\n      throw new ApiException(10020);\n    }\n  }\n\n  @ApiOperation({ summary: '启动任务' })\n  @Post('start')\n  async start(@Body() dto: CheckIdTaskDto): Promise<void> {\n    const task = await this.taskService.info(dto.id);\n    if (!isEmpty(task)) {\n      await this.taskService.start(task);\n    } else {\n      throw new ApiException(10020);\n    }\n  }\n\n  @ApiOperation({ summary: '删除任务' })\n  @Post('delete')\n  async delete(@Body() dto: CheckIdTaskDto): Promise<void> {\n    const task = await this.taskService.info(dto.id);\n    if (!isEmpty(task)) {\n      await this.taskService.delete(task);\n    } else {\n      throw new ApiException(10020);\n    }\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/task/task.dto.ts",
    "content": "import * as parser from 'cron-parser';\nimport { isEmpty } from 'lodash';\nimport {\n  IsDateString,\n  IsIn,\n  IsInt,\n  IsOptional,\n  IsString,\n  MaxLength,\n  Min,\n  MinLength,\n  Validate,\n  ValidateIf,\n  ValidationArguments,\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from 'class-validator';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\n\n// cron 表达式验证，bull lib下引用了cron-parser\n@ValidatorConstraint({ name: 'isCronExpression', async: false })\nexport class IsCronExpression implements ValidatorConstraintInterface {\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  validate(value: string, args: ValidationArguments) {\n    try {\n      if (isEmpty(value)) {\n        throw new Error('cron expression is empty');\n      }\n      parser.parseExpression(value);\n      return true;\n    } catch (e) {\n      return false;\n    }\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  defaultMessage(_args: ValidationArguments) {\n    // here you can provide default error message if validation failed\n    return 'this cron expression ($value) invalid';\n  }\n}\n\nexport class CreateTaskDto {\n  @ApiProperty({ description: '任务名称' })\n  @IsString()\n  @MinLength(2)\n  @MaxLength(50)\n  name: string;\n\n  @ApiProperty({ description: '调用的服务' })\n  @IsString()\n  @MinLength(1)\n  service: string;\n\n  @ApiProperty({ description: '任务类别：cron | interval' })\n  @IsIn([0, 1])\n  type: number;\n\n  @ApiProperty({ description: '任务状态' })\n  @IsIn([0, 1])\n  status: number;\n\n  @ApiPropertyOptional({ description: '开始时间', type: Date })\n  @IsDateString()\n  @ValidateIf((o) => !isEmpty(o.startTime))\n  startTime: string;\n\n  @ApiPropertyOptional({ description: '结束时间', type: Date })\n  @IsDateString()\n  @ValidateIf((o) => !isEmpty(o.endTime))\n  endTime: string;\n\n  @ApiPropertyOptional({ description: '限制执行次数，负数则无限制' })\n  @IsInt()\n  @IsOptional()\n  readonly limit: number = -1;\n\n  @ApiProperty({ description: 'cron表达式' })\n  @Validate(IsCronExpression)\n  @ValidateIf((o) => o.type === 0)\n  cron: string;\n\n  @ApiProperty({ description: '执行间隔，毫秒单位' })\n  @IsInt()\n  @Min(100)\n  @ValidateIf((o) => o.type === 1)\n  every: number;\n\n  @ApiPropertyOptional({ description: '执行参数' })\n  @IsString()\n  @IsOptional()\n  data: string;\n\n  @ApiPropertyOptional({ description: '任务备注' })\n  @IsOptional()\n  @IsString()\n  remark: string;\n}\n\nexport class UpdateTaskDto extends CreateTaskDto {\n  @ApiProperty({ description: '需要更新的任务ID' })\n  @IsInt()\n  @Min(0)\n  id: number;\n}\n\nexport class CheckIdTaskDto {\n  @ApiProperty({ description: '任务ID' })\n  @IsInt()\n  @Min(0)\n  @Type(() => Number)\n  id: number;\n}\n"
  },
  {
    "path": "src/modules/admin/system/task/task.processor.ts",
    "content": "import { OnQueueCompleted, Process, Processor } from '@nestjs/bull';\nimport { Job } from 'bull';\nimport { SYS_TASK_QUEUE_NAME } from '../../admin.constants';\nimport { SysLogService } from '../log/log.service';\nimport { SysTaskService } from './task.service';\n\nexport interface ExecuteData {\n  id: number;\n  args?: string | null;\n  service: string;\n}\n\n@Processor(SYS_TASK_QUEUE_NAME)\nexport class SysTaskConsumer {\n  constructor(\n    private taskService: SysTaskService,\n    private taskLogService: SysLogService,\n  ) {}\n\n  @Process()\n  async handle(job: Job<ExecuteData>): Promise<void> {\n    const startTime = Date.now();\n    const { data } = job;\n    try {\n      await this.taskService.callService(data.service, data.args);\n      const timing = Date.now() - startTime;\n      // 任务执行成功\n      await this.taskLogService.recordTaskLog(data.id, 1, timing);\n    } catch (e) {\n      const timing = Date.now() - startTime;\n      // 执行失败\n      await this.taskLogService.recordTaskLog(data.id, 0, timing, `${e}`);\n    }\n  }\n\n  @OnQueueCompleted()\n  onCompleted(job: Job<ExecuteData>) {\n    this.taskService.updateTaskCompleteStatus(job.data.id);\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/task/task.service.ts",
    "content": "import { InjectQueue } from '@nestjs/bull';\nimport { Injectable, OnModuleInit } from '@nestjs/common';\nimport { ModuleRef, Reflector } from '@nestjs/core';\nimport { UnknownElementException } from '@nestjs/core/errors/exceptions/unknown-element.exception';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Queue } from 'bull';\nimport { isEmpty } from 'lodash';\nimport { MISSION_KEY_METADATA } from 'src/common/contants/decorator.contants';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport SysTask from 'src/entities/admin/sys-task.entity';\nimport { LoggerService } from 'src/shared/logger/logger.service';\nimport { RedisService } from 'src/shared/services/redis.service';\nimport { Repository } from 'typeorm';\nimport {\n  SYS_TASK_QUEUE_NAME,\n  SYS_TASK_QUEUE_PREFIX,\n} from '../../admin.constants';\nimport { CreateTaskDto, UpdateTaskDto } from './task.dto';\n\n@Injectable()\nexport class SysTaskService implements OnModuleInit {\n  constructor(\n    @InjectRepository(SysTask) private taskRepository: Repository<SysTask>,\n    @InjectQueue(SYS_TASK_QUEUE_NAME) private taskQueue: Queue,\n    private moduleRef: ModuleRef,\n    private reflector: Reflector,\n    private redisService: RedisService,\n    private logger: LoggerService,\n  ) {}\n\n  /**\n   * module init\n   */\n  async onModuleInit() {\n    await this.initTask();\n  }\n\n  /**\n   * 初始化任务，系统启动前调用\n   */\n  async initTask(): Promise<void> {\n    const initKey = `${SYS_TASK_QUEUE_PREFIX}:init`;\n    // 防止重复初始化\n    const result = await this.redisService\n      .getRedis()\n      .multi()\n      .setnx(initKey, new Date().getTime())\n      .expire(initKey, 60 * 30)\n      .exec();\n    if (result[0][1] === 0) {\n      // 存在锁则直接跳过防止重复初始化\n      this.logger.log('Init task is lock', SysTaskService.name);\n      return;\n    }\n    const jobs = await this.taskQueue.getJobs([\n      'active',\n      'delayed',\n      'failed',\n      'paused',\n      'waiting',\n      'completed',\n    ]);\n    for (let i = 0; i < jobs.length; i++) {\n      // 先移除所有已存在的任务\n      await jobs[i].remove();\n    }\n    // 查找所有需要运行的任务\n    const tasks = await this.taskRepository.find({ where: { status: 1 } });\n    if (tasks && tasks.length > 0) {\n      for (const t of tasks) {\n        await this.start(t);\n      }\n    }\n    // 启动后释放锁\n    await this.redisService.getRedis().del(initKey);\n  }\n\n  /**\n   * 分页查询\n   */\n  async page(page: number, count: number): Promise<SysTask[]> {\n    const result = await this.taskRepository.find({\n      order: {\n        id: 'ASC',\n      },\n      take: count,\n      skip: page * count,\n    });\n    return result;\n  }\n\n  /**\n   * count task\n   */\n  async count(): Promise<number> {\n    return await this.taskRepository.count();\n  }\n\n  /**\n   * task info\n   */\n  async info(id: number): Promise<SysTask> {\n    return await this.taskRepository.findOne({ where: { id } });\n  }\n\n  /**\n   * delete task\n   */\n  async delete(task: SysTask): Promise<void> {\n    if (!task) {\n      throw new Error('Task is Empty');\n    }\n    await this.stop(task);\n    await this.taskRepository.delete(task.id);\n  }\n\n  /**\n   * 手动执行一次\n   */\n  async once(task: SysTask): Promise<void | never> {\n    if (task) {\n      await this.taskQueue.add(\n        { id: task.id, service: task.service, args: task.data },\n        { jobId: task.id, removeOnComplete: true, removeOnFail: true },\n      );\n    } else {\n      throw new Error('Task is Empty');\n    }\n  }\n\n  /**\n   * 添加任务\n   */\n  async addOrUpdate(param: CreateTaskDto | UpdateTaskDto): Promise<void> {\n    const result = await this.taskRepository.save(param);\n    const task = await this.info(result.id);\n    if (result.status === 0) {\n      await this.stop(task);\n    } else if (result.status === 1) {\n      await this.start(task);\n    }\n  }\n\n  /**\n   * 启动任务\n   */\n  async start(task: SysTask): Promise<void> {\n    if (!task) {\n      throw new Error('Task is Empty');\n    }\n    // 先停掉之前存在的任务\n    await this.stop(task);\n    let repeat: any;\n    if (task.type === 1) {\n      // 间隔 Repeat every millis (cron setting cannot be used together with this setting.)\n      repeat = {\n        every: task.every,\n      };\n    } else {\n      // cron\n      repeat = {\n        cron: task.cron,\n      };\n      // Start date when the repeat job should start repeating (only with cron).\n      if (task.startTime) {\n        repeat.startDate = task.startTime;\n      }\n      if (task.endTime) {\n        repeat.endDate = task.endTime;\n      }\n    }\n    if (task.limit > 0) {\n      repeat.limit = task.limit;\n    }\n    const job = await this.taskQueue.add(\n      { id: task.id, service: task.service, args: task.data },\n      { jobId: task.id, removeOnComplete: true, removeOnFail: true, repeat },\n    );\n    if (job && job.opts) {\n      await this.taskRepository.update(task.id, {\n        jobOpts: JSON.stringify(job.opts.repeat),\n        status: 1,\n      });\n    } else {\n      // update status to 0，标识暂停任务，因为启动失败\n      job && (await job.remove());\n      await this.taskRepository.update(task.id, { status: 0 });\n      throw new Error('Task Start failed');\n    }\n  }\n\n  /**\n   * 停止任务\n   */\n  async stop(task: SysTask): Promise<void> {\n    if (!task) {\n      throw new Error('Task is Empty');\n    }\n    const exist = await this.existJob(task.id.toString());\n    if (!exist) {\n      await this.taskRepository.update(task.id, { status: 0 });\n      return;\n    }\n    const jobs = await this.taskQueue.getJobs([\n      'active',\n      'delayed',\n      'failed',\n      'paused',\n      'waiting',\n      'completed',\n    ]);\n    for (let i = 0; i < jobs.length; i++) {\n      if (jobs[i].data.id === task.id) {\n        await jobs[i].remove();\n      }\n    }\n    await this.taskRepository.update(task.id, { status: 0 });\n    // if (task.jobOpts) {\n    //   await this.app.queue.sys.removeRepeatable(JSON.parse(task.jobOpts));\n    //   // update status\n    //   await this.getRepo().admin.sys.Task.update(task.id, { status: 0 });\n    // }\n  }\n\n  /**\n   * 查看队列中任务是否存在\n   */\n  async existJob(jobId: string): Promise<boolean> {\n    // https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md#queueremoverepeatablebykey\n    const jobs = await this.taskQueue.getRepeatableJobs();\n    const ids = jobs.map((e) => {\n      return e.id;\n    });\n    return ids.includes(jobId);\n  }\n\n  /**\n   * 更新是否已经完成，完成则移除该任务并修改状态\n   */\n  async updateTaskCompleteStatus(tid: number): Promise<void> {\n    const jobs = await this.taskQueue.getRepeatableJobs();\n    const task = await this.taskRepository.findOne({ where: { id: tid } });\n    // 如果下次执行时间小于当前时间，则表示已经执行完成。\n    for (const job of jobs) {\n      const currentTime = new Date().getTime();\n      if (job.id === tid.toString() && job.next < currentTime) {\n        // 如果下次执行时间小于当前时间，则表示已经执行完成。\n        await this.stop(task);\n        break;\n      }\n    }\n  }\n\n  /**\n   * 检测service是否有注解定义\n   * @param serviceName service\n   */\n  async checkHasMissionMeta(\n    nameOrInstance: string | unknown,\n    exec: string,\n  ): Promise<void | never> {\n    try {\n      let service: any;\n      if (typeof nameOrInstance === 'string') {\n        service = await this.moduleRef.get(nameOrInstance, { strict: false });\n      } else {\n        service = nameOrInstance;\n      }\n      // 所执行的任务不存在\n      if (!service || !(exec in service)) {\n        throw new ApiException(10102);\n      }\n      // 检测是否有Mission注解\n      const hasMission = this.reflector.get<boolean>(\n        MISSION_KEY_METADATA,\n        // https://github.com/nestjs/nest/blob/e5f0815da52ce22e5077c461fe881e89c4b5d640/packages/core/helpers/context-utils.ts#L90\n        service.constructor,\n      );\n      // 如果没有，则抛出错误\n      if (!hasMission) {\n        throw new ApiException(10101);\n      }\n    } catch (e) {\n      if (e instanceof UnknownElementException) {\n        // 任务不存在\n        throw new ApiException(10102);\n      } else {\n        // 其余错误则不处理，继续抛出\n        throw e;\n      }\n    }\n  }\n\n  /**\n   * 根据serviceName调用service，例如 SysLogService.clearReqLog\n   */\n  async callService(serviceName: string, args: string): Promise<void> {\n    if (serviceName) {\n      const arr = serviceName.split('.');\n      if (arr.length < 1) {\n        throw new Error('serviceName define error');\n      }\n      const methodName = arr[1];\n      const service = await this.moduleRef.get(arr[0], { strict: false });\n      // 安全注解检查\n      await this.checkHasMissionMeta(service, methodName);\n      if (isEmpty(args)) {\n        await service[methodName]();\n      } else {\n        // 参数安全判断\n        const parseArgs = this.safeParse(args);\n\n        if (Array.isArray(parseArgs)) {\n          // 数组形式则自动扩展成方法参数回掉\n          await service[methodName](...parseArgs);\n        } else {\n          await service[methodName](parseArgs);\n        }\n      }\n    }\n  }\n\n  safeParse(args: string): unknown | string {\n    try {\n      return JSON.parse(args);\n    } catch (e) {\n      return args;\n    }\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/user/user.class.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport SysUser from 'src/entities/admin/sys-user.entity';\n\nexport class AccountInfo {\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  nickName: string;\n\n  @ApiProperty()\n  email: string;\n\n  @ApiProperty()\n  phone: string;\n\n  @ApiProperty()\n  remark: string;\n\n  @ApiProperty()\n  headImg: string;\n}\n\nexport class PageSearchUserInfo {\n  @ApiProperty()\n  createdAt: string;\n\n  @ApiProperty()\n  departmentId: number;\n\n  @ApiProperty()\n  email: string;\n\n  @ApiProperty()\n  headImg: string;\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  nickName: string;\n\n  @ApiProperty()\n  phone: string;\n\n  @ApiProperty()\n  remark: string;\n\n  @ApiProperty()\n  status: number;\n\n  @ApiProperty()\n  updatedAt: string;\n\n  @ApiProperty()\n  username: string;\n\n  @ApiProperty()\n  departmentName: string;\n\n  @ApiProperty({\n    type: [String],\n  })\n  roleNames: string[];\n}\n\nexport class UserDetailInfo extends SysUser {\n  @ApiProperty({\n    description: '关联角色',\n  })\n  roles: number[];\n\n  @ApiProperty({\n    description: '关联部门名称',\n  })\n  departmentName: string;\n}\n"
  },
  {
    "path": "src/modules/admin/system/user/user.controller.ts",
    "content": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiSecurity,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { PageResult } from 'src/common/class/res.class';\nimport { ADMIN_PREFIX } from '../../admin.constants';\nimport { IAdminUser } from '../../admin.interface';\nimport { AdminUser } from '../../core/decorators/admin-user.decorator';\nimport {\n  CreateUserDto,\n  DeleteUserDto,\n  InfoUserDto,\n  PageSearchUserDto,\n  PasswordUserDto,\n  UpdateUserDto,\n} from './user.dto';\nimport { PageSearchUserInfo, UserDetailInfo } from './user.class';\nimport { SysUserService } from './user.service';\nimport { SysMenuService } from '../menu/menu.service';\n\n@ApiSecurity(ADMIN_PREFIX)\n@ApiTags('管理员模块')\n@Controller('user')\nexport class SysUserController {\n  constructor(\n    private userService: SysUserService,\n    private menuService: SysMenuService,\n  ) {}\n\n  @ApiOperation({\n    summary: '新增管理员',\n  })\n  @Post('add')\n  async add(@Body() dto: CreateUserDto): Promise<void> {\n    await this.userService.add(dto);\n  }\n\n  @ApiOperation({\n    summary: '查询管理员信息',\n  })\n  @ApiOkResponse({ type: UserDetailInfo })\n  @Get('info')\n  async info(@Query() dto: InfoUserDto): Promise<UserDetailInfo> {\n    return await this.userService.info(dto.userId);\n  }\n\n  @ApiOperation({\n    summary: '根据ID列表删除管理员',\n  })\n  @Post('delete')\n  async delete(@Body() dto: DeleteUserDto): Promise<void> {\n    await this.userService.delete(dto.userIds);\n    await this.userService.multiForbidden(dto.userIds);\n  }\n\n  @ApiOperation({\n    summary: '分页获取管理员列表',\n  })\n  @ApiOkResponse({ type: [PageSearchUserInfo] })\n  @Post('page')\n  async page(\n    @Body() dto: PageSearchUserDto,\n    @AdminUser() user: IAdminUser,\n  ): Promise<PageResult<PageSearchUserInfo>> {\n    const list = await this.userService.page(\n      user.uid,\n      dto.departmentIds,\n      dto.page - 1,\n      dto.limit,\n    );\n    const total = await this.userService.count(user.uid, dto.departmentIds);\n    return {\n      list,\n      pagination: {\n        total,\n        page: dto.page,\n        size: dto.limit,\n      },\n    };\n  }\n\n  @ApiOperation({\n    summary: '更新管理员信息',\n  })\n  @Post('update')\n  async update(@Body() dto: UpdateUserDto): Promise<void> {\n    await this.userService.update(dto);\n    await this.menuService.refreshPerms(dto.id);\n  }\n\n  @ApiOperation({\n    summary: '更改指定管理员密码',\n  })\n  @Post('password')\n  async password(@Body() dto: PasswordUserDto): Promise<void> {\n    await this.userService.forceUpdatePassword(dto.userId, dto.password);\n  }\n}\n"
  },
  {
    "path": "src/modules/admin/system/user/user.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n  ArrayMaxSize,\n  ArrayMinSize,\n  ArrayNotEmpty,\n  IsArray,\n  IsEmail,\n  IsIn,\n  IsInt,\n  IsOptional,\n  IsString,\n  Matches,\n  MaxLength,\n  Min,\n  MinLength,\n  ValidateIf,\n} from 'class-validator';\nimport { isEmpty } from 'lodash';\nimport { PageOptionsDto } from '../../../../common/dto/page.dto';\n\nexport class UpdateUserInfoDto {\n  @ApiProperty({\n    required: false,\n    description: '用户呢称',\n  })\n  @IsString()\n  @IsOptional()\n  nickName: string;\n\n  @ApiProperty({\n    required: false,\n    description: '用户邮箱',\n  })\n  @IsEmail()\n  @ValidateIf((o) => !isEmpty(o.email))\n  email: string;\n\n  @ApiProperty({\n    required: false,\n    description: '用户手机号',\n  })\n  @IsString()\n  @IsOptional()\n  phone: string;\n\n  @ApiProperty({\n    required: false,\n    description: '用户备注',\n  })\n  @IsString()\n  @IsOptional()\n  remark: string;\n}\n\nexport class UpdatePasswordDto {\n  @ApiProperty({\n    description: '更改前的密码',\n  })\n  @IsString()\n  @MinLength(6)\n  @Matches(/^[a-z0-9A-Z`~!#%^&*=+\\\\|{};:'\\\\\",<>/?]+$/)\n  originPassword: string;\n\n  @ApiProperty({\n    description: '更改后的密码',\n  })\n  @MinLength(6)\n  @Matches(/^[a-z0-9A-Z`~!#%^&*=+\\\\|{};:'\\\\\",<>/?]+$/)\n  newPassword: string;\n}\n\nexport class CreateUserDto {\n  @ApiProperty({\n    description: '所属部门编号',\n  })\n  @IsInt()\n  @Min(0)\n  departmentId: number;\n\n  @ApiProperty({\n    description: '用户姓名',\n  })\n  @IsString()\n  @MinLength(2)\n  name: string;\n\n  @ApiProperty({\n    description: '登录账号',\n  })\n  @IsString()\n  @Matches(/^[a-z0-9A-Z]+$/)\n  @MinLength(6)\n  @MaxLength(20)\n  username: string;\n\n  @ApiProperty({\n    description: '归属角色',\n    type: [Number],\n  })\n  @ArrayNotEmpty()\n  @ArrayMinSize(1)\n  @ArrayMaxSize(3)\n  roles: number[];\n\n  @ApiProperty({\n    required: false,\n    description: '呢称',\n  })\n  @IsString()\n  @IsOptional()\n  nickName: string;\n\n  @ApiProperty({\n    required: false,\n    description: '邮箱',\n  })\n  @IsEmail()\n  @ValidateIf((o) => !isEmpty(o.email))\n  email: string;\n\n  @ApiProperty({\n    required: false,\n    description: '手机号',\n  })\n  @IsString()\n  @IsOptional()\n  phone: string;\n\n  @ApiProperty({\n    required: false,\n    description: '备注',\n  })\n  @IsString()\n  @IsOptional()\n  remark: string;\n\n  @ApiProperty({\n    description: '状态',\n  })\n  @IsIn([0, 1])\n  status: number;\n}\n\nexport class UpdateUserDto extends CreateUserDto {\n  @ApiProperty({\n    description: '用户ID',\n  })\n  @IsInt()\n  @Min(0)\n  id: number;\n}\n\nexport class InfoUserDto {\n  @ApiProperty({\n    description: '用户ID',\n  })\n  @IsInt()\n  @Min(0)\n  @Type(() => Number)\n  userId: number;\n}\n\nexport class DeleteUserDto {\n  @ApiProperty({\n    description: '需要删除的用户ID列表',\n    type: [Number],\n  })\n  @IsArray()\n  @ArrayNotEmpty()\n  userIds: number[];\n}\n\nexport class PageSearchUserDto extends PageOptionsDto {\n  @ApiProperty({\n    required: false,\n    description: '部门列表',\n    type: [Number],\n  })\n  @IsArray()\n  @ArrayNotEmpty()\n  @IsOptional()\n  departmentIds: number[];\n}\n\nexport class PasswordUserDto {\n  @ApiProperty({\n    description: '管理员ID',\n  })\n  @IsInt()\n  @Min(0)\n  userId: number;\n\n  @ApiProperty({\n    description: '更改后的密码',\n  })\n  @Matches(/^[a-z0-9A-Z`~!#%^&*=+\\\\|{};:'\\\\\",<>/?]+$/)\n  password: string;\n}\n"
  },
  {
    "path": "src/modules/admin/system/user/user.service.ts",
    "content": "import { Inject, Injectable } from '@nestjs/common';\nimport { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';\nimport { findIndex, isEmpty } from 'lodash';\nimport { ApiException } from 'src/common/exceptions/api.exception';\nimport SysDepartment from 'src/entities/admin/sys-department.entity';\nimport SysUserRole from 'src/entities/admin/sys-user-role.entity';\nimport SysUser from 'src/entities/admin/sys-user.entity';\nimport { UtilService } from 'src/shared/services/util.service';\nimport { EntityManager, In, Not, Repository } from 'typeorm';\nimport {\n  CreateUserDto,\n  UpdatePasswordDto,\n  UpdateUserDto,\n  UpdateUserInfoDto,\n} from './user.dto';\nimport { AccountInfo, PageSearchUserInfo } from './user.class';\nimport { ROOT_ROLE_ID } from 'src/modules/admin/admin.constants';\nimport { RedisService } from 'src/shared/services/redis.service';\nimport { SysParamConfigService } from '../param-config/param-config.service';\nimport { SYS_USER_INITPASSWORD } from 'src/common/contants/param-config.contants';\n\n@Injectable()\nexport class SysUserService {\n  constructor(\n    @InjectRepository(SysUser) private userRepository: Repository<SysUser>,\n    @InjectRepository(SysDepartment)\n    private departmentRepository: Repository<SysDepartment>,\n    @InjectRepository(SysUserRole)\n    private userRoleRepository: Repository<SysUserRole>,\n    private redisService: RedisService,\n    private paramConfigService: SysParamConfigService,\n    @InjectEntityManager() private entityManager: EntityManager,\n    @Inject(ROOT_ROLE_ID) private rootRoleId: number,\n    private util: UtilService,\n  ) {}\n\n  /**\n   * 根据用户名查找已经启用的用户\n   */\n  async findUserByUserName(username: string): Promise<SysUser | undefined> {\n    return await this.userRepository.findOne({\n      where: {\n        username: username,\n        status: 1,\n      },\n    });\n  }\n\n  /**\n   * 获取用户信息\n   * @param uid user id\n   */\n  async getAccountInfo(uid: number): Promise<AccountInfo> {\n    const user: SysUser = await this.userRepository.findOne({\n      where: { id: uid },\n    });\n    if (isEmpty(user)) {\n      throw new ApiException(10017);\n    }\n    return {\n      name: user.name,\n      nickName: user.nickName,\n      email: user.email,\n      phone: user.phone,\n      remark: user.remark,\n      headImg: user.headImg,\n    };\n  }\n\n  /**\n   * 更新个人信息\n   */\n  async updatePersonInfo(uid: number, info: UpdateUserInfoDto): Promise<void> {\n    await this.userRepository.update(uid, info);\n  }\n\n  /**\n   * 更改管理员密码\n   */\n  async updatePassword(uid: number, dto: UpdatePasswordDto): Promise<void> {\n    const user = await this.userRepository.findOne({ where: { id: uid } });\n    if (isEmpty(user)) {\n      throw new ApiException(10017);\n    }\n    const comparePassword = this.util.md5(`${dto.originPassword}${user.psalt}`);\n    // 原密码不一致，不允许更改\n    if (user.password !== comparePassword) {\n      throw new ApiException(10011);\n    }\n    const password = this.util.md5(`${dto.newPassword}${user.psalt}`);\n    await this.userRepository.update({ id: uid }, { password });\n    await this.upgradePasswordV(user.id);\n  }\n\n  /**\n   * 直接更改管理员密码\n   */\n  async forceUpdatePassword(uid: number, password: string): Promise<void> {\n    const user = await this.userRepository.findOne({ where: { id: uid } });\n    if (isEmpty(user)) {\n      throw new ApiException(10017);\n    }\n    const newPassword = this.util.md5(`${password}${user.psalt}`);\n    await this.userRepository.update({ id: uid }, { password: newPassword });\n    await this.upgradePasswordV(user.id);\n  }\n\n  /**\n   * 增加系统用户，如果返回false则表示已存在该用户\n   * @param param Object 对应SysUser实体类\n   */\n  async add(param: CreateUserDto): Promise<void> {\n    // const insertData: any = { ...CreateUserDto };\n    const exists = await this.userRepository.findOne({\n      where: { username: param.username },\n    });\n    if (!isEmpty(exists)) {\n      throw new ApiException(10001);\n    }\n    // 所有用户初始密码为123456\n    await this.entityManager.transaction(async (manager) => {\n      const salt = this.util.generateRandomValue(32);\n\n      // 查找配置的初始密码\n      const initPassword = await this.paramConfigService.findValueByKey(\n        SYS_USER_INITPASSWORD,\n      );\n\n      const password = this.util.md5(`${initPassword ?? '123456'}${salt}`);\n      const u = manager.create(SysUser, {\n        departmentId: param.departmentId,\n        username: param.username,\n        password,\n        name: param.name,\n        nickName: param.nickName,\n        email: param.email,\n        phone: param.phone,\n        remark: param.remark,\n        status: param.status,\n        psalt: salt,\n      });\n      const result = await manager.save(u);\n      const { roles } = param;\n      const insertRoles = roles.map((e) => {\n        return {\n          roleId: e,\n          userId: result.id,\n        };\n      });\n      // 分配角色\n      await manager.insert(SysUserRole, insertRoles);\n    });\n  }\n\n  /**\n   * 更新用户信息\n   */\n  async update(param: UpdateUserDto): Promise<void> {\n    await this.entityManager.transaction(async (manager) => {\n      await manager.update(SysUser, param.id, {\n        departmentId: param.departmentId,\n        username: param.username,\n        name: param.name,\n        nickName: param.nickName,\n        email: param.email,\n        phone: param.phone,\n        remark: param.remark,\n        status: param.status,\n      });\n      // 先删除原来的角色关系\n      await manager.delete(SysUserRole, { userId: param.id });\n      const insertRoles = param.roles.map((e) => {\n        return {\n          roleId: e,\n          userId: param.id,\n        };\n      });\n      // 重新分配角色\n      await manager.insert(SysUserRole, insertRoles);\n      if (param.status === 0) {\n        // 禁用状态\n        await this.forbidden(param.id);\n      }\n    });\n  }\n\n  /**\n   * 查找用户信息\n   * @param id 用户id\n   */\n  async info(\n    id: number,\n  ): Promise<SysUser & { roles: number[]; departmentName: string }> {\n    const user: any = await this.userRepository.findOne({ where: { id } });\n    if (isEmpty(user)) {\n      throw new ApiException(10017);\n    }\n    const departmentRow = await this.departmentRepository.findOne({\n      where: { id: user.departmentId },\n    });\n    if (isEmpty(departmentRow)) {\n      throw new ApiException(10018);\n    }\n    const roleRows = await this.userRoleRepository.find({\n      where: { userId: user.id },\n    });\n    const roles = roleRows.map((e) => {\n      return e.roleId;\n    });\n    delete user.password;\n    return { ...user, roles, departmentName: departmentRow.name };\n  }\n\n  /**\n   * 查找列表里的信息\n   */\n  async infoList(ids: number[]): Promise<SysUser[]> {\n    const users = await this.userRepository.findByIds(ids);\n    return users;\n  }\n\n  /**\n   * 根据ID列表删除用户\n   */\n  async delete(userIds: number[]): Promise<void | never> {\n    const rootUserId = await this.findRootUserId();\n    if (userIds.includes(rootUserId)) {\n      throw new Error('can not delete root user!');\n    }\n    await this.userRepository.delete(userIds);\n    await this.userRoleRepository.delete({ userId: In(userIds) });\n  }\n\n  /**\n   * 根据部门ID列举用户条数：除去超级管理员\n   */\n  async count(uid: number, deptIds: number[]): Promise<number> {\n    const queryAll: boolean = isEmpty(deptIds);\n    const rootUserId = await this.findRootUserId();\n    if (queryAll) {\n      return await this.userRepository.count({\n        where: { id: Not(In([rootUserId, uid])) },\n      });\n    }\n    return await this.userRepository.count({\n      where: {\n        id: Not(In([rootUserId, uid])),\n        departmentId: In(deptIds),\n      },\n    });\n  }\n\n  /**\n   * 查找超管的用户ID\n   */\n  async findRootUserId(): Promise<number> {\n    const result = await this.userRoleRepository.findOne({\n      where: { id: this.rootRoleId },\n    });\n    return result.userId;\n  }\n\n  /**\n   * 根据部门ID进行分页查询用户列表\n   * deptId = -1 时查询全部\n   */\n  async page(\n    uid: number,\n    deptIds: number[],\n    page: number,\n    count: number,\n  ): Promise<PageSearchUserInfo[]> {\n    const queryAll: boolean = isEmpty(deptIds);\n    const rootUserId = await this.findRootUserId();\n    const result = await this.userRepository\n      .createQueryBuilder('user')\n      .innerJoinAndSelect(\n        'sys_department',\n        'dept',\n        'dept.id = user.departmentId',\n      )\n      .innerJoinAndSelect(\n        'sys_user_role',\n        'user_role',\n        'user_role.user_id = user.id',\n      )\n      .innerJoinAndSelect('sys_role', 'role', 'role.id = user_role.role_id')\n      .where('user.id NOT IN (:...ids)', { ids: [rootUserId, uid] })\n      .andWhere(queryAll ? '1 = 1' : 'user.departmentId IN (:...deptIds)', {\n        deptIds,\n      })\n      .offset(page * count)\n      .limit(count)\n      .getRawMany();\n    const dealResult: PageSearchUserInfo[] = [];\n    // 过滤去重\n    result.forEach((e) => {\n      const index = findIndex(dealResult, (e2) => e2.id === e.user_id);\n      if (index < 0) {\n        // 当前元素不存在则插入\n        dealResult.push({\n          createdAt: e.user_created_at,\n          departmentId: e.user_department_id,\n          email: e.user_email,\n          headImg: e.user_head_img,\n          id: e.user_id,\n          name: e.user_name,\n          nickName: e.user_nick_name,\n          phone: e.user_phone,\n          remark: e.user_remark,\n          status: e.user_status,\n          updatedAt: e.user_updated_at,\n          username: e.user_username,\n          departmentName: e.dept_name,\n          roleNames: [e.role_name],\n        });\n      } else {\n        // 已存在\n        dealResult[index].roleNames.push(e.role_name);\n      }\n    });\n    return dealResult;\n  }\n\n  /**\n   * 禁用用户\n   */\n  async forbidden(uid: number): Promise<void> {\n    await this.redisService.getRedis().del(`admin:passwordVersion:${uid}`);\n    await this.redisService.getRedis().del(`admin:token:${uid}`);\n    await this.redisService.getRedis().del(`admin:perms:${uid}`);\n  }\n\n  /**\n   * 禁用多个用户\n   */\n  async multiForbidden(uids: number[]): Promise<void> {\n    if (uids) {\n      const pvs: string[] = [];\n      const ts: string[] = [];\n      const ps: string[] = [];\n      uids.forEach((e) => {\n        pvs.push(`admin:passwordVersion:${e}`);\n        ts.push(`admin:token:${e}`);\n        ps.push(`admin:perms:${e}`);\n      });\n      await this.redisService.getRedis().del(pvs);\n      await this.redisService.getRedis().del(ts);\n      await this.redisService.getRedis().del(ps);\n    }\n  }\n\n  /**\n   * 升级用户版本密码\n   */\n  async upgradePasswordV(id: number): Promise<void> {\n    // admin:passwordVersion:${param.id}\n    const v = await this.redisService\n      .getRedis()\n      .get(`admin:passwordVersion:${id}`);\n    if (!isEmpty(v)) {\n      await this.redisService\n        .getRedis()\n        .set(`admin:passwordVersion:${id}`, parseInt(v) + 1);\n    }\n  }\n}\n"
  },
  {
    "path": "src/modules/ws/admin-ws.gateway.ts",
    "content": "import {\n  OnGatewayConnection,\n  OnGatewayDisconnect,\n  OnGatewayInit,\n  WebSocketGateway,\n  WebSocketServer,\n} from '@nestjs/websockets';\nimport { Server, Socket } from 'socket.io';\nimport { AuthService } from './auth.service';\nimport { EVENT_OFFLINE, EVENT_ONLINE } from './ws.event';\n\n/**\n * Admin WebSokcet网关，不含权限校验，Socket端只做通知相关操作\n */\n@WebSocketGateway(parseInt(process.env.WS_PORT || '7002'), {\n  path: '/ws',\n  namespace: '/admin',\n})\nexport class AdminWSGateway\n  implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit\n{\n  @WebSocketServer()\n  private wss: Server;\n\n  get socketServer(): Server {\n    return this.wss;\n  }\n\n  constructor(private authService: AuthService) {}\n\n  /**\n   * OnGatewayInit\n   * @param server Server\n   */\n  afterInit() {\n    // TODO\n  }\n\n  /**\n   * OnGatewayConnection\n   */\n  async handleConnection(client: Socket): Promise<void> {\n    try {\n      this.authService.checkAdminAuthToken(client.handshake?.query?.token);\n    } catch (e) {\n      // no auth\n      client.disconnect();\n      return;\n    }\n\n    // broadcast online\n    client.broadcast.emit(EVENT_ONLINE);\n  }\n\n  /**\n   * OnGatewayDisconnect\n   */\n  async handleDisconnect(client: Socket): Promise<void> {\n    // TODO\n    client.broadcast.emit(EVENT_OFFLINE);\n  }\n}\n"
  },
  {
    "path": "src/modules/ws/admin-ws.guard.ts",
    "content": "import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { Socket } from 'socket.io';\nimport { SocketException } from 'src/common/exceptions/socket.exception';\nimport { AuthService } from './auth.service';\n\n@Injectable()\nexport class AdminWsGuard implements CanActivate {\n  constructor(private authService: AuthService) {}\n\n  canActivate(\n    context: ExecutionContext,\n  ): boolean | Promise<boolean> | Observable<boolean> {\n    const client = context.switchToWs().getClient<Socket>();\n    const token = client?.handshake?.query?.token;\n    try {\n      // 挂载对象到当前请求上\n      this.authService.checkAdminAuthToken(token);\n      return true;\n    } catch (e) {\n      // close\n      client.disconnect();\n      // 无法通过token校验\n      throw new SocketException(11001);\n    }\n  }\n}\n"
  },
  {
    "path": "src/modules/ws/auth.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { JwtService } from '@nestjs/jwt';\nimport { isEmpty } from 'lodash';\nimport { SocketException } from 'src/common/exceptions/socket.exception';\nimport { IAdminUser } from '../admin/admin.interface';\n\n@Injectable()\nexport class AuthService {\n  constructor(private jwtService: JwtService) {}\n\n  checkAdminAuthToken(\n    token: string | string[] | undefined,\n  ): IAdminUser | never {\n    if (isEmpty(token)) {\n      throw new SocketException(11001);\n    }\n    try {\n      // 挂载对象到当前请求上\n      return this.jwtService.verify(Array.isArray(token) ? token[0] : token);\n    } catch (e) {\n      // 无法通过token校验\n      throw new SocketException(11001);\n    }\n  }\n}\n"
  },
  {
    "path": "src/modules/ws/ws.event.ts",
    "content": "// 用户上线事件\nexport const EVENT_ONLINE = 'online';\nexport const EVENT_OFFLINE = 'offline';\n// 踢下线\nexport const EVENT_KICK = 'kick';\n"
  },
  {
    "path": "src/modules/ws/ws.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { AdminWSGateway } from './admin-ws.gateway';\nimport { AuthService } from './auth.service';\n\nconst providers = [AdminWSGateway, AuthService];\n\n/**\n * WebSocket Module\n */\n@Module({\n  providers,\n  exports: providers,\n})\nexport class WSModule {}\n"
  },
  {
    "path": "src/polyfill.ts",
    "content": "import { format } from 'date-fns';\n\nDate.prototype.toJSON = function () {\n  return format(this, 'yyyy-MM-dd HH:mm:ss');\n};\n"
  },
  {
    "path": "src/setup-swagger.ts",
    "content": "import { INestApplication } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';\n\nimport { ADMIN_PREFIX } from './modules/admin/admin.constants';\n\nexport function setupSwagger(app: INestApplication): void {\n  const configService: ConfigService = app.get(ConfigService);\n\n  // 默认为启用\n  const enable = configService.get<boolean>('swagger.enable', true);\n\n  // 判断是否需要启用\n  if (!enable) {\n    return;\n  }\n\n  const swaggerConfig = new DocumentBuilder()\n    .setTitle(configService.get<string>('swagger.title'))\n    .setDescription(configService.get<string>('swagger.desc'))\n    .setLicense('MIT', 'https://github.com/hackycy/sf-nest-admin')\n    // JWT鉴权\n    .addSecurity(ADMIN_PREFIX, {\n      description: '后台管理接口授权',\n      type: 'apiKey',\n      in: 'header',\n      name: 'Authorization',\n    })\n    .build();\n  const document = SwaggerModule.createDocument(app, swaggerConfig);\n\n  SwaggerModule.setup(\n    configService.get<string>('swagger.path', '/swagger-api'),\n    app,\n    document,\n  );\n}\n"
  },
  {
    "path": "src/shared/logger/logger.constants.ts",
    "content": "export const LOGGER_MODULE_OPTIONS = Symbol('LOGGER_MODULE_OPTIONS');\nexport const PROJECT_LOG_DIR_NAME = 'logs';\nexport const DEFAULT_WEB_LOG_NAME = 'web.log';\nexport const DEFAULT_ERROR_LOG_NAME = 'common-error.log';\nexport const DEFAULT_ACCESS_LOG_NAME = 'access.log';\nexport const DEFAULT_SQL_SLOW_LOG_NAME = 'sql-slow.log';\nexport const DEFAULT_SQL_ERROR_LOG_NAME = 'sql-error.log';\nexport const DEFAULT_TASK_LOG_NAME = 'task.log';\n// 默认日志存储天数\nexport const DEFAULT_MAX_SIZE = '2m';\n"
  },
  {
    "path": "src/shared/logger/logger.interface.ts",
    "content": "import { ModuleMetadata } from '@nestjs/common';\nimport { LoggerOptions } from 'typeorm';\n\n/**\n * 日志等级\n */\nexport type WinstonLogLevel = 'info' | 'error' | 'warn' | 'debug' | 'verbose';\n\nexport interface TypeORMLoggerOptions {\n  options?: LoggerOptions;\n}\n\n/**\n * 日志配置，默认按天数进行切割\n */\nexport interface LoggerModuleOptions {\n  /**\n   * 日志文件输出\n   * 默认只会输出 log 及以上（warn 和 error）的日志到文件中，等级级别如下\n   */\n  level?: WinstonLogLevel | 'none';\n\n  /**\n   * 控制台输出等级\n   */\n  consoleLevel?: WinstonLogLevel | 'none';\n\n  /**\n   * 如果启用，将打印当前和上一个日志消息之间的时间戳（时差）\n   */\n  timestamp?: boolean;\n\n  /**\n   * 生产环境下，默认会关闭终端日志输出，如有需要，可以设置为 false\n   */\n  disableConsoleAtProd?: boolean;\n\n  /**\n   * Maximum size of the file after which it will rotate. This can be a number of bytes, or units of kb, mb, and gb.\n   *  If using the units, add 'k', 'm', or 'g' as the suffix. The units need to directly follow the number.\n   *  default: 2m\n   */\n  maxFileSize?: string;\n\n  /**\n   * Maximum number of logs to keep. If not set,\n   * no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix.\n   * default: 15d\n   */\n  maxFiles?: string;\n\n  /**\n   * 开发环境下日志产出的目录，绝对路径\n   * 开发环境下为了避免冲突以及集中管理，日志会打印在项目目录下的 logs 目录\n   */\n  dir?: string;\n\n  /**\n   * 任何 logger 的 .error() 调用输出的日志都会重定向到这里，重点通过查看此日志定位异常，默认文件名为 common-error.%DATE%.log\n   * 注意：此文件名可以包含%DATE%占位符\n   */\n  errorLogName?: string;\n\n  /**\n   * 应用相关日志，供应用开发者使用的日志。我们在绝大数情况下都在使用它，默认文件名为 web.%DATE%.log\n   * 注意：此文件名可以包含%DATE%占位符\n   */\n  appLogName?: string;\n}\n\nexport interface LoggerModuleAsyncOptions\n  extends Pick<ModuleMetadata, 'imports'> {\n  useFactory?: (...args: any[]) => LoggerModuleOptions;\n  inject?: any[];\n}\n"
  },
  {
    "path": "src/shared/logger/logger.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { LOGGER_MODULE_OPTIONS } from './logger.constants';\nimport {\n  LoggerModuleAsyncOptions,\n  LoggerModuleOptions,\n} from './logger.interface';\nimport { LoggerService } from './logger.service';\n\n@Module({})\nexport class LoggerModule {\n  static forRoot(\n    options: LoggerModuleOptions,\n    isGlobal = false,\n  ): DynamicModule {\n    return {\n      global: isGlobal,\n      module: LoggerModule,\n      providers: [\n        LoggerService,\n        {\n          provide: LOGGER_MODULE_OPTIONS,\n          useValue: options,\n        },\n      ],\n      exports: [LoggerService, LOGGER_MODULE_OPTIONS],\n    };\n  }\n\n  static forRootAsync(\n    options: LoggerModuleAsyncOptions,\n    isGlobal = false,\n  ): DynamicModule {\n    return {\n      global: isGlobal,\n      module: LoggerModule,\n      imports: options.imports,\n      providers: [\n        LoggerService,\n        {\n          provide: LOGGER_MODULE_OPTIONS,\n          useFactory: options.useFactory,\n          inject: options.inject,\n        },\n      ],\n      exports: [LoggerService, LOGGER_MODULE_OPTIONS],\n    };\n  }\n}\n"
  },
  {
    "path": "src/shared/logger/logger.service.ts",
    "content": "import {\n  Injectable,\n  Optional,\n  Inject,\n  LoggerService as NestLoggerService,\n} from '@nestjs/common';\nimport { clc, yellow } from '@nestjs/common/utils/cli-colors.util';\nimport {\n  DEFAULT_ERROR_LOG_NAME,\n  DEFAULT_MAX_SIZE,\n  DEFAULT_WEB_LOG_NAME,\n  LOGGER_MODULE_OPTIONS,\n  PROJECT_LOG_DIR_NAME,\n} from './logger.constants';\nimport { LoggerModuleOptions, WinstonLogLevel } from './logger.interface';\nimport { getAppRootPath } from './utils/app-root-path.util';\nimport { createLogger, Logger as WinstonLogger, format } from 'winston';\nimport { join } from 'path';\nimport * as WinstonDailyRotateFile from 'winston-daily-rotate-file';\nimport { isDev } from 'src/config/env';\nimport { isPlainObject } from 'lodash';\n\n/**\n * 默认输出的日志等级\n */\nconst DEFAULT_LOG_CONSOLE_LEVELS: WinstonLogLevel = isDev() ? 'info' : 'error';\nconst DEFAULT_LOG_WINSTON_LEVELS: WinstonLogLevel = 'info';\n\n/**\n * 日志输出等级，基于Nest配置扩展，与winston配合，由于log等级与winston定义冲突，需要转为info\n * https://github.com/nestjs/nest/blob/master/packages/common/services/utils/is-log-level-enabled.util.ts\n */\nconst LOG_LEVEL_VALUES: Record<WinstonLogLevel, number> = {\n  debug: 4,\n  verbose: 3,\n  info: 2,\n  warn: 1,\n  error: 0,\n};\n\n@Injectable()\nexport class LoggerService implements NestLoggerService {\n  private static lastTimestampAt?: number;\n  /**\n   * 日志文件存放文件夹路径\n   */\n  private logDir: string;\n\n  /**\n   * winston实例\n   */\n  private winstonLogger: WinstonLogger;\n\n  constructor();\n  constructor(context: string, options: LoggerModuleOptions);\n  constructor(\n    @Optional() protected context?: string,\n    @Optional()\n    @Inject(LOGGER_MODULE_OPTIONS)\n    protected options: LoggerModuleOptions = {},\n  ) {\n    // 默认配置\n    this.options.timestamp === undefined && (this.options.timestamp = true);\n    // 文件输出等级\n    !this.options.level && (this.options.level = DEFAULT_LOG_WINSTON_LEVELS);\n    // 控制台输出等级\n    !this.options.consoleLevel &&\n      (this.options.consoleLevel = DEFAULT_LOG_CONSOLE_LEVELS);\n    // 输出的文件大小\n    !this.options.maxFileSize && (this.options.maxFileSize = DEFAULT_MAX_SIZE);\n    // 默认输出文件名\n    !this.options.appLogName &&\n      (this.options.appLogName = DEFAULT_WEB_LOG_NAME);\n    !this.options.errorLogName &&\n      (this.options.errorLogName = DEFAULT_ERROR_LOG_NAME);\n\n    // 初始化 winston\n    this.initWinston();\n  }\n\n  /**\n   * 初始化winston\n   */\n  private initWinston() {\n    // 配置日志输出目录\n    if (this.options.dir) {\n      this.logDir = this.options.dir;\n    } else {\n      // 如果不指定，则使用 使用 当前项目目录 + logs 路径进行保存\n      this.logDir = join(getAppRootPath(), PROJECT_LOG_DIR_NAME);\n    }\n    const transportOptions: WinstonDailyRotateFile.DailyRotateFileTransportOptions =\n      {\n        dirname: this.logDir,\n        maxSize: this.options.maxFileSize,\n        maxFiles: this.options.maxFiles,\n      };\n    // 多路日志\n    const webTransport = new WinstonDailyRotateFile(\n      Object.assign(transportOptions, { filename: this.options.appLogName }),\n    );\n    // 所有error级别都记录在该文件下\n    const errorTransport = new WinstonDailyRotateFile(\n      Object.assign(transportOptions, {\n        filename: this.options.errorLogName,\n        level: 'error',\n      }),\n    );\n    // 初始化winston\n    this.winstonLogger = createLogger({\n      level: this.options.level,\n      format: format.json({\n        space: 0,\n      }),\n      levels: LOG_LEVEL_VALUES,\n      transports: [webTransport, errorTransport],\n    });\n  }\n\n  /**\n   * 获取日志存放路径\n   */\n  protected getLogDir(): string {\n    return this.logDir;\n  }\n\n  /**\n   * 获取winston实例\n   */\n  protected getWinstonLogger(): WinstonLogger {\n    return this.winstonLogger;\n  }\n\n  /**\n   * Write a 'info' level log, if the configured level allows for it.\n   * Prints to `stdout` with newline.\n   */\n  log(message: any, context?: string): void;\n  log(message: any, ...optionalParams: [...any, string?]): void;\n  log(message: any, ...optionalParams: any[]) {\n    const consoleEnable = this.isConsoleLevelEnabled('info');\n    const winstonEnable = this.isWinstonLevelEnabled('info');\n    if (!consoleEnable && !winstonEnable) {\n      return;\n    }\n    const { messages, context } = this.getContextAndMessagesToPrint([\n      message,\n      ...optionalParams,\n    ]);\n    if (consoleEnable) {\n      this.printMessages(messages, context, 'info');\n    }\n    this.recordMessages(messages, context, 'info');\n  }\n\n  /**\n   * Write an 'error' level log, if the configured level allows for it.\n   * Prints to `stderr` with newline.\n   */\n  error(message: any, context?: string): void;\n  error(message: any, stack?: string, context?: string): void;\n  error(message: any, ...optionalParams: [...any, string?, string?]): void;\n  error(message: any, ...optionalParams: any[]) {\n    const consoleEnable = this.isConsoleLevelEnabled('error');\n    const winstonEnable = this.isWinstonLevelEnabled('error');\n    if (!consoleEnable && !winstonEnable) {\n      return;\n    }\n    const { messages, context, stack } =\n      this.getContextAndStackAndMessagesToPrint([message, ...optionalParams]);\n    if (consoleEnable) {\n      this.printMessages(messages, context, 'error', 'stderr');\n      this.printStackTrace(stack);\n    }\n    this.recordMessages(messages, context, 'error', stack);\n  }\n\n  /**\n   * Write a 'warn' level log, if the configured level allows for it.\n   * Prints to `stdout` with newline.\n   */\n  warn(message: any, context?: string): void;\n  warn(message: any, ...optionalParams: [...any, string?]): void;\n  warn(message: any, ...optionalParams: any[]) {\n    const consoleEnable = this.isConsoleLevelEnabled('warn');\n    const winstonEnable = this.isWinstonLevelEnabled('warn');\n    if (!consoleEnable && !winstonEnable) {\n      return;\n    }\n    const { messages, context } = this.getContextAndMessagesToPrint([\n      message,\n      ...optionalParams,\n    ]);\n    if (consoleEnable) {\n      this.printMessages(messages, context, 'warn');\n    }\n    this.recordMessages(messages, context, 'warn');\n  }\n\n  /**\n   * Write a 'debug' level log, if the configured level allows for it.\n   * Prints to `stdout` with newline.\n   */\n  debug(message: any, context?: string): void;\n  debug(message: any, ...optionalParams: [...any, string?]): void;\n  debug(message: any, ...optionalParams: any[]) {\n    const consoleEnable = this.isConsoleLevelEnabled('debug');\n    const winstonEnable = this.isWinstonLevelEnabled('debug');\n    if (!consoleEnable && !winstonEnable) {\n      return;\n    }\n    const { messages, context } = this.getContextAndMessagesToPrint([\n      message,\n      ...optionalParams,\n    ]);\n    if (consoleEnable) {\n      this.printMessages(messages, context, 'debug');\n    }\n    this.recordMessages(messages, context, 'debug');\n  }\n\n  /**\n   * Write a 'verbose' level log, if the configured level allows for it.\n   * Prints to `stdout` with newline.\n   */\n  verbose(message: any, context?: string): void;\n  verbose(message: any, ...optionalParams: [...any, string?]): void;\n  verbose(message: any, ...optionalParams: any[]) {\n    const consoleEnable = this.isConsoleLevelEnabled('verbose');\n    const winstonEnable = this.isWinstonLevelEnabled('verbose');\n    if (!consoleEnable && !winstonEnable) {\n      return;\n    }\n    const { messages, context } = this.getContextAndMessagesToPrint([\n      message,\n      ...optionalParams,\n    ]);\n    if (consoleEnable) {\n      this.printMessages(messages, context, 'verbose');\n    }\n    this.recordMessages(messages, context, 'verbose');\n  }\n\n  protected isConsoleLevelEnabled(level: WinstonLogLevel): boolean {\n    // 默认禁止生产模式控制台日志输出\n    if (!isDev() && !this.options.disableConsoleAtProd) {\n      return false;\n    }\n    if (this.options.consoleLevel === 'none') {\n      return false;\n    }\n    return LOG_LEVEL_VALUES[level] <= LOG_LEVEL_VALUES[level];\n  }\n\n  protected isWinstonLevelEnabled(level: WinstonLogLevel): boolean {\n    // 默认禁止生产模式控制台日志输出\n    if (this.options.level === 'none') {\n      return false;\n    }\n    return LOG_LEVEL_VALUES[level] <= LOG_LEVEL_VALUES[level];\n  }\n\n  // code from -> https://github.com/nestjs/nest/blob/master/packages/common/services/console-logger.service.ts\n  protected getTimestamp(): string {\n    const localeStringOptions = {\n      year: 'numeric',\n      hour: 'numeric',\n      minute: 'numeric',\n      second: 'numeric',\n      day: '2-digit',\n      month: '2-digit',\n    };\n    return new Date(Date.now()).toLocaleString(\n      undefined,\n      localeStringOptions as Intl.DateTimeFormatOptions,\n    );\n  }\n\n  protected recordMessages(\n    messages: unknown[],\n    context = '',\n    logLevel: WinstonLogLevel = 'info',\n    stack?: string,\n  ) {\n    messages.forEach((message) => {\n      const output = isPlainObject(message)\n        ? JSON.stringify(\n            message,\n            (_, value) =>\n              typeof value === 'bigint' ? value.toString() : value,\n            0,\n          )\n        : (message as string);\n\n      this.winstonLogger.log(logLevel, output, {\n        context,\n        stack,\n        pid: process.pid,\n        timestamp: this.getTimestamp(),\n      });\n    });\n  }\n\n  protected printMessages(\n    messages: unknown[],\n    context = '',\n    logLevel: WinstonLogLevel = 'info',\n    writeStreamType?: 'stdout' | 'stderr',\n  ) {\n    const color = this.getColorByLogLevel(logLevel);\n    messages.forEach((message) => {\n      const output = isPlainObject(message)\n        ? `${color('Object:')}\\n${JSON.stringify(\n            message,\n            (_, value) =>\n              typeof value === 'bigint' ? value.toString() : value,\n            2,\n          )}\\n`\n        : color(message as string);\n\n      const pidMessage = color(`[Nest] ${process.pid}  - `);\n      const contextMessage = context ? yellow(`[${context}] `) : '';\n      const timestampDiff = this.updateAndGetTimestampDiff();\n      const formattedLogLevel = color(logLevel.toUpperCase().padStart(7, ' '));\n      const computedMessage = `${pidMessage}${this.getTimestamp()} ${formattedLogLevel} ${contextMessage}${output}${timestampDiff}\\n`;\n\n      process[writeStreamType ?? 'stdout'].write(computedMessage);\n    });\n  }\n\n  protected printStackTrace(stack: string) {\n    if (!stack) {\n      return;\n    }\n    process.stderr.write(`${stack}\\n`);\n  }\n\n  private updateAndGetTimestampDiff(): string {\n    const includeTimestamp =\n      LoggerService.lastTimestampAt && this.options?.timestamp;\n    const result = includeTimestamp\n      ? yellow(` +${Date.now() - LoggerService.lastTimestampAt}ms`)\n      : '';\n    LoggerService.lastTimestampAt = Date.now();\n    return result;\n  }\n\n  private getContextAndMessagesToPrint(args: unknown[]) {\n    if (args?.length <= 1) {\n      return { messages: args, context: this.context };\n    }\n    const lastElement = args[args.length - 1];\n    const isContext = typeof lastElement === 'string';\n    if (!isContext) {\n      return { messages: args, context: this.context };\n    }\n    return {\n      context: lastElement as string,\n      messages: args.slice(0, args.length - 1),\n    };\n  }\n\n  private getContextAndStackAndMessagesToPrint(args: unknown[]) {\n    const { messages, context } = this.getContextAndMessagesToPrint(args);\n    if (messages?.length <= 1) {\n      return { messages, context };\n    }\n    const lastElement = messages[messages.length - 1];\n    const isStack = typeof lastElement === 'string';\n    if (!isStack) {\n      return { messages, context };\n    }\n    return {\n      stack: lastElement as string,\n      messages: messages.slice(0, messages.length - 1),\n      context,\n    };\n  }\n\n  private getColorByLogLevel(level: WinstonLogLevel): (text: string) => string {\n    switch (level) {\n      case 'debug':\n        return clc.magentaBright;\n      case 'warn':\n        return clc.yellow;\n      case 'error':\n        return clc.red;\n      case 'verbose':\n        return clc.cyanBright;\n      default:\n        return clc.green;\n    }\n  }\n}\n"
  },
  {
    "path": "src/shared/logger/typeorm-logger.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Logger, LoggerOptions } from 'typeorm';\nimport {\n  DEFAULT_SQL_ERROR_LOG_NAME,\n  DEFAULT_SQL_SLOW_LOG_NAME,\n} from './logger.constants';\nimport { LoggerModuleOptions } from './logger.interface';\nimport { LoggerService } from './logger.service';\n\n/**\n * 自定义TypeORM日志，sqlSlow日志及error日志会自动记录至日志文件\n */\n@Injectable()\nexport class TypeORMLoggerService implements Logger {\n  /**\n   * sql logger\n   */\n  private logger: LoggerService;\n\n  constructor(\n    private options: LoggerOptions,\n    private config: LoggerModuleOptions,\n  ) {\n    this.logger = new LoggerService(TypeORMLoggerService.name, {\n      level: 'warn',\n      consoleLevel: 'verbose',\n      appLogName: DEFAULT_SQL_SLOW_LOG_NAME,\n      errorLogName: DEFAULT_SQL_ERROR_LOG_NAME,\n      timestamp: this.config.timestamp,\n      dir: this.config.dir,\n      maxFileSize: this.config.maxFileSize,\n      maxFiles: this.config.maxFiles,\n    });\n  }\n\n  /**\n   * Logs query and parameters used in it.\n   */\n  logQuery(query: string, parameters?: any[]) {\n    if (\n      this.options === 'all' ||\n      this.options === true ||\n      (Array.isArray(this.options) && this.options.indexOf('query') !== -1)\n    ) {\n      const sql =\n        query +\n        (parameters && parameters.length\n          ? ' -- PARAMETERS: ' + this.stringifyParams(parameters)\n          : '');\n      this.logger.verbose('[QUERY]: ' + sql);\n    }\n  }\n\n  /**\n   * Logs query that is failed.\n   */\n  logQueryError(error: string | Error, query: string, parameters?: any[]) {\n    if (\n      this.options === 'all' ||\n      this.options === true ||\n      (Array.isArray(this.options) && this.options.indexOf('error') !== -1)\n    ) {\n      const sql =\n        query +\n        (parameters && parameters.length\n          ? ' -- PARAMETERS: ' + this.stringifyParams(parameters)\n          : '');\n      this.logger.error([`[FAILED QUERY]: ${sql}`, `[QUERY ERROR]: ${error}`]);\n    }\n  }\n\n  /**\n   * Logs query that is slow.\n   */\n  logQuerySlow(time: number, query: string, parameters?: any[]) {\n    const sql =\n      query +\n      (parameters && parameters.length\n        ? ' -- PARAMETERS: ' + this.stringifyParams(parameters)\n        : '');\n    this.logger.warn(`[SLOW QUERY: ${time} ms]: ` + sql);\n  }\n\n  /**\n   * Logs events from the schema build process.\n   */\n  logSchemaBuild(message: string) {\n    if (\n      this.options === 'all' ||\n      (Array.isArray(this.options) && this.options.indexOf('schema') !== -1)\n    ) {\n      this.logger.verbose(message);\n    }\n  }\n\n  /**\n   * Logs events from the migrations run process.\n   */\n  logMigration(message: string) {\n    this.logger.verbose(message);\n  }\n\n  /**\n   * Perform logging using given logger, or by default to the console.\n   * Log has its own level and message.\n   */\n  log(level: 'log' | 'info' | 'warn', message: any) {\n    switch (level) {\n      case 'log':\n        if (\n          this.options === 'all' ||\n          (Array.isArray(this.options) && this.options.indexOf('log') !== -1)\n        )\n          this.logger.verbose('[LOG]: ' + message);\n        break;\n      case 'info':\n        if (\n          this.options === 'all' ||\n          (Array.isArray(this.options) && this.options.indexOf('info') !== -1)\n        )\n          this.logger.log('[INFO]: ' + message);\n        break;\n      case 'warn':\n        if (\n          this.options === 'all' ||\n          (Array.isArray(this.options) && this.options.indexOf('warn') !== -1)\n        )\n          this.logger.warn('[WARN]: ' + message);\n        break;\n    }\n  }\n\n  /**\n   * Converts parameters to a string.\n   * Sometimes parameters can have circular objects and therefor we are handle this case too.\n   */\n  protected stringifyParams(parameters: any[]) {\n    try {\n      return JSON.stringify(parameters);\n    } catch (error) {\n      // most probably circular objects in parameters\n      return parameters;\n    }\n  }\n}\n"
  },
  {
    "path": "src/shared/logger/utils/app-root-path.util.ts",
    "content": "import { parse, resolve, join } from 'path';\nimport { existsSync } from 'fs';\n\n/**\n * 获取应用根目录\n * @returns 应用根目录\n */\nexport function getAppRootPath(): string {\n  // Check for environmental variable\n  if (process.env.APP_ROOT_PATH) {\n    return resolve(process.env.APP_ROOT_PATH);\n  }\n  // 逐级查找 node_modules 目录\n  let cur = __dirname;\n  const root = parse(cur).root;\n\n  let appRootPath = '';\n  while (true) {\n    if (\n      existsSync(join(cur, 'node_modules')) &&\n      existsSync(join(cur, 'package.json'))\n    ) {\n      // 如果存在node_modules、package.json\n      appRootPath = cur;\n    }\n    // 已经为根路径，无需向上查找\n    if (root === cur) {\n      break;\n    }\n\n    // 继续向上查找\n    cur = resolve(cur, '..');\n  }\n\n  if (appRootPath) {\n    process.env.APP_ROOT_PATH = appRootPath;\n  }\n  return appRootPath;\n}\n"
  },
  {
    "path": "src/shared/logger/utils/home-dir.ts",
    "content": "import * as os from 'os';\n\nexport function getHomedir(): string {\n  if (process.env.MOCK_HOME_DIR) return process.env.MOCK_HOME_DIR;\n\n  if (typeof os.userInfo === 'function') {\n    try {\n      const homedir = os.userInfo().homedir;\n      if (homedir) return homedir;\n    } catch (err) {\n      if (err.code !== 'ENOENT') throw err;\n    }\n  }\n\n  if (typeof os.homedir === 'function') {\n    return os.homedir();\n  }\n\n  return process.env.HOME;\n}\n"
  },
  {
    "path": "src/shared/redis/redis.constants.ts",
    "content": "export const REDIS_CLIENT = Symbol('REDIS_CLIENT');\nexport const REDIS_MODULE_OPTIONS = Symbol('REDIS_MODULE_OPTIONS');\nexport const REDIS_DEFAULT_CLIENT_KEY = 'default';\n"
  },
  {
    "path": "src/shared/redis/redis.interface.ts",
    "content": "import { ModuleMetadata } from '@nestjs/common';\nimport { Redis, RedisOptions, ClusterNode, ClusterOptions } from 'ioredis';\n\nexport interface RedisModuleOptions extends RedisOptions {\n  /**\n   * muitl client connection, default\n   */\n  name?: string;\n\n  /**\n   * support url\n   */\n  url?: string;\n\n  /**\n   * is cluster\n   */\n  cluster?: boolean;\n\n  /**\n   * cluster node, using cluster is true\n   */\n  nodes?: ClusterNode[];\n\n  /**\n   * cluster options, using cluster is true\n   */\n  clusterOptions?: ClusterOptions;\n\n  /**\n   * callback\n   */\n  onClientReady?(client: Redis): void;\n}\n\nexport interface RedisModuleAsyncOptions\n  extends Pick<ModuleMetadata, 'imports'> {\n  useFactory?: (\n    ...args: any[]\n  ) =>\n    | RedisModuleOptions\n    | RedisModuleOptions[]\n    | Promise<RedisModuleOptions>\n    | Promise<RedisModuleOptions[]>;\n  inject?: any[];\n}\n"
  },
  {
    "path": "src/shared/redis/redis.module.ts",
    "content": "import {\n  DynamicModule,\n  Module,\n  OnModuleDestroy,\n  Provider,\n} from '@nestjs/common';\nimport IORedis, { Redis, Cluster } from 'ioredis';\nimport { isEmpty } from 'lodash';\nimport {\n  REDIS_CLIENT,\n  REDIS_DEFAULT_CLIENT_KEY,\n  REDIS_MODULE_OPTIONS,\n} from './redis.constants';\nimport { RedisModuleAsyncOptions, RedisModuleOptions } from './redis.interface';\n\n@Module({})\nexport class RedisModule implements OnModuleDestroy {\n  static register(\n    options: RedisModuleOptions | RedisModuleOptions[],\n  ): DynamicModule {\n    const clientProvider = this.createAysncProvider();\n    return {\n      module: RedisModule,\n      providers: [\n        clientProvider,\n        {\n          provide: REDIS_MODULE_OPTIONS,\n          useValue: options,\n        },\n      ],\n      exports: [clientProvider],\n    };\n  }\n\n  static registerAsync(options: RedisModuleAsyncOptions): DynamicModule {\n    const clientProvider = this.createAysncProvider();\n    return {\n      module: RedisModule,\n      imports: options.imports ?? [],\n      providers: [clientProvider, this.createAsyncClientOptions(options)],\n      exports: [clientProvider],\n    };\n  }\n\n  /**\n   * create provider\n   */\n  private static createAysncProvider(): Provider {\n    // create client\n    return {\n      provide: REDIS_CLIENT,\n      useFactory: (\n        options: RedisModuleOptions | RedisModuleOptions[],\n      ): Map<string, Redis | Cluster> => {\n        const clients = new Map<string, Redis | Cluster>();\n        if (Array.isArray(options)) {\n          options.forEach((op) => {\n            const name = op.name ?? REDIS_DEFAULT_CLIENT_KEY;\n            if (clients.has(name)) {\n              throw new Error('Redis Init Error: name must unique');\n            }\n            clients.set(name, this.createClient(op));\n          });\n        } else {\n          // not array\n          clients.set(REDIS_DEFAULT_CLIENT_KEY, this.createClient(options));\n        }\n        return clients;\n      },\n      inject: [REDIS_MODULE_OPTIONS],\n    };\n  }\n\n  /**\n   * 创建IORedis实例\n   */\n  private static createClient(options: RedisModuleOptions): Redis | Cluster {\n    const { onClientReady, url, cluster, clusterOptions, nodes, ...opts } =\n      options;\n    let client = null;\n    // check url\n    if (!isEmpty(url)) {\n      client = new IORedis(url);\n    } else if (cluster) {\n      // check cluster\n      client = new IORedis.Cluster(nodes, clusterOptions);\n    } else {\n      client = new IORedis(opts);\n    }\n    if (onClientReady) {\n      onClientReady(client);\n    }\n    return client;\n  }\n\n  private static createAsyncClientOptions(options: RedisModuleAsyncOptions) {\n    return {\n      provide: REDIS_MODULE_OPTIONS,\n      useFactory: options.useFactory,\n      inject: options.inject,\n    };\n  }\n\n  onModuleDestroy() {\n    // on destroy\n  }\n}\n"
  },
  {
    "path": "src/shared/services/redis.service.ts",
    "content": "import { Inject, Injectable } from '@nestjs/common';\nimport { Cluster } from 'cluster';\nimport { Redis } from 'ioredis';\nimport {\n  REDIS_CLIENT,\n  REDIS_DEFAULT_CLIENT_KEY,\n} from '../redis/redis.constants';\n\n@Injectable()\nexport class RedisService {\n  constructor(\n    @Inject(REDIS_CLIENT)\n    private readonly clients: Map<string, Redis | Cluster>,\n  ) {}\n\n  /**\n   * get redis client by name\n   * @param name client name\n   * @returns Redis Instance\n   */\n  public getRedis(name = REDIS_DEFAULT_CLIENT_KEY): Redis {\n    if (!this.clients.has(name)) {\n      throw new Error(`redis client ${name} does not exist`);\n    }\n    return this.clients.get(name) as Redis;\n  }\n}\n"
  },
  {
    "path": "src/shared/services/util.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { FastifyRequest } from 'fastify';\nimport { customAlphabet, nanoid } from 'nanoid';\nimport * as CryptoJS from 'crypto-js';\n\n@Injectable()\nexport class UtilService {\n  /**\n   * 获取请求IP\n   */\n  getReqIP(req: FastifyRequest): string {\n    return (\n      // 判断是否有反向代理 IP\n      (\n        (req.headers['x-forwarded-for'] as string) ||\n        // 判断后端的 socket 的 IP\n        req.socket.remoteAddress\n      ).replace('::ffff:', '')\n    );\n  }\n\n  /**\n   * AES加密\n   */\n  public aesEncrypt(msg: string, secret: string): string {\n    return CryptoJS.AES.encrypt(msg, secret).toString();\n  }\n\n  /**\n   * AES解密\n   */\n  public aesDecrypt(encrypted: string, secret: string): string {\n    return CryptoJS.AES.decrypt(encrypted, secret).toString(CryptoJS.enc.Utf8);\n  }\n\n  /**\n   * md5加密\n   */\n  public md5(msg: string): string {\n    return CryptoJS.MD5(msg).toString();\n  }\n\n  /**\n   * 生成一个UUID\n   */\n  public generateUUID(): string {\n    return nanoid();\n  }\n\n  /**\n   * 生成一个随机的值\n   */\n  public generateRandomValue(\n    length: number,\n    placeholder = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM',\n  ): string {\n    const customNanoid = customAlphabet(placeholder, length);\n    return customNanoid();\n  }\n}\n"
  },
  {
    "path": "src/shared/shared.module.ts",
    "content": "import { HttpModule } from '@nestjs/axios';\nimport { Global, CacheModule, Module } from '@nestjs/common';\nimport { ConfigModule, ConfigService } from '@nestjs/config';\nimport { JwtModule } from '@nestjs/jwt';\nimport { RedisModule } from './redis/redis.module';\nimport { RedisService } from './services/redis.service';\nimport { UtilService } from './services/util.service';\n\n// common provider list\nconst providers = [UtilService, RedisService];\n\n/**\n * 全局共享模块\n */\n@Global()\n@Module({\n  imports: [\n    HttpModule.register({\n      timeout: 5000,\n      maxRedirects: 5,\n    }),\n    // redis cache\n    CacheModule.register(),\n    // jwt\n    JwtModule.registerAsync({\n      imports: [ConfigModule],\n      useFactory: (configService: ConfigService) => ({\n        secret: configService.get<string>('jwt.secret'),\n      }),\n      inject: [ConfigService],\n    }),\n    RedisModule.registerAsync({\n      imports: [ConfigModule],\n      useFactory: (configService: ConfigService) => ({\n        host: configService.get<string>('redis.host'),\n        port: configService.get<number>('redis.port'),\n        password: configService.get<string>('redis.password'),\n        db: configService.get<number>('redis.db'),\n      }),\n      inject: [ConfigService],\n    }),\n  ],\n  providers: [...providers],\n  exports: [HttpModule, CacheModule, JwtModule, ...providers],\n})\nexport class SharedModule {}\n"
  },
  {
    "path": "test/app.e2e-spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { INestApplication } from '@nestjs/common';\nimport * as request from 'supertest';\nimport { AppModule } from './../src/app.module';\n\ndescribe('AppController (e2e)', () => {\n  let app: INestApplication;\n\n  beforeEach(async () => {\n    const moduleFixture: TestingModule = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = moduleFixture.createNestApplication();\n    await app.init();\n  });\n\n  it('/ (GET)', () => {\n    return request(app.getHttpServer())\n      .get('/')\n      .expect(200)\n      .expect('Hello World!');\n  });\n});\n"
  },
  {
    "path": "test/jest-e2e.json",
    "content": "{\n  \"moduleFileExtensions\": [\"js\", \"json\", \"ts\"],\n  \"rootDir\": \".\",\n  \"testEnvironment\": \"node\",\n  \"testRegex\": \".e2e-spec.ts$\",\n  \"transform\": {\n    \"^.+\\\\.(t|j)s$\": \"ts-jest\"\n  }\n}\n"
  },
  {
    "path": "tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\"node_modules\", \"test\", \"dist\", \"**/*spec.ts\", \"docs\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"es2017\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"incremental\": true\n  }\n}"
  }
]